shoulda-matchers 2.0.0 → 2.1.0

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 (39) hide show
  1. checksums.yaml +8 -8
  2. data/.travis.yml +9 -15
  3. data/Appraisals +10 -8
  4. data/Gemfile.lock +1 -1
  5. data/NEWS.md +62 -32
  6. data/gemfiles/3.0.gemfile.lock +2 -2
  7. data/gemfiles/3.1.gemfile.lock +2 -2
  8. data/gemfiles/3.2.gemfile +1 -1
  9. data/gemfiles/3.2.gemfile.lock +3 -3
  10. data/lib/shoulda/matchers/active_model.rb +1 -0
  11. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +7 -2
  12. data/lib/shoulda/matchers/active_model/ensure_exclusion_of_matcher.rb +1 -1
  13. data/lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb +1 -1
  14. data/lib/shoulda/matchers/active_model/ensure_length_of_matcher.rb +2 -0
  15. data/lib/shoulda/matchers/active_model/exception_message_finder.rb +3 -2
  16. data/lib/shoulda/matchers/active_model/helpers.rb +5 -2
  17. data/lib/shoulda/matchers/active_model/odd_even_number_matcher.rb +47 -0
  18. data/lib/shoulda/matchers/active_model/only_integer_matcher.rb +4 -0
  19. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +26 -3
  20. data/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb +68 -16
  21. data/lib/shoulda/matchers/active_model/validation_matcher.rb +5 -0
  22. data/lib/shoulda/matchers/active_model/validation_message_finder.rb +3 -2
  23. data/lib/shoulda/matchers/active_record/association_matcher.rb +29 -4
  24. data/lib/shoulda/matchers/version.rb +1 -1
  25. data/shoulda-matchers.gemspec +1 -0
  26. data/spec/shoulda/active_model/validate_uniqueness_of_matcher_spec.rb +195 -0
  27. data/spec/shoulda/matchers/active_model/allow_value_matcher_spec.rb +18 -0
  28. data/spec/shoulda/matchers/active_model/ensure_exclusion_of_matcher_spec.rb +21 -0
  29. data/spec/shoulda/matchers/active_model/ensure_inclusion_of_matcher_spec.rb +23 -0
  30. data/spec/shoulda/matchers/active_model/ensure_length_of_matcher_spec.rb +46 -0
  31. data/spec/shoulda/matchers/active_model/odd_even_number_matcher_spec.rb +93 -0
  32. data/spec/shoulda/matchers/active_model/only_integer_matcher_spec.rb +4 -4
  33. data/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +60 -0
  34. data/spec/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +16 -0
  35. data/spec/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb +45 -8
  36. data/spec/shoulda/matchers/active_model/validation_message_finder_spec.rb +13 -0
  37. data/spec/shoulda/matchers/active_record/association_matcher_spec.rb +50 -2
  38. data/spec/support/i18n_faker.rb +10 -0
  39. metadata +10 -3
@@ -27,6 +27,10 @@ module Shoulda # :nodoc:
27
27
  def failure_message_for_should
28
28
  @disallow_value_matcher.failure_message_for_should
29
29
  end
30
+
31
+ def failure_message_for_should_not
32
+ @disallow_value_matcher.failure_message_for_should_not
33
+ end
30
34
  end
31
35
  end
32
36
  end
@@ -8,10 +8,14 @@ module Shoulda # :nodoc:
8
8
  # <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
9
9
  # translation for <tt>:not_a_number</tt>.
10
10
  # * <tt>only_integer</tt> - allows only integer values
11
+ # * <tt>odd</tt> - Specifies the value must be an odd number.
12
+ # * <tt>even</tt> - Specifies the value must be an even number.
11
13
  #
12
14
  # Examples:
13
15
  # it { should validate_numericality_of(:price) }
14
16
  # it { should validate_numericality_of(:age).only_integer }
17
+ # it { should validate_numericality_of(:frequency).odd }
18
+ # it { should validate_numericality_of(:frequency).even }
15
19
  #
16
20
  def validate_numericality_of(attr)
17
21
  ValidateNumericalityOfMatcher.new(attr)
@@ -22,7 +26,6 @@ module Shoulda # :nodoc:
22
26
 
23
27
  def initialize(attribute)
24
28
  @attribute = attribute
25
- @options = {}
26
29
  @submatchers = []
27
30
 
28
31
  add_disallow_value_matcher
@@ -34,6 +37,18 @@ module Shoulda # :nodoc:
34
37
  self
