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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +32 -0
- data/.yardopts +7 -0
- data/Appraisals +45 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +31 -0
- data/Gemfile.lock +166 -0
- data/MIT-LICENSE +22 -0
- data/NEWS.md +299 -0
- data/README.md +163 -0
- data/Rakefile +116 -0
- data/doc_config/gh-pages/index.html.erb +9 -0
- data/doc_config/yard/setup.rb +22 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/bootstrap.css +5967 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/full_list.css +12 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/global.css +45 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/solarized.css +69 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/style.css +283 -0
- data/doc_config/yard/templates/default/fulldoc/html/full_list.erb +32 -0
- data/doc_config/yard/templates/default/fulldoc/html/full_list_class.erb +1 -0
- data/doc_config/yard/templates/default/fulldoc/html/full_list_method.erb +8 -0
- data/doc_config/yard/templates/default/fulldoc/html/js/app.js +300 -0
- data/doc_config/yard/templates/default/fulldoc/html/js/full_list.js +1 -0
- data/doc_config/yard/templates/default/fulldoc/html/js/jquery.stickyheaders.js +289 -0
- data/doc_config/yard/templates/default/fulldoc/html/js/underscore.min.js +6 -0
- data/doc_config/yard/templates/default/fulldoc/html/setup.rb +8 -0
- data/doc_config/yard/templates/default/layout/html/breadcrumb.erb +14 -0
- data/doc_config/yard/templates/default/layout/html/fonts.erb +1 -0
- data/doc_config/yard/templates/default/layout/html/layout.erb +23 -0
- data/doc_config/yard/templates/default/layout/html/search.erb +13 -0
- data/doc_config/yard/templates/default/layout/html/setup.rb +8 -0
- data/doc_config/yard/templates/default/method_details/html/source.erb +10 -0
- data/doc_config/yard/templates/default/module/html/box_info.erb +31 -0
- data/features/rails_integration.feature +113 -0
- data/features/step_definitions/rails_steps.rb +162 -0
- data/features/support/env.rb +5 -0
- data/gemfiles/3.0.gemfile +24 -0
- data/gemfiles/3.0.gemfile.lock +150 -0
- data/gemfiles/3.1.gemfile +27 -0
- data/gemfiles/3.1.gemfile.lock +173 -0
- data/gemfiles/3.2.gemfile +27 -0
- data/gemfiles/3.2.gemfile.lock +171 -0
- data/gemfiles/4.0.0.gemfile +28 -0
- data/gemfiles/4.0.0.gemfile.lock +172 -0
- data/gemfiles/4.0.1.gemfile +28 -0
- data/gemfiles/4.0.1.gemfile.lock +172 -0
- data/lib/shoulda-matchers.rb +1 -0
- data/lib/shoulda/matchers.rb +11 -0
- data/lib/shoulda/matchers/action_controller.rb +17 -0
- data/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +64 -0
- data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +97 -0
- data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +81 -0
- data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +117 -0
- data/lib/shoulda/matchers/action_controller/rescue_from_matcher.rb +114 -0
- data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +154 -0
- data/lib/shoulda/matchers/action_controller/route_matcher.rb +116 -0
- data/lib/shoulda/matchers/action_controller/route_params.rb +48 -0
- data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +164 -0
- data/lib/shoulda/matchers/action_controller/set_the_flash_matcher.rb +296 -0
- data/lib/shoulda/matchers/active_model.rb +30 -0
- data/lib/shoulda/matchers/active_model/allow_mass_assignment_of_matcher.rb +167 -0
- data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +314 -0
- data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +46 -0
- data/lib/shoulda/matchers/active_model/ensure_exclusion_of_matcher.rb +160 -0
- data/lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb +417 -0
- data/lib/shoulda/matchers/active_model/ensure_length_of_matcher.rb +337 -0
- data/lib/shoulda/matchers/active_model/errors.rb +10 -0
- data/lib/shoulda/matchers/active_model/exception_message_finder.rb +58 -0
- data/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +92 -0
- data/lib/shoulda/matchers/active_model/helpers.rb +46 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers.rb +9 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +75 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +27 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +27 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +26 -0
- data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +112 -0
- data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +77 -0
- data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +121 -0
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +380 -0
- data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +89 -0
- data/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb +372 -0
- data/lib/shoulda/matchers/active_model/validation_matcher.rb +97 -0
- data/lib/shoulda/matchers/active_model/validation_message_finder.rb +69 -0
- data/lib/shoulda/matchers/active_record.rb +22 -0
- data/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb +204 -0
- data/lib/shoulda/matchers/active_record/association_matcher.rb +901 -0
- data/lib/shoulda/matchers/active_record/association_matchers.rb +9 -0
- data/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +81 -0
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +65 -0
- data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +94 -0
- data/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_record/association_matchers/through_matcher.rb +63 -0
- data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +261 -0
- data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +149 -0
- data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +72 -0
- data/lib/shoulda/matchers/active_record/serialize_matcher.rb +181 -0
- data/lib/shoulda/matchers/assertion_error.rb +19 -0
- data/lib/shoulda/matchers/error.rb +6 -0
- data/lib/shoulda/matchers/integrations/rspec.rb +20 -0
- data/lib/shoulda/matchers/integrations/test_unit.rb +30 -0
- data/lib/shoulda/matchers/rails_shim.rb +50 -0
- data/lib/shoulda/matchers/version.rb +6 -0
- data/lib/shoulda/matchers/warn.rb +8 -0
- data/shoulda-matchers.gemspec +23 -0
- data/spec/shoulda/matchers/action_controller/filter_param_matcher_spec.rb +22 -0
- data/spec/shoulda/matchers/action_controller/redirect_to_matcher_spec.rb +42 -0
- data/spec/shoulda/matchers/action_controller/render_template_matcher_spec.rb +78 -0
- data/spec/shoulda/matchers/action_controller/render_with_layout_matcher_spec.rb +63 -0
- data/spec/shoulda/matchers/action_controller/rescue_from_matcher_spec.rb +63 -0
- data/spec/shoulda/matchers/action_controller/respond_with_matcher_spec.rb +31 -0
- data/spec/shoulda/matchers/action_controller/route_matcher_spec.rb +70 -0
- data/spec/shoulda/matchers/action_controller/route_params_spec.rb +30 -0
- data/spec/shoulda/matchers/action_controller/set_session_matcher_spec.rb +51 -0
- data/spec/shoulda/matchers/action_controller/set_the_flash_matcher_spec.rb +153 -0
- data/spec/shoulda/matchers/active_model/allow_mass_assignment_of_matcher_spec.rb +111 -0
- data/spec/shoulda/matchers/active_model/allow_value_matcher_spec.rb +170 -0
- data/spec/shoulda/matchers/active_model/disallow_value_matcher_spec.rb +81 -0
- data/spec/shoulda/matchers/active_model/ensure_exclusion_of_matcher_spec.rb +95 -0
- data/spec/shoulda/matchers/active_model/ensure_inclusion_of_matcher_spec.rb +320 -0
- data/spec/shoulda/matchers/active_model/ensure_length_of_matcher_spec.rb +166 -0
- data/spec/shoulda/matchers/active_model/exception_message_finder_spec.rb +111 -0
- data/spec/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb +20 -0
- data/spec/shoulda/matchers/active_model/helpers_spec.rb +158 -0
- data/spec/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +169 -0
- data/spec/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +59 -0
- data/spec/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +59 -0
- data/spec/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +57 -0
- data/spec/shoulda/matchers/active_model/validate_absence_of_matcher_spec.rb +139 -0
- data/spec/shoulda/matchers/active_model/validate_acceptance_of_matcher_spec.rb +41 -0
- data/spec/shoulda/matchers/active_model/validate_confirmation_of_matcher_spec.rb +47 -0
- data/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +331 -0
- data/spec/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +180 -0
- data/spec/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb +398 -0
- data/spec/shoulda/matchers/active_model/validation_message_finder_spec.rb +127 -0
- data/spec/shoulda/matchers/active_record/accept_nested_attributes_for_matcher_spec.rb +107 -0
- data/spec/shoulda/matchers/active_record/association_matcher_spec.rb +860 -0
- data/spec/shoulda/matchers/active_record/association_matchers/model_reflection_spec.rb +247 -0
- data/spec/shoulda/matchers/active_record/have_db_column_matcher_spec.rb +111 -0
- data/spec/shoulda/matchers/active_record/have_db_index_matcher_spec.rb +78 -0
- data/spec/shoulda/matchers/active_record/have_readonly_attributes_matcher_spec.rb +41 -0
- data/spec/shoulda/matchers/active_record/serialize_matcher_spec.rb +86 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/active_model_versions.rb +13 -0
- data/spec/support/active_resource_builder.rb +29 -0
- data/spec/support/activemodel_helpers.rb +19 -0
- data/spec/support/capture_helpers.rb +19 -0
- data/spec/support/class_builder.rb +42 -0
- data/spec/support/controller_builder.rb +74 -0
- data/spec/support/fail_with_message_including_matcher.rb +33 -0
- data/spec/support/fail_with_message_matcher.rb +32 -0
- data/spec/support/i18n_faker.rb +10 -0
- data/spec/support/mailer_builder.rb +10 -0
- data/spec/support/model_builder.rb +81 -0
- data/spec/support/rails_versions.rb +18 -0
- data/spec/support/shared_examples/numerical_submatcher.rb +19 -0
- data/spec/support/shared_examples/numerical_type_submatcher.rb +17 -0
- data/spec/support/test_application.rb +120 -0
- data/yard.watchr +5 -0
- 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
|