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,89 @@
|
|
1
|
+
module Shoulda
|
2
|
+
module Matchers
|
3
|
+
module ActiveModel
|
4
|
+
# The `validate_presence_of` matcher tests usage of the
|
5
|
+
# `validates_presence_of` validation.
|
6
|
+
#
|
7
|
+
# class Robot < ActiveRecord::Base
|
8
|
+
# validates_presence_of :arms
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# # RSpec
|
12
|
+
# describe Robot do
|
13
|
+
# it { should validate_presence_of(:arms) }
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# # Test::Unit
|
17
|
+
# class RobotTest < ActiveSupport::TestCase
|
18
|
+
# should validate_presence_of(:arms)
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# #### Qualifiers
|
22
|
+
#
|
23
|
+
# ##### with_message
|
24
|
+
#
|
25
|
+
# Use `with_message` if you are using a custom validation message.
|
26
|
+
#
|
27
|
+
# class Robot < ActiveRecord::Base
|
28
|
+
# validates_presence_of :legs, message: 'Robot has no legs'
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# # RSpec
|
32
|
+
# describe Robot do
|
33
|
+
# it { should validate_presence_of(:legs).with_message('Robot has no legs') }
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# # Test::Unit
|
37
|
+
# class RobotTest < ActiveSupport::TestCase
|
38
|
+
# should validate_presence_of(:legs).with_message('Robot has no legs')
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# @return [ValidatePresenceOfMatcher]
|
42
|
+
#
|
43
|
+
def validate_presence_of(attr)
|
44
|
+
ValidatePresenceOfMatcher.new(attr)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @private
|
48
|
+
class ValidatePresenceOfMatcher < ValidationMatcher
|
49
|
+
def with_message(message)
|
50
|
+
@expected_message = message if message
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def matches?(subject)
|
55
|
+
super(subject)
|
56
|
+
@expected_message ||= :blank
|
57
|
+
disallows_value_of(blank_value, @expected_message)
|
58
|
+
end
|
59
|
+
|
60
|
+
def description
|
61
|
+
"require #{@attribute} to be set"
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def blank_value
|
67
|
+
if collection?
|
68
|
+
[]
|
69
|
+
else
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def collection?
|
75
|
+
if reflection
|
76
|
+
[:has_many, :has_and_belongs_to_many].include?(reflection.macro)
|
77
|
+
else
|
78
|
+
false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def reflection
|
83
|
+
@subject.class.respond_to?(:reflect_on_association) &&
|
84
|
+
@subject.class.reflect_on_association(@attribute)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,372 @@
|
|
1
|
+
module Shoulda
|
2
|
+
module Matchers
|
3
|
+
module ActiveModel
|
4
|
+
# The `validate_uniqueness_of` matcher tests usage of the
|
5
|
+
# `validates_uniqueness_of` validation. It first checks for an existing
|
6
|
+
# instance of your model in the database, creating one if necessary. It
|
7
|
+
# then takes a new record and asserts that it fails validation if the
|
8
|
+
# attribute or attributes you've specified in the validation are set to
|
9
|
+
# values which are the same as those of the pre-existing record (thereby
|
10
|
+
# failing the uniqueness check).
|
11
|
+
#
|
12
|
+
# class Post < ActiveRecord::Base
|
13
|
+
# validates_uniqueness_of :permalink
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# # RSpec
|
17
|
+
# describe Post do
|
18
|
+
# it { should validate_uniqueness_of(:permalink) }
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# # Test::Unit
|
22
|
+
# class PostTest < ActiveSupport::TestCase
|
23
|
+
# should validate_uniqueness_of(:permalink)
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# #### Caveat
|
27
|
+
#
|
28
|
+
# This matcher works a bit differently than other matchers. As noted
|
29
|
+
# before, it will create an instance of your model if one doesn't already
|
30
|
+
# exist. Sometimes this step fails, especially if you have database-level
|
31
|
+
# restrictions on any attributes other than the one which is unique. In
|
32
|
+
# this case, the solution is to **create a record manually** before you
|
33
|
+
# call `validate_uniqueness_of`.
|
34
|
+
#
|
35
|
+
# For example, say you have the following migration and model:
|
36
|
+
#
|
37
|
+
# class CreatePosts < ActiveRecord::Migration
|
38
|
+
# def change
|
39
|
+
# create_table :posts do |t|
|
40
|
+
# t.string :title
|
41
|
+
# t.text :content, null: false
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# class Post < ActiveRecord::Base
|
47
|
+
# validates :title, uniqueness: true
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# You may be tempted to test the model like this:
|
51
|
+
#
|
52
|
+
# describe Post do
|
53
|
+
# it { should validate_uniqueness_of(:title) }
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# However, running this test will fail with something like:
|
57
|
+
#
|
58
|
+
# Failures:
|
59
|
+
#
|
60
|
+
# 1) Post should require case sensitive unique value for title
|
61
|
+
# Failure/Error: it { should validate_uniqueness_of(:title) }
|
62
|
+
# ActiveRecord::StatementInvalid:
|
63
|
+
# SQLite3::ConstraintException: posts.content may not be NULL: INSERT INTO "posts" ("title") VALUES (?)
|
64
|
+
#
|
65
|
+
# To fix this, you'll need to write this instead:
|
66
|
+
#
|
67
|
+
# describe Post do
|
68
|
+
# it do
|
69
|
+
# Post.create!(content: 'Here is the content')
|
70
|
+
# should validate_uniqueness_of(:title)
|
71
|
+
# end
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# Or, if you're using
|
75
|
+
# [FactoryGirl](http://github.com/thoughtbot/factory_girl) and you have a
|
76
|
+
# `post` factory defined which automatically sets `content`, you can say:
|
77
|
+
#
|
78
|
+
# describe Post do
|
79
|
+
# it do
|
80
|
+
# FactoryGirl.create(:post)
|
81
|
+
# should validate_uniqueness_of(:title)
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# #### Qualifiers
|
86
|
+
#
|
87
|
+
# ##### with_message
|
88
|
+
#
|
89
|
+
# Use `with_message` if you are using a custom validation message.
|
90
|
+
#
|
91
|
+
# class Post < ActiveRecord::Base
|
92
|
+
# validates_uniqueness_of :title, message: 'Please choose another title'
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# # RSpec
|
96
|
+
# describe Post do
|
97
|
+
# it do
|
98
|
+
# should validate_uniqueness_of(:title).
|
99
|
+
# with_message('Please choose another title')
|
100
|
+
# end
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# # Test::Unit
|
104
|
+
# class PostTest < ActiveSupport::TestCase
|
105
|
+
# should validate_uniqueness_of(:title).
|
106
|
+
# with_message('Please choose another title')
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# ##### scoped_to
|
110
|
+
#
|
111
|
+
# Use `scoped_to` to test usage of the `:scope` option. This asserts that
|
112
|
+
# a new record fails validation if not only the primary attribute is not
|
113
|
+
# unique, but the scoped attributes are not unique either.
|
114
|
+
#
|
115
|
+
# class Post < ActiveRecord::Base
|
116
|
+
# validates_uniqueness_of :slug, scope: :user_id
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# # RSpec
|
120
|
+
# describe Post do
|
121
|
+
# it { should validate_uniqueness_of(:slug).scoped_to(:journal_id) }
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
# # Test::Unit
|
125
|
+
# class PostTest < ActiveSupport::TestCase
|
126
|
+
# should validate_uniqueness_of(:slug).scoped_to(:journal_id)
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# ##### case_insensitive
|
130
|
+
#
|
131
|
+
# Use `case_insensitive` to test usage of the `:case_sensitive` option
|
132
|
+
# with a false value. This asserts that the uniquable attributes fail
|
133
|
+
# validation even if their values are a different case than corresponding
|
134
|
+
# attributes in the pre-existing record.
|
135
|
+
#
|
136
|
+
# class Post < ActiveRecord::Base
|
137
|
+
# validates_uniqueness_of :key, case_sensitive: false
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# # RSpec
|
141
|
+
# describe Post do
|
142
|
+
# it { should validate_uniqueness_of(:key).case_insensitive }
|
143
|
+
# end
|
144
|
+
#
|
145
|
+
# # Test::Unit
|
146
|
+
# class PostTest < ActiveSupport::TestCase
|
147
|
+
# should validate_uniqueness_of(:key).case_insensitive
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
# ##### allow_nil
|
151
|
+
#
|
152
|
+
# Use `allow_nil` to assert that the attribute allows nil.
|
153
|
+
#
|
154
|
+
# class Post < ActiveRecord::Base
|
155
|
+
# validates_uniqueness_of :author_id, allow_nil: true
|
156
|
+
# end
|
157
|
+
#
|
158
|
+
# # RSpec
|
159
|
+
# describe Post do
|
160
|
+
# it { should validate_uniqueness_of(:author_id).allow_nil }
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
# # Test::Unit
|
164
|
+
# class PostTest < ActiveSupport::TestCase
|
165
|
+
# should validate_uniqueness_of(:author_id).allow_nil
|
166
|
+
# end
|
167
|
+
#
|
168
|
+
# @return [ValidateUniquenessOfMatcher]
|
169
|
+
#
|
170
|
+
def validate_uniqueness_of(attr)
|
171
|
+
ValidateUniquenessOfMatcher.new(attr)
|
172
|
+
end
|
173
|
+
|
174
|
+
# @private
|
175
|
+
class ValidateUniquenessOfMatcher < ValidationMatcher
|
176
|
+
include Helpers
|
177
|
+
|
178
|
+
def initialize(attribute)
|
179
|
+
super(attribute)
|
180
|
+
@options = {}
|
181
|
+
end
|
182
|
+
|
183
|
+
def scoped_to(*scopes)
|
184
|
+
@options[:scopes] = [*scopes].flatten
|
185
|
+
self
|
186
|
+
end
|
187
|
+
|
188
|
+
def with_message(message)
|
189
|
+
@expected_message = message
|
190
|
+
self
|
191
|
+
end
|
192
|
+
|
193
|
+
def case_insensitive
|
194
|
+
@options[:case_insensitive] = true
|
195
|
+
self
|
196
|
+
end
|
197
|
+
|
198
|
+
def allow_nil
|
199
|
+
@options[:allow_nil] = true
|
200
|
+
self
|
201
|
+
end
|
202
|
+
|
203
|
+
def description
|
204
|
+
result = "require "
|
205
|
+
result << "case sensitive " unless @options[:case_insensitive]
|
206
|
+
result << "unique value for #{@attribute}"
|
207
|
+
result << " scoped to #{@options[:scopes].join(', ')}" if @options[:scopes].present?
|
208
|
+
result
|
209
|
+
end
|
210
|
+
|
211
|
+
def matches?(subject)
|
212
|
+
@subject = subject.class.new
|
213
|
+
@expected_message ||= :taken
|
214
|
+
set_scoped_attributes &&
|
215
|
+
validate_everything_except_duplicate_nils? &&
|
216
|
+
validate_after_scope_change? &&
|
217
|
+
allows_nil?
|
218
|
+
end
|
219
|
+
|
220
|
+
private
|
221
|
+
|
222
|
+
def allows_nil?
|
223
|
+
if @options[:allow_nil]
|
224
|
+
ensure_nil_record_in_database
|
225
|
+
allows_value_of(nil, @expected_message)
|
226
|
+
else
|
227
|
+
true
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def existing_record
|
232
|
+
@existing_record ||= first_instance
|
233
|
+
end
|
234
|
+
|
235
|
+
def first_instance
|
236
|
+
@subject.class.first || create_record_in_database
|
237
|
+
end
|
238
|
+
|
239
|
+
def ensure_nil_record_in_database
|
240
|
+
unless existing_record_is_nil?
|
241
|
+
create_record_in_database(nil_value: true)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def existing_record_is_nil?
|
246
|
+
@existing_record.present? && existing_value.nil?
|
247
|
+
end
|
248
|
+
|
249
|
+
def create_record_in_database(options = {})
|
250
|
+
if options[:nil_value]
|
251
|
+
value = nil
|
252
|
+
else
|
253
|
+
value = 'a'
|
254
|
+
end
|
255
|
+
|
256
|
+
@subject.class.new.tap do |instance|
|
257
|
+
instance.__send__("#{@attribute}=", value)
|
258
|
+
|
259
|
+
other_non_nullable_columns.each do |non_nullable_column|
|
260
|
+
instance.__send__("#{non_nullable_column.name}=", correct_type_for_column(non_nullable_column))
|
261
|
+
end
|
262
|
+
|
263
|
+
if has_secure_password?
|
264
|
+
instance.password = 'password'
|
265
|
+
instance.password_confirmation = 'password'
|
266
|
+
end
|
267
|
+
instance.save(validate: false)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def has_secure_password?
|
272
|
+
@subject.class.ancestors.map(&:to_s).include?('ActiveModel::SecurePassword::InstanceMethodsOnActivation')
|
273
|
+
end
|
274
|
+
|
275
|
+
def set_scoped_attributes
|
276
|
+
if @options[:scopes].present?
|
277
|
+
@options[:scopes].all? do |scope|
|
278
|
+
setter = :"#{scope}="
|
279
|
+
if @subject.respond_to?(setter)
|
280
|
+
@subject.__send__(setter, existing_record.__send__(scope))
|
281
|
+
true
|
282
|
+
else
|
283
|
+
@failure_message = "#{class_name} doesn't seem to have a #{scope} attribute."
|
284
|
+
false
|
285
|
+
end
|
286
|
+
end
|
287
|
+
else
|
288
|
+
true
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def validate_everything_except_duplicate_nils?
|
293
|
+
if @options[:allow_nil] && existing_value.nil?
|
294
|
+
create_record_without_nil
|
295
|
+
end
|
296
|
+
|
297
|
+
disallows_value_of(existing_value, @expected_message)
|
298
|
+
end
|
299
|
+
|
300
|
+
def create_record_without_nil
|
301
|
+
@existing_record = create_record_in_database
|
302
|
+
end
|
303
|
+
|
304
|
+
def validate_after_scope_change?
|
305
|
+
if @options[:scopes].blank?
|
306
|
+
true
|
307
|
+
else
|
308
|
+
all_records = @subject.class.all
|
309
|
+
@options[:scopes].all? do |scope|
|
310
|
+
previous_value = all_records.map(&scope).max
|
311
|
+
|
312
|
+
# Assume the scope is a foreign key if the field is nil
|
313
|
+
previous_value ||= correct_type_for_column(@subject.class.columns_hash[scope.to_s])
|
314
|
+
|
315
|
+
next_value =
|
316
|
+
if previous_value.respond_to?(:next)
|
317
|
+
previous_value.next
|
318
|
+
elsif previous_value.respond_to?(:to_datetime)
|
319
|
+
previous_value.to_datetime.next
|
320
|
+
else
|
321
|
+
previous_value.to_s.next
|
322
|
+
end
|
323
|
+
|
324
|
+
@subject.__send__("#{scope}=", next_value)
|
325
|
+
|
326
|
+
if allows_value_of(existing_value, @expected_message)
|
327
|
+
@subject.__send__("#{scope}=", previous_value)
|
328
|
+
|
329
|
+
@failure_message_when_negated <<
|
330
|
+
" (with different value of #{scope})"
|
331
|
+
true
|
332
|
+
else
|
333
|
+
@failure_message << " (with different value of #{scope})"
|
334
|
+
false
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def correct_type_for_column(column)
|
341
|
+
if column.type == :string || column.type == :binary
|
342
|
+
'0'
|
343
|
+
elsif column.type == :datetime
|
344
|
+
DateTime.now
|
345
|
+
elsif column.type == :uuid
|
346
|
+
SecureRandom.uuid
|
347
|
+
else
|
348
|
+
0
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def class_name
|
353
|
+
@subject.class.name
|
354
|
+
end
|
355
|
+
|
356
|
+
def existing_value
|
357
|
+
value = existing_record.__send__(@attribute)
|
358
|
+
if @options[:case_insensitive] && value.respond_to?(:swapcase!)
|
359
|
+
value.swapcase!
|
360
|
+
end
|
361
|
+
value
|
362
|
+
end
|
363
|
+
|
364
|
+
def other_non_nullable_columns
|
365
|
+
@subject.class.columns.select do |column|
|
366
|
+
column.name != @attribute && !column.null && !column.primary
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|