35
38
  end
36
39
 
40
+ def odd
41
+ odd_number_matcher = OddEvenNumberMatcher.new(@attribute, :odd => true)
42
+ add_submatcher(odd_number_matcher)
43
+ self
44
+ end
45
+
46
+ def even
47
+ even_number_matcher = OddEvenNumberMatcher.new(@attribute, :even => true)
48
+ add_submatcher(even_number_matcher)
49
+ self
50
+ end
51
+
37
52
  def with_message(message)
38
53
  @submatchers.each { |matcher| matcher.with_message(message) }
39
54
  self
@@ -49,7 +64,11 @@ module Shoulda # :nodoc:
49
64
  end
50
65
 
51
66
  def failure_message_for_should
52
- submatcher_failure_messages.last
67
+ submatcher_failure_messages_for_should.last
68
+ end
69
+
70
+ def failure_message_for_should_not
71
+ submatcher_failure_messages_for_should_not.last
53
72
  end
54
73
 
55
74
  private
@@ -70,10 +89,14 @@ module Shoulda # :nodoc:
70
89
  failing_submatchers.empty?
71
90
  end
72
91
 
73
- def submatcher_failure_messages
92
+ def submatcher_failure_messages_for_should
74
93
  failing_submatchers.map(&:failure_message_for_should)
75
94
  end
76
95
 
96
+ def submatcher_failure_messages_for_should_not
97
+ failing_submatchers.map(&:failure_message_for_should_not)
98
+ end
99
+
77
100
  def failing_submatchers
78
101
  @failing_submatchers ||= @submatchers.select { |matcher| !matcher.matches?(@subject) }
79
102
  end
@@ -55,12 +55,26 @@ module Shoulda # :nodoc:
55
55
  self
56
56
  end
57
57
 
58
+ def allow_nil
59
+ @options[:allow_nil] = true
60
+ self
61
+ end
62
+
63
+ def description
64
+ result = "require "
65
+ result << "case sensitive " unless @options[:case_insensitive]
66
+ result << "unique value for #{@attribute}"
67
+ result << " scoped to #{@options[:scopes].join(', ')}" if @options[:scopes].present?
68
+ result
69
+ end
70
+
58
71
  def matches?(subject)
59
72
  @subject = subject.class.new
60
73
  @expected_message ||= :taken
61
74
  set_scoped_attributes &&
62
- validate_attribute? &&
63
- validate_after_scope_change?
75
+ validate_everything_except_duplicate_nils? &&
76
+ validate_after_scope_change? &&
77
+ allows_nil?
64
78
  end
65
79
 
66
80
  def description
@@ -73,17 +87,42 @@ module Shoulda # :nodoc:
73
87
 
74
88
  private
75
89
 
76
- def existing
77
- @existing ||= first_instance
90
+ def allows_nil?
91
+ if @options[:allow_nil]
92
+ ensure_nil_record_in_database
93
+ allows_value_of(nil, @expected_message)
94
+ else
95
+ true
96
+ end
97
+ end
98
+
99
+ def existing_record
100
+ @existing_record ||= first_instance
78
101
  end
79
102
 
80
103
  def first_instance
81
- @subject.class.first || create_instance_in_database
104
+ @subject.class.first || create_record_in_database
82
105
  end
83
106
 
84
- def create_instance_in_database
107
+ def ensure_nil_record_in_database
108
+ unless existing_record_is_nil?
109
+ create_record_in_database(nil_value: true)
110
+ end
111
+ end
112
+
113
+ def existing_record_is_nil?
114
+ @existing_record.present? && existing_value.nil?
115
+ end
116
+
117
+ def create_record_in_database(options = {})
118
+ if options[:nil_value]
119
+ value = nil
120
+ else
121
+ value = "arbitrary_string"
122
+ end
123
+
85
124
  @subject.class.new.tap do |instance|
86
- instance.send("#{@attribute}=", 'arbitrary_string')
125
+ instance.send("#{@attribute}=", value)
87
126
  instance.save(:validate => false)
88
127
  end
89
128
  end
@@ -93,7 +132,7 @@ module Shoulda # :nodoc:
93
132
  @options[:scopes].all? do |scope|
94
133
  setter = :"#{scope}="
95
134
  if @subject.respond_to?(setter)
96
- @subject.send(setter, existing.send(scope))
135
+ @subject.send(setter, existing_record.send(scope))
97
136
  true
98
137
  else
99
138
  @failure_message_for_should = "#{class_name} doesn't seem to have a #{scope} attribute."
