yetanothernguyen-shoulda-matchers 1.0.0.beta3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Appraisals +12 -0
- data/CONTRIBUTION_GUIDELINES.rdoc +10 -0
- data/Gemfile +5 -0
- data/MIT-LICENSE +22 -0
- data/README.rdoc +78 -0
- data/Rakefile +55 -0
- data/lib/shoulda-matchers.rb +1 -0
- data/lib/shoulda/matchers.rb +8 -0
- data/lib/shoulda/matchers/action_controller.rb +38 -0
- data/lib/shoulda/matchers/action_controller/assign_to_matcher.rb +114 -0
- data/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +50 -0
- data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +62 -0
- data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +54 -0
- data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +99 -0
- data/lib/shoulda/matchers/action_controller/respond_with_content_type_matcher.rb +74 -0
- data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +85 -0
- data/lib/shoulda/matchers/action_controller/route_matcher.rb +93 -0
- data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +98 -0
- data/lib/shoulda/matchers/action_controller/set_the_flash_matcher.rb +94 -0
- data/lib/shoulda/matchers/action_mailer.rb +22 -0
- data/lib/shoulda/matchers/action_mailer/have_sent_email.rb +166 -0
- data/lib/shoulda/matchers/active_model.rb +33 -0
- data/lib/shoulda/matchers/active_model/allow_mass_assignment_of_matcher.rb +83 -0
- data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +110 -0
- data/lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb +87 -0
- data/lib/shoulda/matchers/active_model/ensure_length_of_matcher.rb +141 -0
- data/lib/shoulda/matchers/active_model/helpers.rb +29 -0
- data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_model/validate_format_of_matcher.rb +65 -0
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +39 -0
- data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +61 -0
- data/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb +148 -0
- data/lib/shoulda/matchers/active_model/validation_matcher.rb +56 -0
- data/lib/shoulda/matchers/active_record.rb +24 -0
- data/lib/shoulda/matchers/active_record/association_matcher.rb +226 -0
- data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +169 -0
- data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +112 -0
- data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +59 -0
- data/lib/shoulda/matchers/assertion_error.rb +11 -0
- data/lib/shoulda/matchers/integrations/rspec.rb +38 -0
- data/lib/shoulda/matchers/integrations/test_unit.rb +54 -0
- data/lib/shoulda/matchers/version.rb +5 -0
- metadata +169 -0
@@ -0,0 +1,148 @@
|
|
1
|
+
module Shoulda # :nodoc:
|
2
|
+
module Matchers
|
3
|
+
module ActiveModel # :nodoc:
|
4
|
+
|
5
|
+
# Ensures that the model is invalid if the given attribute is not unique.
|
6
|
+
#
|
7
|
+
# Internally, this uses values from existing records to test validations,
|
8
|
+
# so this will always fail if you have not saved at least one record for
|
9
|
+
# the model being tested, like so:
|
10
|
+
#
|
11
|
+
# describe User do
|
12
|
+
# before(:each) { User.create!(:email => 'address@example.com') }
|
13
|
+
# it { should validate_uniqueness_of(:email) }
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# Options:
|
17
|
+
#
|
18
|
+
# * <tt>with_message</tt> - value the test expects to find in
|
19
|
+
# <tt>errors.on(:attribute)</tt>. <tt>Regexp</tt> or <tt>String</tt>.
|
20
|
+
# Defaults to the translation for <tt>:taken</tt>.
|
21
|
+
# * <tt>scoped_to</tt> - field(s) to scope the uniqueness to.
|
22
|
+
# * <tt>case_insensitive</tt> - ensures that the validation does not
|
23
|
+
# check case. Off by default. Ignored by non-text attributes.
|
24
|
+
#
|
25
|
+
# Examples:
|
26
|
+
# it { should validate_uniqueness_of(:keyword) }
|
27
|
+
# it { should validate_uniqueness_of(:keyword).with_message(/dup/) }
|
28
|
+
# it { should validate_uniqueness_of(:email).scoped_to(:name) }
|
29
|
+
# it { should validate_uniqueness_of(:email).
|
30
|
+
# scoped_to(:first_name, :last_name) }
|
31
|
+
# it { should validate_uniqueness_of(:keyword).case_insensitive }
|
32
|
+
#
|
33
|
+
def validate_uniqueness_of(attr)
|
34
|
+
ValidateUniquenessOfMatcher.new(attr)
|
35
|
+
end
|
36
|
+
|
37
|
+
class ValidateUniquenessOfMatcher < ValidationMatcher # :nodoc:
|
38
|
+
include Helpers
|
39
|
+
|
40
|
+
def initialize(attribute)
|
41
|
+
@attribute = attribute
|
42
|
+
end
|
43
|
+
|
44
|
+
def scoped_to(*scopes)
|
45
|
+
@scopes = [*scopes].flatten
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def with_message(message)
|
50
|
+
@expected_message = message
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def case_insensitive
|
55
|
+
@case_insensitive = true
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
def description
|
60
|
+
result = "require "
|
61
|
+
result << "case sensitive " unless @case_insensitive
|
62
|
+
result << "unique value for #{@attribute}"
|
63
|
+
result << " scoped to #{@scopes.join(', ')}" unless @scopes.blank?
|
64
|
+
result
|
65
|
+
end
|
66
|
+
|
67
|
+
def matches?(subject)
|
68
|
+
@subject = subject.class.new
|
69
|
+
@expected_message ||= :taken
|
70
|
+
find_existing &&
|
71
|
+
set_scoped_attributes &&
|
72
|
+
validate_attribute &&
|
73
|
+
validate_after_scope_change
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def find_existing
|
79
|
+
if @existing = @subject.class.first
|
80
|
+
true
|
81
|
+
else
|
82
|
+
@failure_message = "Can't find first #{class_name}"
|
83
|
+
false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def set_scoped_attributes
|
88
|
+
unless @scopes.blank?
|
89
|
+
@scopes.each do |scope|
|
90
|
+
setter = :"#{scope}="
|
91
|
+
unless @subject.respond_to?(setter)
|
92
|
+
@failure_message =
|
93
|
+
"#{class_name} doesn't seem to have a #{scope} attribute."
|
94
|
+
return false
|
95
|
+
end
|
96
|
+
@subject.send("#{scope}=", @existing.send(scope))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
def validate_attribute
|
103
|
+
disallows_value_of(existing_value, @expected_message)
|
104
|
+
end
|
105
|
+
|
106
|
+
# TODO: There is a chance that we could change the scoped field
|
107
|
+
# to a value that's already taken. An alternative implementation
|
108
|
+
# could actually find all values for scope and create a unique
|
109
|
+
def validate_after_scope_change
|
110
|
+
if @scopes.blank?
|
111
|
+
true
|
112
|
+
else
|
113
|
+
@scopes.all? do |scope|
|
114
|
+
previous_value = @existing.send(scope)
|
115
|
+
|
116
|
+
# Assume the scope is a foreign key if the field is nil
|
117
|
+
previous_value ||= 0
|
118
|
+
|
119
|
+
next_value = previous_value.next
|
120
|
+
|
121
|
+
@subject.send("#{scope}=", next_value)
|
122
|
+
|
123
|
+
if allows_value_of(existing_value, @expected_message)
|
124
|
+
@negative_failure_message <<
|
125
|
+
" (with different value of #{scope})"
|
126
|
+
true
|
127
|
+
else
|
128
|
+
@failure_message << " (with different value of #{scope})"
|
129
|
+
false
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def class_name
|
136
|
+
@subject.class.name
|
137
|
+
end
|
138
|
+
|
139
|
+
def existing_value
|
140
|
+
value = @existing.send(@attribute)
|
141
|
+
value.swapcase! if @case_insensitive && value.respond_to?(:swapcase!)
|
142
|
+
value
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Shoulda # :nodoc:
|
2
|
+
module Matchers
|
3
|
+
module ActiveModel # :nodoc:
|
4
|
+
|
5
|
+
class ValidationMatcher # :nodoc:
|
6
|
+
|
7
|
+
attr_reader :failure_message
|
8
|
+
|
9
|
+
def initialize(attribute)
|
10
|
+
@attribute = attribute
|
11
|
+
end
|
12
|
+
|
13
|
+
def negative_failure_message
|
14
|
+
@negative_failure_message || @failure_message
|
15
|
+
end
|
16
|
+
|
17
|
+
def matches?(subject)
|
18
|
+
@subject = subject
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def allows_value_of(value, message = nil)
|
25
|
+
allow = AllowValueMatcher.
|
26
|
+
new(value).
|
27
|
+
for(@attribute).
|
28
|
+
with_message(message)
|
29
|
+
if allow.matches?(@subject)
|
30
|
+
@negative_failure_message = allow.failure_message
|
31
|
+
true
|
32
|
+
else
|
33
|
+
@failure_message = allow.negative_failure_message
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def disallows_value_of(value, message = nil)
|
39
|
+
disallow = AllowValueMatcher.
|
40
|
+
new(value).
|
41
|
+
for(@attribute).
|
42
|
+
with_message(message)
|
43
|
+
if disallow.matches?(@subject)
|
44
|
+
@failure_message = disallow.negative_failure_message
|
45
|
+
false
|
46
|
+
else
|
47
|
+
@negative_failure_message = disallow.failure_message
|
48
|
+
true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'shoulda/matchers/active_record/association_matcher'
|
2
|
+
require 'shoulda/matchers/active_record/have_db_column_matcher'
|
3
|
+
require 'shoulda/matchers/active_record/have_db_index_matcher'
|
4
|
+
require 'shoulda/matchers/active_record/have_readonly_attribute_matcher'
|
5
|
+
|
6
|
+
|
7
|
+
module Shoulda
|
8
|
+
module Matchers
|
9
|
+
# = Matchers for your active record models
|
10
|
+
#
|
11
|
+
# These matchers will test the associations for your
|
12
|
+
# ActiveRecord models.
|
13
|
+
#
|
14
|
+
# describe User do
|
15
|
+
# it { should have_one(:profile) }
|
16
|
+
# it { should have_many(:dogs) }
|
17
|
+
# it { should have_many(:messes).through(:dogs) }
|
18
|
+
# it { should belong_to(:lover) }
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
module ActiveRecord
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
module Shoulda # :nodoc:
|
2
|
+
module Matchers
|
3
|
+
module ActiveRecord # :nodoc:
|
4
|
+
|
5
|
+
# Ensure that the belongs_to relationship exists.
|
6
|
+
#
|
7
|
+
# it { should belong_to(:parent) }
|
8
|
+
#
|
9
|
+
def belong_to(name)
|
10
|
+
AssociationMatcher.new(:belongs_to, name)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Ensures that the has_many relationship exists. Will also test that the
|
14
|
+
# associated table has the required columns. Works with polymorphic
|
15
|
+
# associations.
|
16
|
+
#
|
17
|
+
# Options:
|
18
|
+
# * <tt>through</tt> - association name for <tt>has_many :through</tt>
|
19
|
+
# * <tt>dependent</tt> - tests that the association makes use of the
|
20
|
+
# dependent option.
|
21
|
+
#
|
22
|
+
# Example:
|
23
|
+
# it { should have_many(:friends) }
|
24
|
+
# it { should have_many(:enemies).through(:friends) }
|
25
|
+
# it { should have_many(:enemies).dependent(:destroy) }
|
26
|
+
#
|
27
|
+
def have_many(name)
|
28
|
+
AssociationMatcher.new(:has_many, name)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Ensure that the has_one relationship exists. Will also test that the
|
32
|
+
# associated table has the required columns. Works with polymorphic
|
33
|
+
# associations.
|
34
|
+
#
|
35
|
+
# Options:
|
36
|
+
# * <tt>:dependent</tt> - tests that the association makes use of the
|
37
|
+
# dependent option.
|
38
|
+
#
|
39
|
+
# Example:
|
40
|
+
# it { should have_one(:god) } # unless hindu
|
41
|
+
#
|
42
|
+
def have_one(name)
|
43
|
+
AssociationMatcher.new(:has_one, name)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Ensures that the has_and_belongs_to_many relationship exists, and that
|
47
|
+
# the join table is in place.
|
48
|
+
#
|
49
|
+
# it { should have_and_belong_to_many(:posts) }
|
50
|
+
#
|
51
|
+
def have_and_belong_to_many(name)
|
52
|
+
AssociationMatcher.new(:has_and_belongs_to_many, name)
|
53
|
+
end
|
54
|
+
|
55
|
+
class AssociationMatcher # :nodoc:
|
56
|
+
def initialize(macro, name)
|
57
|
+
@macro = macro
|
58
|
+
@name = name
|
59
|
+
end
|
60
|
+
|
61
|
+
def through(through)
|
62
|
+
@through = through
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
def dependent(dependent)
|
67
|
+
@dependent = dependent
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def matches?(subject)
|
72
|
+
@subject = subject
|
73
|
+
association_exists? &&
|
74
|
+
macro_correct? &&
|
75
|
+
foreign_key_exists? &&
|
76
|
+
through_association_valid? &&
|
77
|
+
dependent_correct? &&
|
78
|
+
join_table_exists?
|
79
|
+
end
|
80
|
+
|
81
|
+
def failure_message
|
82
|
+
"Expected #{expectation} (#{@missing})"
|
83
|
+
end
|
84
|
+
|
85
|
+
def negative_failure_message
|
86
|
+
"Did not expect #{expectation}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def description
|
90
|
+
description = "#{macro_description} #{@name}"
|
91
|
+
description += " through #{@through}" if @through
|
92
|
+
description += " dependent => #{@dependent}" if @dependent
|
93
|
+
description
|
94
|
+
end
|
95
|
+
|
96
|
+
protected
|
97
|
+
|
98
|
+
def association_exists?
|
99
|
+
if reflection.nil?
|
100
|
+
@missing = "no association called #{@name}"
|
101
|
+
false
|
102
|
+
else
|
103
|
+
true
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def macro_correct?
|
108
|
+
if reflection.macro == @macro
|
109
|
+
true
|
110
|
+
else
|
111
|
+
@missing = "actual association type was #{reflection.macro}"
|
112
|
+
false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def foreign_key_exists?
|
117
|
+
!(belongs_foreign_key_missing? || has_foreign_key_missing?)
|
118
|
+
end
|
119
|
+
|
120
|
+
def belongs_foreign_key_missing?
|
121
|
+
@macro == :belongs_to && !class_has_foreign_key?(model_class)
|
122
|
+
end
|
123
|
+
|
124
|
+
def has_foreign_key_missing?
|
125
|
+
[:has_many, :has_one].include?(@macro) &&
|
126
|
+
!through? &&
|
127
|
+
!class_has_foreign_key?(associated_class)
|
128
|
+
end
|
129
|
+
|
130
|
+
def through_association_valid?
|
131
|
+
@through.nil? || (through_association_exists? && through_association_correct?)
|
132
|
+
end
|
133
|
+
|
134
|
+
def through_association_exists?
|
135
|
+
if through_reflection.nil?
|
136
|
+
@missing = "#{model_class.name} does not have any relationship to #{@through}"
|
137
|
+
false
|
138
|
+
else
|
139
|
+
true
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def through_association_correct?
|
144
|
+
if @through == reflection.options[:through]
|
145
|
+
true
|
146
|
+
else
|
147
|
+
@missing = "Expected #{model_class.name} to have #{@name} through #{@through}, " <<
|
148
|
+
"but got it through #{reflection.options[:through]}"
|
149
|
+
false
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def dependent_correct?
|
154
|
+
if @dependent.nil? || @dependent.to_s == reflection.options[:dependent].to_s
|
155
|
+
true
|
156
|
+
else
|
157
|
+
@missing = "#{@name} should have #{@dependent} dependency"
|
158
|
+
false
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def join_table_exists?
|
163
|
+
if @macro != :has_and_belongs_to_many ||
|
164
|
+
::ActiveRecord::Base.connection.tables.include?(join_table.to_s)
|
165
|
+
true
|
166
|
+
else
|
167
|
+
@missing = "join table #{join_table} doesn't exist"
|
168
|
+
false
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def class_has_foreign_key?(klass)
|
173
|
+
if klass.column_names.include?(foreign_key.to_s)
|
174
|
+
true
|
175
|
+
else
|
176
|
+
@missing = "#{klass} does not have a #{foreign_key} foreign key."
|
177
|
+
false
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def model_class
|
182
|
+
@subject.class
|
183
|
+
end
|
184
|
+
|
185
|
+
def join_table
|
186
|
+
reflection.options[:join_table]
|
187
|
+
end
|
188
|
+
|
189
|
+
def associated_class
|
190
|
+
reflection.klass
|
191
|
+
end
|
192
|
+
|
193
|
+
def foreign_key
|
194
|
+
reflection.respond_to?(:foreign_key) ? reflection.foreign_key : reflection.primary_key_name
|
195
|
+
end
|
196
|
+
|
197
|
+
def through?
|
198
|
+
reflection.options[:through]
|
199
|
+
end
|
200
|
+
|
201
|
+
def reflection
|
202
|
+
@reflection ||= model_class.reflect_on_association(@name)
|
203
|
+
end
|
204
|
+
|
205
|
+
def through_reflection
|
206
|
+
@through_reflection ||= model_class.reflect_on_association(@through)
|
207
|
+
end
|
208
|
+
|
209
|
+
def expectation
|
210
|
+
"#{model_class.name} to have a #{@macro} association called #{@name}"
|
211
|
+
end
|
212
|
+
|
213
|
+
def macro_description
|
214
|
+
case @macro.to_s
|
215
|
+
when 'belongs_to' then 'belong to'
|
216
|
+
when 'has_many' then 'have many'
|
217
|
+
when 'has_one' then 'have one'
|
218
|
+
when 'has_and_belongs_to_many' then
|
219
|
+
'have and belong to many'
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|