mcmire-shoulda-matchers 2.5.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 (164) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +32 -0
  4. data/.yardopts +7 -0
  5. data/Appraisals +45 -0
  6. data/CONTRIBUTING.md +41 -0
  7. data/Gemfile +31 -0
  8. data/Gemfile.lock +166 -0
  9. data/MIT-LICENSE +22 -0
  10. data/NEWS.md +299 -0
  11. data/README.md +163 -0
  12. data/Rakefile +116 -0
  13. data/doc_config/gh-pages/index.html.erb +9 -0
  14. data/doc_config/yard/setup.rb +22 -0
  15. data/doc_config/yard/templates/default/fulldoc/html/css/bootstrap.css +5967 -0
  16. data/doc_config/yard/templates/default/fulldoc/html/css/full_list.css +12 -0
  17. data/doc_config/yard/templates/default/fulldoc/html/css/global.css +45 -0
  18. data/doc_config/yard/templates/default/fulldoc/html/css/solarized.css +69 -0
  19. data/doc_config/yard/templates/default/fulldoc/html/css/style.css +283 -0
  20. data/doc_config/yard/templates/default/fulldoc/html/full_list.erb +32 -0
  21. data/doc_config/yard/templates/default/fulldoc/html/full_list_class.erb +1 -0
  22. data/doc_config/yard/templates/default/fulldoc/html/full_list_method.erb +8 -0
  23. data/doc_config/yard/templates/default/fulldoc/html/js/app.js +300 -0
  24. data/doc_config/yard/templates/default/fulldoc/html/js/full_list.js +1 -0
  25. data/doc_config/yard/templates/default/fulldoc/html/js/jquery.stickyheaders.js +289 -0
  26. data/doc_config/yard/templates/default/fulldoc/html/js/underscore.min.js +6 -0
  27. data/doc_config/yard/templates/default/fulldoc/html/setup.rb +8 -0
  28. data/doc_config/yard/templates/default/layout/html/breadcrumb.erb +14 -0
  29. data/doc_config/yard/templates/default/layout/html/fonts.erb +1 -0
  30. data/doc_config/yard/templates/default/layout/html/layout.erb +23 -0
  31. data/doc_config/yard/templates/default/layout/html/search.erb +13 -0
  32. data/doc_config/yard/templates/default/layout/html/setup.rb +8 -0
  33. data/doc_config/yard/templates/default/method_details/html/source.erb +10 -0
  34. data/doc_config/yard/templates/default/module/html/box_info.erb +31 -0
  35. data/features/rails_integration.feature +113 -0
  36. data/features/step_definitions/rails_steps.rb +162 -0
  37. data/features/support/env.rb +5 -0
  38. data/gemfiles/3.0.gemfile +24 -0
  39. data/gemfiles/3.0.gemfile.lock +150 -0
  40. data/gemfiles/3.1.gemfile +27 -0
  41. data/gemfiles/3.1.gemfile.lock +173 -0
  42. data/gemfiles/3.2.gemfile +27 -0
  43. data/gemfiles/3.2.gemfile.lock +171 -0
  44. data/gemfiles/4.0.0.gemfile +28 -0
  45. data/gemfiles/4.0.0.gemfile.lock +172 -0
  46. data/gemfiles/4.0.1.gemfile +28 -0
  47. data/gemfiles/4.0.1.gemfile.lock +172 -0
  48. data/lib/shoulda-matchers.rb +1 -0
  49. data/lib/shoulda/matchers.rb +11 -0
  50. data/lib/shoulda/matchers/action_controller.rb +17 -0
  51. data/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +64 -0
  52. data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +97 -0
  53. data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +81 -0
  54. data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +117 -0
  55. data/lib/shoulda/matchers/action_controller/rescue_from_matcher.rb +114 -0
  56. data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +154 -0
  57. data/lib/shoulda/matchers/action_controller/route_matcher.rb +116 -0
  58. data/lib/shoulda/matchers/action_controller/route_params.rb +48 -0
  59. data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +164 -0
  60. data/lib/shoulda/matchers/action_controller/set_the_flash_matcher.rb +296 -0
  61. data/lib/shoulda/matchers/active_model.rb +30 -0
  62. data/lib/shoulda/matchers/active_model/allow_mass_assignment_of_matcher.rb +167 -0
  63. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +314 -0
  64. data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +46 -0
  65. data/lib/shoulda/matchers/active_model/ensure_exclusion_of_matcher.rb +160 -0
  66. data/lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb +417 -0
  67. data/lib/shoulda/matchers/active_model/ensure_length_of_matcher.rb +337 -0
  68. data/lib/shoulda/matchers/active_model/errors.rb +10 -0
  69. data/lib/shoulda/matchers/active_model/exception_message_finder.rb +58 -0
  70. data/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +92 -0
  71. data/lib/shoulda/matchers/active_model/helpers.rb +46 -0
  72. data/lib/shoulda/matchers/active_model/numericality_matchers.rb +9 -0
  73. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +75 -0
  74. data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +27 -0
  75. data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +41 -0
  76. data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +27 -0
  77. data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +26 -0
  78. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +112 -0
  79. data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +77 -0
  80. data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +121 -0
  81. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +380 -0
  82. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +89 -0
  83. data/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb +372 -0
  84. data/lib/shoulda/matchers/active_model/validation_matcher.rb +97 -0
  85. data/lib/shoulda/matchers/active_model/validation_message_finder.rb +69 -0
  86. data/lib/shoulda/matchers/active_record.rb +22 -0
  87. data/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb +204 -0
  88. data/lib/shoulda/matchers/active_record/association_matcher.rb +901 -0
  89. data/lib/shoulda/matchers/active_record/association_matchers.rb +9 -0
  90. data/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb +41 -0
  91. data/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb +41 -0
  92. data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +81 -0
  93. data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +65 -0
  94. data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +94 -0
  95. data/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb +41 -0
  96. data/lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb +41 -0
  97. data/lib/shoulda/matchers/active_record/association_matchers/through_matcher.rb +63 -0
  98. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +261 -0
  99. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +149 -0
  100. data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +72 -0
  101. data/lib/shoulda/matchers/active_record/serialize_matcher.rb +181 -0
  102. data/lib/shoulda/matchers/assertion_error.rb +19 -0
  103. data/lib/shoulda/matchers/error.rb +6 -0
  104. data/lib/shoulda/matchers/integrations/rspec.rb +20 -0
  105. data/lib/shoulda/matchers/integrations/test_unit.rb +30 -0
  106. data/lib/shoulda/matchers/rails_shim.rb +50 -0
  107. data/lib/shoulda/matchers/version.rb +6 -0
  108. data/lib/shoulda/matchers/warn.rb +8 -0
  109. data/shoulda-matchers.gemspec +23 -0
  110. data/spec/shoulda/matchers/action_controller/filter_param_matcher_spec.rb +22 -0
  111. data/spec/shoulda/matchers/action_controller/redirect_to_matcher_spec.rb +42 -0
  112. data/spec/shoulda/matchers/action_controller/render_template_matcher_spec.rb +78 -0
  113. data/spec/shoulda/matchers/action_controller/render_with_layout_matcher_spec.rb +63 -0
  114. data/spec/shoulda/matchers/action_controller/rescue_from_matcher_spec.rb +63 -0
  115. data/spec/shoulda/matchers/action_controller/respond_with_matcher_spec.rb +31 -0
  116. data/spec/shoulda/matchers/action_controller/route_matcher_spec.rb +70 -0
  117. data/spec/shoulda/matchers/action_controller/route_params_spec.rb +30 -0
  118. data/spec/shoulda/matchers/action_controller/set_session_matcher_spec.rb +51 -0
  119. data/spec/shoulda/matchers/action_controller/set_the_flash_matcher_spec.rb +153 -0
  120. data/spec/shoulda/matchers/active_model/allow_mass_assignment_of_matcher_spec.rb +111 -0
  121. data/spec/shoulda/matchers/active_model/allow_value_matcher_spec.rb +170 -0
  122. data/spec/shoulda/matchers/active_model/disallow_value_matcher_spec.rb +81 -0
  123. data/spec/shoulda/matchers/active_model/ensure_exclusion_of_matcher_spec.rb +95 -0
  124. data/spec/shoulda/matchers/active_model/ensure_inclusion_of_matcher_spec.rb +320 -0
  125. data/spec/shoulda/matchers/active_model/ensure_length_of_matcher_spec.rb +166 -0
  126. data/spec/shoulda/matchers/active_model/exception_message_finder_spec.rb +111 -0
  127. data/spec/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb +20 -0
  128. data/spec/shoulda/matchers/active_model/helpers_spec.rb +158 -0
  129. data/spec/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +169 -0
  130. data/spec/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +59 -0
  131. data/spec/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +59 -0
  132. data/spec/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +57 -0
  133. data/spec/shoulda/matchers/active_model/validate_absence_of_matcher_spec.rb +139 -0
  134. data/spec/shoulda/matchers/active_model/validate_acceptance_of_matcher_spec.rb +41 -0
  135. data/spec/shoulda/matchers/active_model/validate_confirmation_of_matcher_spec.rb +47 -0
  136. data/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +331 -0
  137. data/spec/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +180 -0
  138. data/spec/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb +398 -0
  139. data/spec/shoulda/matchers/active_model/validation_message_finder_spec.rb +127 -0
  140. data/spec/shoulda/matchers/active_record/accept_nested_attributes_for_matcher_spec.rb +107 -0
  141. data/spec/shoulda/matchers/active_record/association_matcher_spec.rb +860 -0
  142. data/spec/shoulda/matchers/active_record/association_matchers/model_reflection_spec.rb +247 -0
  143. data/spec/shoulda/matchers/active_record/have_db_column_matcher_spec.rb +111 -0
  144. data/spec/shoulda/matchers/active_record/have_db_index_matcher_spec.rb +78 -0
  145. data/spec/shoulda/matchers/active_record/have_readonly_attributes_matcher_spec.rb +41 -0
  146. data/spec/shoulda/matchers/active_record/serialize_matcher_spec.rb +86 -0
  147. data/spec/spec_helper.rb +26 -0
  148. data/spec/support/active_model_versions.rb +13 -0
  149. data/spec/support/active_resource_builder.rb +29 -0
  150. data/spec/support/activemodel_helpers.rb +19 -0
  151. data/spec/support/capture_helpers.rb +19 -0
  152. data/spec/support/class_builder.rb +42 -0
  153. data/spec/support/controller_builder.rb +74 -0
  154. data/spec/support/fail_with_message_including_matcher.rb +33 -0
  155. data/spec/support/fail_with_message_matcher.rb +32 -0
  156. data/spec/support/i18n_faker.rb +10 -0
  157. data/spec/support/mailer_builder.rb +10 -0
  158. data/spec/support/model_builder.rb +81 -0
  159. data/spec/support/rails_versions.rb +18 -0
  160. data/spec/support/shared_examples/numerical_submatcher.rb +19 -0
  161. data/spec/support/shared_examples/numerical_type_submatcher.rb +17 -0
  162. data/spec/support/test_application.rb +120 -0
  163. data/yard.watchr +5 -0
  164. metadata +281 -0