@@ -105,10 +144,18 @@ module Shoulda # :nodoc:
105
144
  end
106
145
  end
107
146
 
108
- def validate_attribute?
147
+ def validate_everything_except_duplicate_nils?
148
+ if @options[:allow_nil] && existing_value.nil?
149
+ create_record_without_nil
150
+ end
151
+
109
152
  disallows_value_of(existing_value, @expected_message)
110
153
  end
111
154
 
155
+ def create_record_without_nil
156
+ @existing_record = create_record_in_database
157
+ end
158
+
112
159
  # TODO: There is a chance that we could change the scoped field
113
160
  # to a value that's already taken. An alternative implementation
114
161
  # could actually find all values for scope and create a unique
@@ -117,16 +164,19 @@ module Shoulda # :nodoc:
117
164
  true
118
165
  else
119
166
  @options[:scopes].all? do |scope|
120
- previous_value = existing.send(scope)
167
+ previous_value = existing_record.send(scope)
121
168
 
122
169
  # Assume the scope is a foreign key if the field is nil
123
170
  previous_value ||= correct_type_for_column(@subject.class.columns_hash[scope.to_s])
124
171
 
125
- next_value = if previous_value.respond_to?(:next)
126
- previous_value.next
127
- else
128
- previous_value.to_s.next
129
- end
172
+ next_value =
173
+ if previous_value.respond_to?(:next)
174
+ previous_value.next
175
+ elsif previous_value.respond_to?(:to_datetime)
176
+ previous_value.to_datetime.next
177
+ else
178
+ previous_value.to_s.next
179
+ end
130
180
 
131
181
  @subject.send("#{scope}=", next_value)
132
182
 
@@ -147,6 +197,8 @@ module Shoulda # :nodoc:
147
197
  def correct_type_for_column(column)
148
198
  if column.type == :string
149
199
  '0'
200
+ elsif column.type == :datetime
201
+ DateTime.now
150
202
  else
151
203
  0
152
204
  end
@@ -157,7 +209,7 @@ module Shoulda # :nodoc:
157
209
  end
158
210
 
159
211
  def existing_value
160
- value = existing.send(@attribute)
212
+ value = existing_record.send(@attribute)
161
213
  if @options[:case_insensitive] && value.respond_to?(:swapcase!)
162
214
  value.swapcase!
163
215
  end
@@ -9,6 +9,11 @@ module Shoulda # :nodoc:
9
9
  @strict = false
10
10
  end
11
11
 
12
+ def on(context)
13
+ @context = context
14
+ self
15
+ end
16
+
12
17
  def strict
13
18
  @strict = true
14
19
  self
@@ -6,9 +6,10 @@ module Shoulda
6
6
  class ValidationMessageFinder
7
7
  include Helpers
8
8
 
9
- def initialize(instance, attribute)
9
+ def initialize(instance, attribute, context=nil)
10
10
  @instance = instance
11
11
  @attribute = attribute
12
+ @context = context
12
13
  end
13
14
 
14
15
  def allow_description(allowed_values)
@@ -58,7 +59,7 @@ module Shoulda
58
59
  end
59
60
 
60
61
  def validate_instance
61
- @instance.valid?
62
+ @instance.valid?(@context)
62
63
  @instance
63
64
  end
64
65
  end
@@ -7,6 +7,8 @@ module Shoulda # :nodoc:
7
7
  # * <tt>:class_name</tt> - tests that the association resolves to class_name.
8
8
  # * <tt>:validate</tt> - tests that the association makes use of the validate
9
9
  # option.
10
+ # * <tt>:touch</tt> - tests that the association makes use of the touch
11
+ # option.
10
12
  #
11
13
  # Example:
12
14
  # it { should belong_to(:parent) }
@@ -107,7 +109,12 @@ module Shoulda # :nodoc:
107
109
  end
108
110
 
109
111
  def validate(validate = true)
110
- @validate = validate
112
+ @options[:validate] = validate
113
+ self
114
+ end
115
+
116
+ def touch(touch = true)
117
+ @options[:touch] = touch
111
118
  self
112
119
  end
113
120
 
@@ -122,7 +129,8 @@ module Shoulda # :nodoc:
122
129
  order_correct? &&
123
130
  conditions_correct? &&
124
131
  join_table_exists? &&
125
- validate_correct?
132
+ validate_correct? &&
133
+ touch_correct?
126
134
  end
127
135
 
128
136
  def failure_message_for_should
