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.
Files changed (43) hide show
  1. data/Appraisals +12 -0
  2. data/CONTRIBUTION_GUIDELINES.rdoc +10 -0
  3. data/Gemfile +5 -0
  4. data/MIT-LICENSE +22 -0
  5. data/README.rdoc +78 -0
  6. data/Rakefile +55 -0
  7. data/lib/shoulda-matchers.rb +1 -0
  8. data/lib/shoulda/matchers.rb +8 -0
  9. data/lib/shoulda/matchers/action_controller.rb +38 -0
  10. data/lib/shoulda/matchers/action_controller/assign_to_matcher.rb +114 -0
  11. data/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +50 -0
  12. data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +62 -0
  13. data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +54 -0
  14. data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +99 -0
  15. data/lib/shoulda/matchers/action_controller/respond_with_content_type_matcher.rb +74 -0
  16. data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +85 -0
  17. data/lib/shoulda/matchers/action_controller/route_matcher.rb +93 -0
  18. data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +98 -0
  19. data/lib/shoulda/matchers/action_controller/set_the_flash_matcher.rb +94 -0
  20. data/lib/shoulda/matchers/action_mailer.rb +22 -0
  21. data/lib/shoulda/matchers/action_mailer/have_sent_email.rb +166 -0
  22. data/lib/shoulda/matchers/active_model.rb +33 -0
  23. data/lib/shoulda/matchers/active_model/allow_mass_assignment_of_matcher.rb +83 -0
  24. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +110 -0
  25. data/lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb +87 -0
  26. data/lib/shoulda/matchers/active_model/ensure_length_of_matcher.rb +141 -0
  27. data/lib/shoulda/matchers/active_model/helpers.rb +29 -0
  28. data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +41 -0
  29. data/lib/shoulda/matchers/active_model/validate_format_of_matcher.rb +65 -0
  30. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +39 -0
  31. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +61 -0
  32. data/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb +148 -0
  33. data/lib/shoulda/matchers/active_model/validation_matcher.rb +56 -0
  34. data/lib/shoulda/matchers/active_record.rb +24 -0
  35. data/lib/shoulda/matchers/active_record/association_matcher.rb +226 -0
  36. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +169 -0
  37. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +112 -0
  38. data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +59 -0
  39. data/lib/shoulda/matchers/assertion_error.rb +11 -0
  40. data/lib/shoulda/matchers/integrations/rspec.rb +38 -0
  41. data/lib/shoulda/matchers/integrations/test_unit.rb +54 -0
  42. data/lib/shoulda/matchers/version.rb +5 -0
  43. 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