@@ -0,0 +1,180 @@
1
+ require 'spec_helper'
2
+
3
+ describe Shoulda::Matchers::ActiveModel::ValidatePresenceOfMatcher do
4
+ context 'a model with a presence validation' do
5
+ it 'accepts' do
6
+ expect(validating_presence).to matcher
7
+ end
8
+
9
+ it 'does not override the default message with a blank' do
10
+ expect(validating_presence).to matcher.with_message(nil)
11
+ end
12
+ end
13
+
14
+ context 'a model without a presence validation' do
15
+ it 'rejects' do
16
+ expect(define_model(:example, attr: :string).new).not_to matcher
17
+ end
18
+ end
19
+
20
+ context 'an ActiveModel class with a presence validation' do
21
+ it 'accepts' do
22
+ expect(active_model_validating_presence).to matcher
23
+ end
24
+
25
+ it 'does not override the default message with a blank' do
26
+ expect(active_model_validating_presence).to matcher.with_message(nil)
27
+ end
28
+ end
29
+
30
+ context 'an ActiveModel class without a presence validation' do
31
+ it 'rejects' do
32
+ expect(active_model).not_to matcher
33
+ end
34
+
35
+ it 'provides the correct failure message' do
36
+ message = %{Expected errors to include "can't be blank" when attr is set to nil, got no errors}
37
+
38
+ expect { expect(active_model).to matcher }.to fail_with_message(message)
39
+ end
40
+ end
41
+
42
+ context 'a has_many association with a presence validation' do
43
+ it 'requires the attribute to be set' do
44
+ expect(has_many_children(presence: true)).to validate_presence_of(:children)
45
+ end
46
+ end
47
+
48
+ context 'a has_many association without a presence validation' do
49
+ it 'does not require the attribute to be set' do
50
+ expect(has_many_children(presence: false)).
51
+ not_to validate_presence_of(:children)
52
+ end
53
+ end
54
+
55
+ context 'a required has_and_belongs_to_many association' do
56
+ before do
57
+ define_model :child
58
+ @model = define_model :parent do
59
+ has_and_belongs_to_many :children
60
+ validates_presence_of :children
61
+ end.new
62
+ create_table 'children_parents', id: false do |t|
63
+ t.integer :child_id
64
+ t.integer :parent_id
65
+ end
66
+ end
67
+
68
+ it 'accepts' do
69
+ expect(@model).to validate_presence_of(:children)
70
+ end
71
+ end
72
+
73
+ context 'an optional has_and_belongs_to_many association' do
74
+ before do
75
+ define_model :child
76
+ @model = define_model :parent do
77
+ has_and_belongs_to_many :children
78
+ end.new
79
+ create_table 'children_parents', id: false do |t|
80
+ t.integer :child_id
81
+ t.integer :parent_id
82
+ end
83
+ end
84
+
85
+ it 'rejects' do
86
+ expect(@model).not_to validate_presence_of(:children)
87
+ end
88
+ end
89
+
90
+ context "an i18n translation containing %{attribute} and %{model}" do
91
+ before do
92
+ stub_translation(
93
+ "activerecord.errors.messages.blank",
94
+ "Please enter a %{attribute} for your %{model}")
95
+ end
96
+
97
+ after { I18n.backend.reload! }
98
+
99
+ it "does not raise an exception" do
100
+ expect {
101
+ expect(validating_presence).to validate_presence_of(:attr)
102
+ }.to_not raise_exception
103
+ end
104
+ end
105
+
106
+ if active_model_3_2?
107
+ context 'a strictly required attribute' do
108
+ it 'accepts when the :strict options match' do
109
+ expect(validating_presence(strict: true)).to matcher.strict
110
+ end
111
+
112
+ it 'rejects when the :strict options do not match' do
113
+ expect(validating_presence(strict: false)).not_to matcher.strict
114
+ end
115
+ end
116
+
117
+ it 'does not override the default message with a blank' do
118
+ expect(validating_presence(strict: true)).
119
+ to matcher.strict.with_message(nil)
120
+ end
121
+ end
122
+
123
+ context "an attribute with a context-dependent validation" do
124
+ context "without the validation context" do
125
+ it "does not match" do
126
+ expect(validating_presence(on: :customisable)).not_to matcher
127
+ end
128
+ end
129
+
130
+ context "with the validation context" do
131
+ it "matches" do
132
+ expect(validating_presence(on: :customisable)).to matcher.on(:customisable)
133
+ end
134
+ end
135
+ end
136
+
137
+ context 'an active_resource model' do
138
+ context 'with the validation context' do
139
+ it 'does not raise an exception' do
140
+ expect {
141
+ expect(active_resource_model).to validate_presence_of(:attr)
142
+ }.to_not raise_exception
143
+ end
144
+ end
145
+ end
146
+
147
+ def matcher
148
+ validate_presence_of(:attr)
149
+ end
150
+
151
+ def validating_presence(options = {})
152
+ define_model :example, attr: :string do
153
+ validates_presence_of :attr, options
154
+ end.new
155
+ end
156
+
157
+ def active_model(&block)
158
+ define_active_model_class('Example', accessors: [:attr], &block).new
159
+ end
160
+
161
+ def active_model_validating_presence
162
+ active_model { validates_presence_of :attr }
163
+ end
164
+
165
+ def has_many_children(options = {})
166
+ define_model :child
167
+ define_model :parent do
168
+ has_many :children
169
+ if options[:presence]
170
+ validates_presence_of :children
171
+ end
172
+ end.new
173
+ end
174
+
175
+ def active_resource_model
176
+ define_active_resource_class :foo, attr: :string do
177
+ validates_presence_of :attr
178
+ end.new
179
+ end
180
+ end
@@ -0,0 +1,398 @@
1
+ require 'spec_helper'
2
+
3
+ describe Shoulda::Matchers::ActiveModel::ValidateUniquenessOfMatcher do
4
+ context 'a model without a a uniqueness validation' do
5
+ it 'rejects' do
6
+ model = define_model(:example, attr: :string) { attr_accessible :attr } .new
7
+ Example.create!(attr: 'value')
8
+ expect(model).not_to matcher
9
+ end
10
+ end
11
+
12
+ context 'a model with a uniqueness validation' do
13
+ context 'where the subject has a character limit' do
14
+ it 'tests with values within the character limit' do
15
+ model = define_model(:example, attr: { type: :string, options: { limit: 1 } }) do
16
+ attr_accessible :attr
17
+ validates_uniqueness_of :attr
18
+ end.new
19
+ expect(model).to matcher
20
+ end
21
+ end
22
+
23
+ context 'with an existing record' do
24
+ it 'requires a unique value for that attribute' do
25
+ create_existing
26
+ expect(validating_uniqueness_with_other).to matcher
27
+ end
28
+
29
+ it 'accepts when the subject is an existing record' do
30
+ expect(create_existing).to matcher
31
+ end
32
+
33
+ it 'rejects when a scope is specified' do
34
+ create_existing
35
+ expect(validating_uniqueness_with_other).not_to matcher.scoped_to(:other)
36
+ end
37
+
38
+ def create_existing
39
+ define_model_with_other
40
+ Example.create!(attr: 'value', other: 1)
41
+ end
42
+ end
43
+
44
+ context 'without an existing record' do
45
+ it 'does not require a created instance' do
46
+ define_model_with_other
47
+ expect(Example.count).to eq 0
48
+ expect(validating_uniqueness_with_other).to matcher
49
+ end
50
+ end
51
+
52
+ def define_model_with_other(options = {})
53
+ @model ||= define_model(:example, attr: :string, other: :integer) do
54
+ attr_accessible :attr, :other
55
+ validates_uniqueness_of :attr, options
56
+ end
57
+ end
58
+
59
+ def validating_uniqueness_with_other(options = {})
60
+ define_model_with_other.new
61
+ end
62
+ end
63
+
64
+ context 'a model with a uniqueness validation, a custom error, and an existing record' do
65
+ it 'rejects when the actual message does not match the default message' do
66
+ expect(validating_uniqueness_with_existing_record(message: 'Bad value')).
67
+ not_to matcher
68
+ end
69
+
70
+ it 'rejects when the messages do not match' do
71
+ expect(validating_uniqueness_with_existing_record(message: 'Bad value')).
72
+ not_to matcher.with_message(/abc/)
73
+ end
74
+
75
+ it 'accepts when the messages match' do
76
+ expect(validating_uniqueness_with_existing_record(message: 'Bad value')).
77
+ to matcher.with_message(/Bad/)
78
+ end
79
+
80
+ def validating_uniqueness_with_existing_record(options = {})
81
+ model = define_model(:example, attr: :string) do
82
+ attr_accessible :attr
83
+ validates_uniqueness_of :attr, options
84
+ end.new
85
+ Example.create!(attr: 'value')
86
+ model
87
+ end
88
+ end
89
+
90
+ context 'a model with a scoped uniqueness validation with an existing value' do
91
+ it 'accepts when the correct scope is specified' do
92
+ expect(validating_scoped_uniqueness([:scope1, :scope2])).
93
+ to matcher.scoped_to(:scope1, :scope2)
94
+ end
95
+
96
+ it 'accepts when the subject is an existing record' do
97
+ define_scoped_model([:scope1, :scope2])
98
+ expect(create_existing_record).to matcher.scoped_to(:scope1, :scope2)
99
+ end
100
+
101
+ it 'rejects when too narrow of a scope is specified' do
102
+ expect(validating_scoped_uniqueness([:scope1, :scope2])).
103
+ not_to matcher.scoped_to(:scope1, :scope2, :other)
104
+ end
105
+
106
+ it 'rejects when too broad of a scope is specified' do
107
+ expect(validating_scoped_uniqueness([:scope1, :scope2])).
108
+ not_to matcher.scoped_to(:scope1)
109
+ end
110
+
111
+ it 'rejects when a different scope is specified' do
112
+ expect(validating_scoped_uniqueness([:scope1])).
113
+ not_to matcher.scoped_to(:other)
114
+ end
115
+
116
+ it 'rejects when no scope is specified' do
117
+ expect(validating_scoped_uniqueness([:scope1])).not_to matcher
118
+ end
119
+
120
+ it 'rejects when a non-existent attribute is specified as a scope' do
121
+ expect(validating_scoped_uniqueness([:scope1])).
122
+ not_to matcher.scoped_to(:fake)
123
+ end
124
+
125
+ context 'when the scoped attribute is a date' do
126
+ it "accepts" do
127
+ expect(validating_scoped_uniqueness([:scope1], :date, scope1: Date.today)).
128
+ to matcher.scoped_to(:scope1)
129
+ end
130
+
131
+ context 'with an existing record that conflicts with scope.next' do
132
+ it 'accepts' do
133
+ expect(validating_scoped_uniqueness_with_conflicting_next(:scope1, :date, scope1: Date.today)).
134
+ to matcher.scoped_to(:scope1)
135
+ end
136
+ end
137
+
138
+ context 'when too narrow of a scope is specified' do
139
+ it 'rejects' do
140
+ expect(validating_scoped_uniqueness([:scope1, :scope2], :date, scope1: Date.today, scope2: Date.today)).
141
+ not_to matcher.scoped_to(:scope1, :scope2, :other)
142
+ end
143
+ end
144
+
145
+ context 'when too broad of a scope is specified' do
146
+ it 'rejects' do
147
+ expect(validating_scoped_uniqueness([:scope1, :scope2], :date, scope1: Date.today, scope2: Date.today)).
148
+ not_to matcher.scoped_to(:scope1)
149
+ end
150
+ end
151
+ end
152
+
153
+ context 'when the scoped attribute is a datetime' do
154
+ it 'accepts' do
155
+ expect(validating_scoped_uniqueness([:scope1], :datetime, scope1: DateTime.now)).
156
+ to matcher.scoped_to(:scope1)
157
+ end
158
+
159
+ context 'with an existing record that conflicts with scope.next' do
160
+ it 'accepts' do
161
+ expect(validating_scoped_uniqueness_with_conflicting_next(:scope1, :datetime, scope1: DateTime.now)).
162
+ to matcher.scoped_to(:scope1)
163
+ end
164
+ end
165
+
166
+ context 'with a nil value' do
167
+ it 'accepts' do
168
+ expect(validating_scoped_uniqueness([:scope1], :datetime, scope1: nil)).
169
+ to matcher.scoped_to(:scope1)
170
+ end
171
+ end
172
+
173
+ context 'when too narrow of a scope is specified' do
174
+ it 'rejects' do
175
+ expect(validating_scoped_uniqueness([:scope1, :scope2], :datetime, scope1: DateTime.now, scope2: DateTime.now)).
176
+ not_to matcher.scoped_to(:scope1, :scope2, :other)
177
+ end
178
+ end
179
+
180
+ context 'when too broad of a scope is specified' do
181
+ it 'rejects' do
182
+ expect(validating_scoped_uniqueness([:scope1, :scope2], :datetime, scope1: DateTime.now, scope2: DateTime.now)).
183
+ not_to matcher.scoped_to(:scope1)
184
+ end
185
+ end
186
+ end
187
+
188
+ context 'when the scoped attribute is a uuid' do
189
+ it 'accepts' do
190
+ expect(validating_scoped_uniqueness([:scope1], :uuid, scope1: SecureRandom.uuid)).
191
+ to matcher.scoped_to(:scope1)
192
+ end
193
+
194
+ context 'with an existing record that conflicts with scope.next' do
195
+ it 'accepts' do
196
+ expect(validating_scoped_uniqueness_with_conflicting_next(:scope1, :uuid, scope1: SecureRandom.uuid)).
197
+ to matcher.scoped_to(:scope1)
198
+ end
199
+ end
200
+
201
+ context 'with a nil value' do
202
+ it 'accepts' do
203
+ expect(validating_scoped_uniqueness([:scope1], :uuid, scope1: nil)).
204
+ to matcher.scoped_to(:scope1)
205
+ end
206
+ end
207
+
208
+ context 'when too narrow of a scope is specified' do
209
+ it 'rejects' do
210
+ record = validating_scoped_uniqueness([:scope1, :scope2], :uuid,
211
+ scope1: SecureRandom.uuid,
212
+ scope2: SecureRandom.uuid
213
+ )
214
+ expect(record).not_to matcher.scoped_to(:scope1, :scope2, :other)
215
+ end
216
+ end
217
+
218
+ context 'when too broad of a scope is specified' do
219
+ it 'rejects' do
220
+ record = validating_scoped_uniqueness([:scope1, :scope2], :uuid,
221
+ scope1: SecureRandom.uuid,
222
+ scope2: SecureRandom.uuid
223
+ )
224
+ expect(record).not_to matcher.scoped_to(:scope1)
225
+ end
226
+ end
227
+ end
228
+
229
+ def create_existing_record(attributes = {})
230
+ @existing ||= create_record(attributes)
231
+ end
232
+
233
+ def create_record(attributes = {})
234
+ default_attributes = {attr: 'value', scope1: 1, scope2: 2, other: 3}
235
+ Example.create!(default_attributes.merge(attributes))
236
+ end
237
+
238
+ def define_scoped_model(scope, scope_attr_type = :integer)
239
+ define_model(:example, attr: :string, scope1: scope_attr_type,
240
+ scope2: scope_attr_type, other: :integer) do
241
+ attr_accessible :attr, :scope1, :scope2, :other
242
+ validates_uniqueness_of :attr, scope: scope
243
+ end
244
+ end
245
+
246
+ def validating_scoped_uniqueness(*args)
247
+ attributes = args.extract_options!
248
+ model = define_scoped_model(*args).new
249
+ create_existing_record(attributes)
250
+ model
251
+ end
252
+
253
+ def validating_scoped_uniqueness_with_conflicting_next(*args)
254
+ attributes = args.extract_options!
255
+ model = define_scoped_model(*args).new
256
+ 2.times do
257
+ attributes[:scope1] = attributes[:scope1].next
258
+ create_record(attributes)
259
+ end
260
+ model
261
+ end
262
+ end
263
+
264
+ context 'a model with a case-sensitive uniqueness validation on a string attribute and an existing record' do
265
+ it 'accepts a case-sensitive value for that attribute' do
266
+ expect(case_sensitive_validation_with_existing_value(:string)).
267
+ to matcher
268
+ end
269
+
270
+ it 'rejects a case-insensitive value for that attribute' do
271
+ expect(case_sensitive_validation_with_existing_value(:string)).
272
+ not_to matcher.case_insensitive
273
+ end
274
+ end
275
+
276
+ context 'a model with a case-sensitive uniqueness validation on an integer attribute with an existing value' do
277
+ it 'accepts a case-insensitive value for that attribute' do
278
+ expect(case_sensitive_validation_with_existing_value(:integer)).
279
+ to matcher.case_insensitive
280
+ end
281
+
282
+ it 'accepts a case-sensitive value for that attribute' do
283
+ expect(case_sensitive_validation_with_existing_value(:integer)).to matcher
284
+ end
285
+ end
286
+
287
+ context "when the validation allows nil" do
288
+ context "when there is an existing entry with a nil" do
289
+ it "should allow_nil" do
290
+ model = define_model_with_allow_nil
291
+ Example.create!(attr: nil)
292
+ expect(model).to matcher.allow_nil
293
+ end
294
+ end
295
+
296
+ if active_model_3_1?
297
+ context 'when the subject has a secure password' do
298
+ it 'allows nil on the attribute' do
299
+ model = define_model(:example, attr: :string, password_digest: :string) do |m|
300
+ validates_uniqueness_of :attr, allow_nil: true
301
+ has_secure_password
302
+ end.new
303
+ expect(model).to matcher.allow_nil
304
+ end
305
+ end
306
+ end
307
+
308
+ it "should create a nil and verify that it is allowed" do
309
+ model = define_model_with_allow_nil
310
+ expect(model).to matcher.allow_nil
311
+ Example.all.any?{ |instance| instance.attr.nil? }
312
+ end
313
+
314
+ def define_model_with_allow_nil
315
+ define_model(:example, attr: :integer) do
316
+ attr_accessible :attr
317
+ validates_uniqueness_of :attr, allow_nil: true
318
+ end.new
319
+ end
320
+ end
321
+
322
+ context "when the validation does not allow a nil" do
323
+ context "when there is an existing entry with a nil" do
324
+ it "should not allow_nil" do
325
+ model = define_model_without_allow_nil
326
+ Example.create!(attr: nil)
327
+ expect(model).not_to matcher.allow_nil
328
+ end
329
+ end
330
+
331
+ it "should not allow_nil" do
332
+ model = define_model_without_allow_nil
333
+ expect(model).not_to matcher.allow_nil
334
+ end
335
+
336
+ def define_model_without_allow_nil
337
+ define_model(:example, attr: :integer) do
338
+ attr_accessible :attr
339
+ validates_uniqueness_of :attr
340
+ end.new
341
+ end
342
+ end
343
+
344
+ context "a model with non-nullable attribute" do
345
+ context "of type" do
346
+ [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |type|
347
+ context type do
348
+ it "does not raise an error" do
349
+ model = define_model_with_non_nullable(type)
350
+ expect { expect(model).to matcher }.not_to raise_error
351
+ end
352
+ end
353
+ end
354
+ end
355
+
356
+ context "that is a primary key" do
357
+ it "does not cause duplicate entry errors by re-using default values for primary keys" do
358
+ create_table :examples, id: false do |t|
359
+ t.string :attr
360
+ t.integer :non_nullable, primary: true
361
+ end
362
+ model_class = define_model(:example, attr: :string) do
363
+ validates_uniqueness_of :attr
364
+ end
365
+ model_1 = model_class.new
366
+ model_2 = model_class.new
367
+ expect(model_1).to matcher
368
+ expect { expect(model_2).to matcher }.not_to raise_error
369
+ end
370
+ end
371
+
372
+ def define_model_with_non_nullable(type)
373
+ define_model(:example, attr: :string, non_nullable: { type: type, options: { null: false } }) do
374
+ attr_accessible :attr, :non_nullable
375
+ validates_uniqueness_of :attr
376
+ end.new
377
+ end
378
+ end
379
+
380
+ def case_sensitive_validation_with_existing_value(attr_type)
381
+ model = define_model(:example, attr: attr_type) do
382
+ attr_accessible :attr
383
+ validates_uniqueness_of :attr, case_sensitive: true
384
+ end.new
385
+ if attr_type == :string
386
+ Example.create!(attr: 'value')
387
+ elsif attr_type == :integer
388
+ Example.create!(attr: 1)
389
+ else
390
+ raise 'Must be :string or :integer'
391
+ end
392
+ model
393
+ end
394
+
395
+ def matcher
396
+ validate_uniqueness_of(:attr)
397
+ end
398
+ end