shoulda-matchers 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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