@@ -258,14 +266,31 @@ module Shoulda # :nodoc:
258
266
  end
259
267
 
260
268
  def validate_correct?
261
- if !@validate && !reflection.options[:validate] || @validate == reflection.options[:validate]
269
+ if option_correct?(:validate)
270
+ true
271
+ else
272
+ @missing = "#{@name} should have :validate => #{@options[:validate]}"
273
+ false
274
+ end
275
+ end
276
+
277
+ def touch_correct?
278
+ if option_correct?(:touch)
262
279
  true
263
280
  else
264
- @missing = "#{@name} should have :validate => #{@validate}"
281
+ @missing = "#{@name} should have :touch => #{@options[:touch]}"
265
282
  false
266
283
  end
267
284
  end
268
285
 
286
+ def option_correct?(key)
287
+ !@options.key?(key) || reflection_set_properly_for?(key)
288
+ end
289
+
290
+ def reflection_set_properly_for?(key)
291
+ @options[key] == !!reflection.options[key]
292
+ end
293
+
269
294
  def class_has_foreign_key?(klass)
270
295
  if @options.key?(:foreign_key)
271
296
  reflection.options[:foreign_key] == @options[:foreign_key]
@@ -1,5 +1,5 @@
1
1
  module Shoulda
2
2
  module Matchers
3
- VERSION = '2.0.0'.freeze
3
+ VERSION = '2.1.0'.freeze
4
4
  end
5
5
  end
@@ -18,6 +18,7 @@ Gem::Specification.new do |s|
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
20
 
21
+ s.required_ruby_version = '>= 1.9.2'
21
22
  s.add_dependency('activesupport', '>= 3.0.0')
22
23
 
23
24
  s.add_development_dependency('appraisal', '~> 0.4')
@@ -0,0 +1,195 @@
1
+ require 'spec_helper'
2
+
3
+ describe Shoulda::Matchers::ActiveModel::ValidateUniquenessOfMatcher do
4
+ context "a unique attribute" do
5
+ before do
6
+ @model = define_model(:example, :attr => :string,
7
+ :other => :integer) do
8
+ attr_accessible :attr, :other
9
+ validates_uniqueness_of :attr
10
+ end.new
11
+ end
12
+
13
+ context "with an existing value" do
14
+ before do
15
+ @existing = Example.create!(:attr => 'value', :other => 1)
16
+ end
17
+
18
+ it "should require a unique value for that attribute" do
19
+ @model.should validate_uniqueness_of(:attr)
20
+ end
21
+
22
+ it "should pass when the subject is an existing record" do
23
+ @existing.should validate_uniqueness_of(:attr)
24
+ end
25
+
26
+ it "should fail when a scope is specified" do
27
+ @model.should_not validate_uniqueness_of(:attr).scoped_to(:other)
28
+ end
29
+ end
30
+
31
+ context "without an existing value" do
32
+ before do
33
+ Example.first.should be_nil
34
+ @matcher = validate_uniqueness_of(:attr)
35
+ end
36
+
37
+ it "does not not require a created instance" do
38
+ @model.should @matcher
39
+ end
40
+ end
41
+ end
42
+
43
+ context "a unique attribute with a custom error and an existing value" do
44
+ before do
45
+ @model = define_model(:example, :attr => :string) do
46
+ attr_accessible :attr
47
+ validates_uniqueness_of :attr, :message => 'Bad value'
48
+ end.new
49
+ Example.create!(:attr => 'value')
50
+ end
51
+
52
+ it "should fail when checking the default message" do
53
+ @model.should_not validate_uniqueness_of(:attr)
54
+ end
55
+
56
+ it "should fail when checking a message that doesn't match" do
57
+ @model.should_not validate_uniqueness_of(:attr).with_message(/abc/i)
58
+ end
59
+
60
+ it "should pass when checking a message that matches" do
61
+ @model.should validate_uniqueness_of(:attr).with_message(/bad/i)
62
+ end
63
+ end
64
+
65
+ context "a scoped unique attribute with an existing value" do
66
+ before do
67
+ @model = define_model(:example, :attr => :string,
68
+ :scope1 => :integer,
69
+ :scope2 => :integer,
70
+ :other => :integer) do
71
+ attr_accessible :attr, :scope1, :scope2, :other
72
+ validates_uniqueness_of :attr, :scope => [:scope1, :scope2]
73
+ end.new
74
+ @existing = Example.create!(:attr => 'value', :scope1 => 1, :scope2 => 2, :other => 3)
75
+ end
76
+
77
+ it "should pass when the correct scope is specified" do
78
+ @model.should validate_uniqueness_of(:attr).scoped_to(:scope1, :scope2)
79
+ end
80
+
81
+ it "should pass when the subject is an existing record" do
82
+ @existing.should validate_uniqueness_of(:attr).scoped_to(:scope1, :scope2)
83
+ end
84
+
85
+ it "should fail when too narrow of a scope is specified" do
86
+ @model.should_not validate_uniqueness_of(:attr).scoped_to(:scope1, :scope2, :other)
87
+ end
88
+
89
+ it "should fail when too broad of a scope is specified" do
90
+ @model.should_not validate_uniqueness_of(:attr).scoped_to(:scope1)
91
+ end
92
+
93
+ it "should fail when a different scope is specified" do
94
+ @model.should_not validate_uniqueness_of(:attr).scoped_to(:other)
95
+ end
96
+
97
+ it "should fail when no scope is specified" do
98
+ @model.should_not validate_uniqueness_of(:attr)
99
+ end
100
+
101
+ it "should fail when a non-existent attribute is specified as a scope" do
102
+ @model.should_not validate_uniqueness_of(:attr).scoped_to(:fake)
103
+ end
104
+ end
105
+
106
+ context "a non-unique attribute with an existing value" do
107
+ before do
108
+ @model = define_model(:example, :attr => :string) do
109
+ attr_accessible :attr
110
+ end.new
111
+ Example.create!(:attr => 'value')
112
+ end
113
+
114
+ it "should not require a unique value for that attribute" do
115
+ @model.should_not validate_uniqueness_of(:attr)
116
+ end
117
+ end
118
+
119
+ context "a case sensitive unique attribute with an existing value" do
120
+ before do
121
+ @model = define_model(:example, :attr => :string) do
122
+ attr_accessible :attr
123
+ validates_uniqueness_of :attr, :case_sensitive => true
124
+ end.new
125
+ Example.create!(:attr => 'value')
126
+ end
127
+
128
+ it "should not require a unique, case-insensitive value for that attribute" do
129
+ @model.should_not validate_uniqueness_of(:attr).case_insensitive
130
+ end
131
+
132
+ it "should require a unique, case-sensitive value for that attribute" do
133
+ @model.should validate_uniqueness_of(:attr)
134
+ end
135
+ end
136
+
137
+ context "a case sensitive unique integer attribute with an existing value" do
138
+ before do
139
+ @model = define_model(:example, :attr => :integer) do
140
+ attr_accessible :attr
141
+ validates_uniqueness_of :attr, :case_sensitive => true
142
+ end.new
143
+ Example.create!(:attr => 'value')
144
+ end
145
+
146
+ it "should require a unique, case-insensitive value for that attribute" do
147
+ @model.should validate_uniqueness_of(:attr).case_insensitive
148
+ end
149
+
150
+ it "should require a unique, case-sensitive value for that attribute" do
151
+ @model.should validate_uniqueness_of(:attr)
152
+ end
153
+ end
154
+
155
+ context "when the validation allows nil" do
156
+ before do
157
+ @model = define_model(:example, :attr => :integer) do
158
+ attr_accessible :attr
159
+ validates_uniqueness_of :attr, :allow_nil => true
160
+ end.new
161
+ end
162
+
163
+ context "when there is an existing entry with a nil" do
164
+ it "should allow_nil" do
165
+ Example.create!(:attr => nil)
166
+ @model.should validate_uniqueness_of(:attr).allow_nil
167
+ end
168
+ end
169
+
170
+ it "should create a nil and verify that it is allowed" do
171
+ @model.should validate_uniqueness_of(:attr).allow_nil
172
+ Example.all.any?{ |instance| instance.attr.nil? }
173
+ end
174
+ end
175
+
176
+ context "when the validation does not allow a nil" do
177
+ before do
178
+ @model = define_model(:example, :attr => :integer) do
179
+ attr_accessible :attr
180
+ validates_uniqueness_of :attr
181
+ end.new
182
+ end
183
+
184
+ context "when there is an existing entry with a nil" do
185
+ it "should not allow_nil" do
186
+ Example.create!(:attr => nil)
187
+ @model.should_not validate_uniqueness_of(:attr).allow_nil
188
+ end
189
+ end
190
+
191
+ it "should not allow_nil" do
192
+ @model.should_not validate_uniqueness_of(:attr).allow_nil
193
+ end
194
+ end
195
+ end