ultra-fast-box 0.0.1
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/shoulda-matchers-8.0.1/LICENSE +22 -0
- data/shoulda-matchers-8.0.1/README.md +567 -0
- data/shoulda-matchers-8.0.1/docs/errors/NonCaseSwappableValueError.md +111 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/callback_matcher.rb +121 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +70 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/flash_store.rb +86 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/permit_matcher.rb +445 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +96 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/render_template_matcher.rb +84 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +145 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/rescue_from_matcher.rb +115 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +148 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/route_matcher.rb +207 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/route_params.rb +62 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/session_store.rb +34 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/set_flash_matcher.rb +228 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/set_session_matcher.rb +163 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/set_session_or_flash_matcher.rb +107 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller.rb +24 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_changed_value_error.rb +45 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_does_not_exist_error.rb +23 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter.rb +232 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter_and_validator.rb +62 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters.rb +40 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb +48 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/successful_check.rb +14 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/successful_setting.rb +14 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher.rb +695 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/comparison_matcher.rb +157 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +78 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/errors.rb +52 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +125 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/helpers.rb +37 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +45 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +67 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +45 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +44 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/numericality_matchers/range_matcher.rb +71 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb +53 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/numericality_matchers.rb +9 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/qualifiers/allow_blank.rb +26 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/qualifiers/allow_nil.rb +26 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/qualifiers/ignore_interference_by_writer.rb +101 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/qualifiers/ignoring_interference_by_writer.rb +21 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/qualifiers.rb +14 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +180 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +128 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +567 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +185 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +272 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +664 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +575 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +711 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +428 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validation_matcher/build_description.rb +62 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validation_matcher.rb +195 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validation_message_finder.rb +63 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validator.rb +127 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model.rb +96 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb +217 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matcher.rb +2134 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb +74 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb +62 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/inverse_of_matcher.rb +41 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb +105 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +101 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +89 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +142 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/optional_matcher.rb +71 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb +41 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/required_matcher.rb +76 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb +42 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/through_matcher.rb +65 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers.rb +9 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +784 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/encrypt_matcher.rb +174 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/have_attached_matcher.rb +343 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +366 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +287 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +104 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +95 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/have_rich_text_matcher.rb +83 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/have_secure_token_matcher.rb +132 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/normalize_matcher.rb +151 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/serialize_matcher.rb +203 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/uniqueness/model.rb +58 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/uniqueness/namespace.rb +39 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/uniqueness/test_model_creator.rb +49 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/uniqueness/test_models.rb +55 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/uniqueness.rb +14 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +1197 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record.rb +37 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/configuration.rb +31 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/double.rb +103 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/double_collection.rb +60 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/double_implementation_registry.rb +31 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/method_call.rb +44 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/object_double.rb +43 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/proxy_implementation.rb +32 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/stub_implementation.rb +31 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/world.rb +63 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak.rb +38 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/error.rb +29 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/independent/delegate_method_matcher/target_not_defined_error.rb +15 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/independent/delegate_method_matcher.rb +540 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/independent.rb +11 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/configuration.rb +70 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/configuration_error.rb +9 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/inclusion.rb +20 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/libraries/action_controller.rb +33 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/libraries/active_model.rb +26 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/libraries/active_record.rb +26 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/libraries/missing_library.rb +19 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/libraries/rails.rb +31 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/libraries/routing.rb +29 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/libraries.rb +16 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/rails.rb +12 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/registry.rb +28 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/test_frameworks/active_support_test_case.rb +37 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/test_frameworks/minitest_4.rb +36 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/test_frameworks/minitest_5.rb +37 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/test_frameworks/missing_test_framework.rb +40 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/test_frameworks/rspec.rb +29 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/test_frameworks/test_unit.rb +36 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/test_frameworks.rb +16 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations.rb +43 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/matcher_collection.rb +99 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/matcher_context.rb +36 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/rails_shim.rb +190 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/routing.rb +10 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/util/word_wrap.rb +211 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/util.rb +110 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/version.rb +6 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers/warn.rb +28 -0
- data/shoulda-matchers-8.0.1/lib/shoulda/matchers.rb +25 -0
- data/shoulda-matchers-8.0.1/lib/shoulda-matchers.rb +1 -0
- data/shoulda-matchers-8.0.1/shoulda-matchers.gemspec +43 -0
- data/ultra-fast-box.gemspec +12 -0
- metadata +179 -0
data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb
ADDED
|
@@ -0,0 +1,1197 @@
|
|
|
1
|
+
module Shoulda
|
|
2
|
+
module Matchers
|
|
3
|
+
module ActiveRecord
|
|
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 instance of that model and asserts that it fails
|
|
8
|
+
# validation if the attribute or attributes you've specified in the
|
|
9
|
+
# validation are set to values which are the same as those of the
|
|
10
|
+
# pre-existing record (thereby failing the uniqueness check).
|
|
11
|
+
#
|
|
12
|
+
# class Post < ActiveRecord::Base
|
|
13
|
+
# validates :permalink, uniqueness: true
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# # RSpec
|
|
17
|
+
# RSpec.describe Post, type: :model do
|
|
18
|
+
# it { should validate_uniqueness_of(:permalink) }
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# # Minitest (Shoulda)
|
|
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 populate these attributes with values
|
|
33
|
+
# before you 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
|
+
# RSpec.describe Post, type: :model do
|
|
53
|
+
# it { should validate_uniqueness_of(:title) }
|
|
54
|
+
# end
|
|
55
|
+
#
|
|
56
|
+
# However, running this test will fail with an exception such as:
|
|
57
|
+
#
|
|
58
|
+
# Shoulda::Matchers::ActiveRecord::ValidateUniquenessOfMatcher::ExistingRecordInvalid:
|
|
59
|
+
# validate_uniqueness_of works by matching a new record against an
|
|
60
|
+
# existing record. If there is no existing record, it will create one
|
|
61
|
+
# using the record you provide.
|
|
62
|
+
#
|
|
63
|
+
# While doing this, the following error was raised:
|
|
64
|
+
#
|
|
65
|
+
# PG::NotNullViolation: ERROR: null value in column "content" violates not-null constraint
|
|
66
|
+
# DETAIL: Failing row contains (1, null, null).
|
|
67
|
+
# : INSERT INTO "posts" DEFAULT VALUES RETURNING "id"
|
|
68
|
+
#
|
|
69
|
+
# The best way to fix this is to provide the matcher with a record where
|
|
70
|
+
# any required attributes are filled in with valid values beforehand.
|
|
71
|
+
#
|
|
72
|
+
# (The exact error message will differ depending on which database you're
|
|
73
|
+
# using, but you get the idea.)
|
|
74
|
+
#
|
|
75
|
+
# This happens because `validate_uniqueness_of` tries to create a new post
|
|
76
|
+
# but cannot do so because of the `content` attribute: though unrelated to
|
|
77
|
+
# this test, it nevertheless needs to be filled in. As indicated at the
|
|
78
|
+
# end of the error message, the solution is to build a custom Post object
|
|
79
|
+
# ahead of time with `content` filled in:
|
|
80
|
+
#
|
|
81
|
+
# RSpec.describe Post, type: :model do
|
|
82
|
+
# describe "validations" do
|
|
83
|
+
# subject { Post.new(content: "Here is the content") }
|
|
84
|
+
# it { should validate_uniqueness_of(:title) }
|
|
85
|
+
# end
|
|
86
|
+
# end
|
|
87
|
+
#
|
|
88
|
+
# Or, if you're using
|
|
89
|
+
# [FactoryBot](https://github.com/thoughtbot/factory_bot) and you have a
|
|
90
|
+
# `post` factory defined which automatically fills in `content`, you can
|
|
91
|
+
# say:
|
|
92
|
+
#
|
|
93
|
+
# RSpec.describe Post, type: :model do
|
|
94
|
+
# describe "validations" do
|
|
95
|
+
# subject { FactoryBot.build(:post) }
|
|
96
|
+
# it { should validate_uniqueness_of(:title) }
|
|
97
|
+
# end
|
|
98
|
+
# end
|
|
99
|
+
#
|
|
100
|
+
# #### Qualifiers
|
|
101
|
+
#
|
|
102
|
+
# Use `on` if your validation applies only under a certain context.
|
|
103
|
+
#
|
|
104
|
+
# class Post < ActiveRecord::Base
|
|
105
|
+
# validates :title, uniqueness: true, on: :create
|
|
106
|
+
# end
|
|
107
|
+
#
|
|
108
|
+
# # RSpec
|
|
109
|
+
# RSpec.describe Post, type: :model do
|
|
110
|
+
# it { should validate_uniqueness_of(:title).on(:create) }
|
|
111
|
+
# end
|
|
112
|
+
#
|
|
113
|
+
# # Minitest (Shoulda)
|
|
114
|
+
# class PostTest < ActiveSupport::TestCase
|
|
115
|
+
# should validate_uniqueness_of(:title).on(:create)
|
|
116
|
+
# end
|
|
117
|
+
#
|
|
118
|
+
# ##### with_message
|
|
119
|
+
#
|
|
120
|
+
# Use `with_message` if you are using a custom validation message.
|
|
121
|
+
#
|
|
122
|
+
# class Post < ActiveRecord::Base
|
|
123
|
+
# validates :title, uniqueness: true, message: 'Please choose another title'
|
|
124
|
+
# end
|
|
125
|
+
#
|
|
126
|
+
# # RSpec
|
|
127
|
+
# RSpec.describe Post, type: :model do
|
|
128
|
+
# it do
|
|
129
|
+
# should validate_uniqueness_of(:title).
|
|
130
|
+
# with_message('Please choose another title')
|
|
131
|
+
# end
|
|
132
|
+
# end
|
|
133
|
+
#
|
|
134
|
+
# # Minitest (Shoulda)
|
|
135
|
+
# class PostTest < ActiveSupport::TestCase
|
|
136
|
+
# should validate_uniqueness_of(:title).
|
|
137
|
+
# with_message('Please choose another title')
|
|
138
|
+
# end
|
|
139
|
+
#
|
|
140
|
+
# ##### scoped_to
|
|
141
|
+
#
|
|
142
|
+
# Use `scoped_to` to test usage of the `:scope` option. This asserts that
|
|
143
|
+
# a new record fails validation if not only the primary attribute is not
|
|
144
|
+
# unique, but the scoped attributes are not unique either.
|
|
145
|
+
#
|
|
146
|
+
# class Post < ActiveRecord::Base
|
|
147
|
+
# validates :slug, uniqueness: { scope: :journal_id }
|
|
148
|
+
# end
|
|
149
|
+
#
|
|
150
|
+
# # RSpec
|
|
151
|
+
# RSpec.describe Post, type: :model do
|
|
152
|
+
# it { should validate_uniqueness_of(:slug).scoped_to(:journal_id) }
|
|
153
|
+
# end
|
|
154
|
+
#
|
|
155
|
+
# # Minitest (Shoulda)
|
|
156
|
+
# class PostTest < ActiveSupport::TestCase
|
|
157
|
+
# should validate_uniqueness_of(:slug).scoped_to(:journal_id)
|
|
158
|
+
# end
|
|
159
|
+
#
|
|
160
|
+
# NOTE: Support for testing uniqueness validation scoped to an array of
|
|
161
|
+
# associations is not available.
|
|
162
|
+
#
|
|
163
|
+
# For more information, please refer to
|
|
164
|
+
# https://github.com/thoughtbot/shoulda-matchers/issues/814
|
|
165
|
+
#
|
|
166
|
+
# ##### case_insensitive
|
|
167
|
+
#
|
|
168
|
+
# Use `case_insensitive` to test usage of the `:case_sensitive` option
|
|
169
|
+
# with a false value. This asserts that the uniquable attributes fail
|
|
170
|
+
# validation even if their values are a different case than corresponding
|
|
171
|
+
# attributes in the pre-existing record.
|
|
172
|
+
#
|
|
173
|
+
# class Post < ActiveRecord::Base
|
|
174
|
+
# validates :key, uniqueness: { case_sensitive: false }
|
|
175
|
+
# end
|
|
176
|
+
#
|
|
177
|
+
# # RSpec
|
|
178
|
+
# RSpec.describe Post, type: :model do
|
|
179
|
+
# it { should validate_uniqueness_of(:key).case_insensitive }
|
|
180
|
+
# end
|
|
181
|
+
#
|
|
182
|
+
# # Minitest (Shoulda)
|
|
183
|
+
# class PostTest < ActiveSupport::TestCase
|
|
184
|
+
# should validate_uniqueness_of(:key).case_insensitive
|
|
185
|
+
# end
|
|
186
|
+
#
|
|
187
|
+
# ##### ignoring_case_sensitivity
|
|
188
|
+
#
|
|
189
|
+
# By default, `validate_uniqueness_of` will check that the
|
|
190
|
+
# validation is case sensitive: it asserts that uniquable attributes pass
|
|
191
|
+
# validation when their values are in a different case than corresponding
|
|
192
|
+
# attributes in the pre-existing record.
|
|
193
|
+
#
|
|
194
|
+
# Use `ignoring_case_sensitivity` to skip this check. This qualifier is
|
|
195
|
+
# particularly handy if your model has somehow changed the behavior of
|
|
196
|
+
# attribute you're testing so that it modifies the case of incoming values
|
|
197
|
+
# as they are set. For instance, perhaps you've overridden the writer
|
|
198
|
+
# method or added a `before_validation` callback to normalize the
|
|
199
|
+
# attribute.
|
|
200
|
+
#
|
|
201
|
+
# class User < ActiveRecord::Base
|
|
202
|
+
# validates :email, uniqueness: true
|
|
203
|
+
#
|
|
204
|
+
# def email=(value)
|
|
205
|
+
# super(value.downcase)
|
|
206
|
+
# end
|
|
207
|
+
# end
|
|
208
|
+
#
|
|
209
|
+
# # RSpec
|
|
210
|
+
# RSpec.describe Post, type: :model do
|
|
211
|
+
# it do
|
|
212
|
+
# should validate_uniqueness_of(:email).ignoring_case_sensitivity
|
|
213
|
+
# end
|
|
214
|
+
# end
|
|
215
|
+
#
|
|
216
|
+
# # Minitest (Shoulda)
|
|
217
|
+
# class PostTest < ActiveSupport::TestCase
|
|
218
|
+
# should validate_uniqueness_of(:email).ignoring_case_sensitivity
|
|
219
|
+
# end
|
|
220
|
+
#
|
|
221
|
+
# ##### allow_nil
|
|
222
|
+
#
|
|
223
|
+
# Use `allow_nil` to assert that the attribute allows nil.
|
|
224
|
+
#
|
|
225
|
+
# class Post < ActiveRecord::Base
|
|
226
|
+
# validates :author_id, uniqueness: true, allow_nil: true
|
|
227
|
+
# end
|
|
228
|
+
#
|
|
229
|
+
# # RSpec
|
|
230
|
+
# RSpec.describe Post, type: :model do
|
|
231
|
+
# it { should validate_uniqueness_of(:author_id).allow_nil }
|
|
232
|
+
# end
|
|
233
|
+
#
|
|
234
|
+
# # Minitest (Shoulda)
|
|
235
|
+
# class PostTest < ActiveSupport::TestCase
|
|
236
|
+
# should validate_uniqueness_of(:author_id).allow_nil
|
|
237
|
+
# end
|
|
238
|
+
#
|
|
239
|
+
# @return [ValidateUniquenessOfMatcher]
|
|
240
|
+
#
|
|
241
|
+
# ##### allow_blank
|
|
242
|
+
#
|
|
243
|
+
# Use `allow_blank` to assert that the attribute allows a blank value.
|
|
244
|
+
#
|
|
245
|
+
# class Post < ActiveRecord::Base
|
|
246
|
+
# validates :author_id, uniqueness: true, allow_blank: true
|
|
247
|
+
# end
|
|
248
|
+
#
|
|
249
|
+
# # RSpec
|
|
250
|
+
# RSpec.describe Post, type: :model do
|
|
251
|
+
# it { should validate_uniqueness_of(:author_id).allow_blank }
|
|
252
|
+
# end
|
|
253
|
+
#
|
|
254
|
+
# # Minitest (Shoulda)
|
|
255
|
+
# class PostTest < ActiveSupport::TestCase
|
|
256
|
+
# should validate_uniqueness_of(:author_id).allow_blank
|
|
257
|
+
# end
|
|
258
|
+
#
|
|
259
|
+
# @return [ValidateUniquenessOfMatcher]
|
|
260
|
+
#
|
|
261
|
+
# ##### alternatives
|
|
262
|
+
#
|
|
263
|
+
# Use `alternatives` to specify alternative valid values to use
|
|
264
|
+
# for testing uniqueness.
|
|
265
|
+
#
|
|
266
|
+
# class Post < ActiveRecord::Base
|
|
267
|
+
# validates :title, uniqueness: true
|
|
268
|
+
# end
|
|
269
|
+
#
|
|
270
|
+
# # RSpec
|
|
271
|
+
# RSpec.describe Post, type: :model do
|
|
272
|
+
# it do
|
|
273
|
+
# should validate_uniqueness_of(:title).alternatives('Alternative Title')
|
|
274
|
+
# end
|
|
275
|
+
# end
|
|
276
|
+
#
|
|
277
|
+
# # Minitest (Shoulda)
|
|
278
|
+
# class PostTest < ActiveSupport::TestCase
|
|
279
|
+
# should validate_uniqueness_of(:title).alternatives(['Alternative Title', 'Another Title'])
|
|
280
|
+
# end
|
|
281
|
+
#
|
|
282
|
+
# @param values [String, Array<String>]
|
|
283
|
+
# Alternative value(s) to use for testing uniqueness instead of using
|
|
284
|
+
# the `succ` operator on the existing value.
|
|
285
|
+
#
|
|
286
|
+
# @return [ValidateUniquenessOfMatcher]
|
|
287
|
+
#
|
|
288
|
+
# @example
|
|
289
|
+
# it { should validate_uniqueness_of(:title).alternatives('Alternative Title') }
|
|
290
|
+
# it { should validate_uniqueness_of(:title).alternatives(['Title 1', 'Title 2']) }
|
|
291
|
+
def validate_uniqueness_of(attr)
|
|
292
|
+
ValidateUniquenessOfMatcher.new(attr)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# @private
|
|
296
|
+
class ValidateUniquenessOfMatcher < ActiveModel::ValidationMatcher
|
|
297
|
+
include ActiveModel::Helpers
|
|
298
|
+
|
|
299
|
+
def initialize(attribute)
|
|
300
|
+
super(attribute)
|
|
301
|
+
@expected_message = :taken
|
|
302
|
+
@options = {
|
|
303
|
+
case_sensitivity_strategy: :sensitive,
|
|
304
|
+
}
|
|
305
|
+
@existing_record_created = false
|
|
306
|
+
@failure_reason = nil
|
|
307
|
+
@attribute_setters = {
|
|
308
|
+
existing_record: AttributeSetters.new,
|
|
309
|
+
new_record: AttributeSetters.new,
|
|
310
|
+
}
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def scoped_to(*scopes)
|
|
314
|
+
@options[:scopes] = [*scopes].flatten.map(&:to_sym)
|
|
315
|
+
self
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# @param values [String, Array<String>]
|
|
319
|
+
# Alternative value(s) to use for testing uniqueness instead of using
|
|
320
|
+
# the `succ` operator on the existing value.
|
|
321
|
+
#
|
|
322
|
+
# @return [ValidateUniquenessOfMatcher]
|
|
323
|
+
#
|
|
324
|
+
# @example
|
|
325
|
+
# it { should validate_uniqueness_of(:title).alternatives('Alternative Title') }
|
|
326
|
+
# it { should validate_uniqueness_of(:title).alternatives(['Title 1', 'Title 2']) }
|
|
327
|
+
def alternatives(values)
|
|
328
|
+
@options[:alternatives] = values
|
|
329
|
+
self
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def case_insensitive
|
|
333
|
+
@options[:case_sensitivity_strategy] = :insensitive
|
|
334
|
+
self
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def ignoring_case_sensitivity
|
|
338
|
+
@options[:case_sensitivity_strategy] = :ignore
|
|
339
|
+
self
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def allow_nil
|
|
343
|
+
@options[:allow_nil] = true
|
|
344
|
+
self
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def expects_to_allow_nil?
|
|
348
|
+
@options[:allow_nil] == true
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def allow_blank
|
|
352
|
+
@options[:allow_blank] = true
|
|
353
|
+
self
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def expects_to_allow_blank?
|
|
357
|
+
@options[:allow_blank] == true
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def simple_description
|
|
361
|
+
description = "validate that :#{@attribute} is"
|
|
362
|
+
description << description_for_case_sensitive_qualifier
|
|
363
|
+
description << ' unique'
|
|
364
|
+
|
|
365
|
+
if @options[:scopes].present?
|
|
366
|
+
description << " within the scope of #{inspected_expected_scopes}"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
description
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def matches?(given_record)
|
|
373
|
+
@given_record = given_record
|
|
374
|
+
@all_records = model.all
|
|
375
|
+
|
|
376
|
+
matches_presence_of_attribute? &&
|
|
377
|
+
matches_presence_of_scopes? &&
|
|
378
|
+
matches_scopes_configuration? &&
|
|
379
|
+
matches_uniqueness_without_scopes? &&
|
|
380
|
+
matches_uniqueness_with_case_sensitivity_strategy? &&
|
|
381
|
+
matches_uniqueness_with_scopes? &&
|
|
382
|
+
matches_allow_nil? &&
|
|
383
|
+
matches_allow_blank?
|
|
384
|
+
ensure
|
|
385
|
+
Uniqueness::TestModels.remove_all
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def does_not_match?(given_record)
|
|
389
|
+
@given_record = given_record
|
|
390
|
+
@all_records = model.all
|
|
391
|
+
|
|
392
|
+
does_not_match_presence_of_scopes? ||
|
|
393
|
+
does_not_match_scopes_configuration? ||
|
|
394
|
+
does_not_match_uniqueness_without_scopes? ||
|
|
395
|
+
does_not_match_uniqueness_with_case_sensitivity_strategy? ||
|
|
396
|
+
does_not_match_uniqueness_with_scopes? ||
|
|
397
|
+
does_not_match_allow_nil? ||
|
|
398
|
+
does_not_match_allow_blank?
|
|
399
|
+
ensure
|
|
400
|
+
Uniqueness::TestModels.remove_all
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
protected
|
|
404
|
+
|
|
405
|
+
def failure_reason
|
|
406
|
+
@failure_reason || super
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def build_allow_or_disallow_value_matcher(args)
|
|
410
|
+
super.tap do |matcher|
|
|
411
|
+
matcher.failure_message_preface = method(:failure_message_preface)
|
|
412
|
+
matcher.attribute_changed_value_message =
|
|
413
|
+
method(:attribute_changed_value_message)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
private
|
|
418
|
+
|
|
419
|
+
def case_sensitivity_strategy
|
|
420
|
+
@options[:case_sensitivity_strategy]
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def new_record
|
|
424
|
+
unless defined?(@new_record)
|
|
425
|
+
build_new_record
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
@new_record
|
|
429
|
+
end
|
|
430
|
+
alias_method :subject, :new_record
|
|
431
|
+
|
|
432
|
+
def description_for_case_sensitive_qualifier
|
|
433
|
+
case case_sensitivity_strategy
|
|
434
|
+
when :sensitive
|
|
435
|
+
' case-sensitively'
|
|
436
|
+
when :insensitive
|
|
437
|
+
' case-insensitively'
|
|
438
|
+
else
|
|
439
|
+
''
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def validations
|
|
444
|
+
model.validators_on(@attribute).select do |validator|
|
|
445
|
+
validator.is_a?(::ActiveRecord::Validations::UniquenessValidator)
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def matches_scopes_configuration?
|
|
450
|
+
if scopes_match?
|
|
451
|
+
true
|
|
452
|
+
else
|
|
453
|
+
@failure_reason = String.new('Expected the validation ').tap do |failure_reason_string|
|
|
454
|
+
failure_reason_string <<
|
|
455
|
+
if expected_scopes.empty?
|
|
456
|
+
'not to be scoped to anything, '
|
|
457
|
+
else
|
|
458
|
+
"to be scoped to #{inspected_expected_scopes}, "
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
if actual_sets_of_scopes.any?
|
|
462
|
+
failure_reason_string << 'but it was scoped to '
|
|
463
|
+
failure_reason_string << "#{inspected_actual_scopes} instead."
|
|
464
|
+
else
|
|
465
|
+
failure_reason_string << 'but it was not scoped to anything.'
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
false
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def build_failure_reason
|
|
474
|
+
String.new('Expected the validation ').tap do |failure_reason_string|
|
|
475
|
+
failure_reason_string <<
|
|
476
|
+
if expected_scopes.empty?
|
|
477
|
+
'not to be scoped to anything, '
|
|
478
|
+
else
|
|
479
|
+
"to be scoped to #{inspected_expected_scopes}, "
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
if actual_sets_of_scopes.any?
|
|
483
|
+
failure_reason_string << 'but it was scoped to '
|
|
484
|
+
failure_reason_string << "#{inspected_actual_scopes} instead."
|
|
485
|
+
else
|
|
486
|
+
failure_reason_string << 'but it was not scoped to anything.'
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def does_not_match_scopes_configuration?
|
|
492
|
+
if scopes_match?
|
|
493
|
+
@failure_reason = String.new('Expected the validation ').tap do |failure_reason|
|
|
494
|
+
if expected_scopes.empty?
|
|
495
|
+
failure_reason << 'to be scoped to nothing, '
|
|
496
|
+
failure_reason << 'but it was scoped to '
|
|
497
|
+
failure_reason << "#{inspected_actual_scopes} instead."
|
|
498
|
+
else
|
|
499
|
+
failure_reason << 'not to be scoped to '
|
|
500
|
+
failure_reason << inspected_expected_scopes
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
false
|
|
505
|
+
else
|
|
506
|
+
true
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def scopes_match?
|
|
511
|
+
actual_sets_of_scopes.empty? && expected_scopes.empty? ||
|
|
512
|
+
actual_sets_of_scopes.any? { |scopes| scopes == expected_scopes }
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def inspected_expected_scopes
|
|
516
|
+
expected_scopes.map(&:inspect).to_sentence
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def inspected_actual_scopes
|
|
520
|
+
inspected_actual_sets_of_scopes.to_sentence(
|
|
521
|
+
words_connector: ' and ',
|
|
522
|
+
last_word_connector: ', and',
|
|
523
|
+
)
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def inspected_actual_sets_of_scopes
|
|
527
|
+
inspected_sets_of_scopes = actual_sets_of_scopes.map do |scopes|
|
|
528
|
+
scopes.map(&:inspect)
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
if inspected_sets_of_scopes.many?
|
|
532
|
+
inspected_sets_of_scopes.map { |x| "(#{x.to_sentence})" }
|
|
533
|
+
else
|
|
534
|
+
inspected_sets_of_scopes.map(&:to_sentence)
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def expected_scopes
|
|
539
|
+
Array.wrap(@options[:scopes])
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def actual_sets_of_scopes
|
|
543
|
+
validations.map do |validation|
|
|
544
|
+
Array.wrap(validation.options[:scope]).map(&:to_sym)
|
|
545
|
+
end.reject(&:empty?)
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def matches_allow_nil?
|
|
549
|
+
!expects_to_allow_nil? || (
|
|
550
|
+
update_existing_record!(nil) &&
|
|
551
|
+
allows_value_of(nil, @expected_message)
|
|
552
|
+
)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def does_not_match_allow_nil?
|
|
556
|
+
expects_to_allow_nil? && update_existing_record!(nil) &&
|
|
557
|
+
(@failure_reason = disallows_value_of(nil, @expected_message)
|
|
558
|
+
)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def matches_allow_blank?
|
|
562
|
+
!expects_to_allow_blank? || (
|
|
563
|
+
update_existing_record!('') &&
|
|
564
|
+
allows_value_of('', @expected_message)
|
|
565
|
+
)
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def does_not_match_allow_blank?
|
|
569
|
+
expects_to_allow_blank? && update_existing_record!('') &&
|
|
570
|
+
(@failure_reason = disallows_value_of('', @expected_message))
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def existing_record
|
|
574
|
+
unless defined?(@existing_record)
|
|
575
|
+
find_or_create_existing_record
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
@existing_record
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def find_or_create_existing_record
|
|
582
|
+
@existing_record = find_existing_record
|
|
583
|
+
|
|
584
|
+
unless @existing_record
|
|
585
|
+
@existing_record = create_existing_record
|
|
586
|
+
@existing_record_created = true
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def find_existing_record
|
|
591
|
+
model.first.presence
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def create_existing_record
|
|
595
|
+
@given_record.tap do |existing_record|
|
|
596
|
+
existing_record.save(validate: false)
|
|
597
|
+
end
|
|
598
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
|
599
|
+
raise ExistingRecordInvalid.create(underlying_exception: e)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def update_existing_record!(value)
|
|
603
|
+
if existing_value_read != value
|
|
604
|
+
set_attribute_on_existing_record!(@attribute, value)
|
|
605
|
+
# It would be nice if we could ensure that the record was valid,
|
|
606
|
+
# but that would break users' existing tests
|
|
607
|
+
existing_record.save(validate: false)
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
true
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def arbitrary_non_blank_value
|
|
614
|
+
non_blank_value = dummy_value_for(@attribute)
|
|
615
|
+
limit = column_limit_for(@attribute)
|
|
616
|
+
|
|
617
|
+
is_string_value = non_blank_value.is_a?(String)
|
|
618
|
+
if is_string_value && limit && limit < non_blank_value.length
|
|
619
|
+
'x' * limit
|
|
620
|
+
else
|
|
621
|
+
non_blank_value
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def has_secure_password?
|
|
626
|
+
Shoulda::Matchers::RailsShim.has_secure_password?(subject, @attribute)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def build_new_record
|
|
630
|
+
@new_record = existing_record.dup
|
|
631
|
+
|
|
632
|
+
attribute_names_under_test.each do |attribute_name|
|
|
633
|
+
set_attribute_on_new_record!(
|
|
634
|
+
attribute_name,
|
|
635
|
+
existing_record.public_send(attribute_name),
|
|
636
|
+
)
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
@new_record
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def matches_presence_of_attribute?
|
|
643
|
+
if attribute_present_on_model?
|
|
644
|
+
true
|
|
645
|
+
else
|
|
646
|
+
@failure_reason =
|
|
647
|
+
":#{attribute} does not seem to be an attribute on #{model.name}."
|
|
648
|
+
false
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def does_not_match_presence_of_attribute?
|
|
653
|
+
if attribute_present_on_model?
|
|
654
|
+
@failure_reason =
|
|
655
|
+
":#{attribute} seems to be an attribute on #{model.name}."
|
|
656
|
+
false
|
|
657
|
+
else
|
|
658
|
+
true
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def attribute_present_on_model?
|
|
663
|
+
model.method_defined?("#{attribute}=") ||
|
|
664
|
+
model.columns_hash.key?(attribute.to_s)
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def matches_presence_of_scopes?
|
|
668
|
+
if scopes_missing_on_model.none?
|
|
669
|
+
true
|
|
670
|
+
else
|
|
671
|
+
inspected_scopes = scopes_missing_on_model.map(&:inspect)
|
|
672
|
+
|
|
673
|
+
@failure_reason = String.new.tap do |reason|
|
|
674
|
+
reason << inspected_scopes.to_sentence
|
|
675
|
+
|
|
676
|
+
reason <<
|
|
677
|
+
if inspected_scopes.many?
|
|
678
|
+
' do not seem to be attributes'
|
|
679
|
+
else
|
|
680
|
+
' does not seem to be an attribute'
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
reason << " on #{model.name}."
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
false
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def does_not_match_presence_of_scopes?
|
|
691
|
+
if scopes_missing_on_model.any?
|
|
692
|
+
true
|
|
693
|
+
else
|
|
694
|
+
inspected_scopes = scopes_present_on_model.map(&:inspect)
|
|
695
|
+
|
|
696
|
+
@failure_reason = String.new.tap do |reason|
|
|
697
|
+
reason << inspected_scopes.to_sentence
|
|
698
|
+
|
|
699
|
+
reason <<
|
|
700
|
+
if inspected_scopes.many?
|
|
701
|
+
' seem to be attributes'
|
|
702
|
+
else
|
|
703
|
+
' seems to be an attribute'
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
reason << " on #{model.name}."
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
false
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def scopes_present_on_model
|
|
714
|
+
@_scopes_present_on_model ||= expected_scopes.select do |scope|
|
|
715
|
+
model.method_defined?("#{scope}=")
|
|
716
|
+
end
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def scopes_missing_on_model
|
|
720
|
+
@_scopes_missing_on_model ||= expected_scopes.reject do |scope|
|
|
721
|
+
model.method_defined?("#{scope}=")
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
def matches_uniqueness_without_scopes?
|
|
726
|
+
if existing_value_read.blank?
|
|
727
|
+
update_existing_record!(arbitrary_non_blank_value)
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
disallows_value_of(existing_value_read, @expected_message)
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
def does_not_match_uniqueness_without_scopes?
|
|
734
|
+
@failure_reason = nil
|
|
735
|
+
|
|
736
|
+
if existing_value_read.blank?
|
|
737
|
+
update_existing_record!(arbitrary_non_blank_value)
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
allows_value_of(existing_value_read, @expected_message)
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
def matches_uniqueness_with_case_sensitivity_strategy?
|
|
744
|
+
if should_test_case_sensitivity?
|
|
745
|
+
value = existing_value_read
|
|
746
|
+
swapcased_value = value.swapcase
|
|
747
|
+
|
|
748
|
+
if case_sensitivity_strategy == :sensitive
|
|
749
|
+
if value == swapcased_value
|
|
750
|
+
raise NonCaseSwappableValueError.create(
|
|
751
|
+
model:,
|
|
752
|
+
attribute: @attribute,
|
|
753
|
+
value:,
|
|
754
|
+
)
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
allows_value_of(swapcased_value, @expected_message)
|
|
758
|
+
else
|
|
759
|
+
disallows_value_of(swapcased_value, @expected_message)
|
|
760
|
+
end
|
|
761
|
+
else
|
|
762
|
+
true
|
|
763
|
+
end
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
def does_not_match_uniqueness_with_case_sensitivity_strategy?
|
|
767
|
+
if should_test_case_sensitivity?
|
|
768
|
+
@failure_reason = nil
|
|
769
|
+
|
|
770
|
+
value = existing_value_read
|
|
771
|
+
swapcased_value = value.swapcase
|
|
772
|
+
|
|
773
|
+
if case_sensitivity_strategy == :sensitive
|
|
774
|
+
disallows_value_of(swapcased_value, @expected_message)
|
|
775
|
+
else
|
|
776
|
+
if value == swapcased_value
|
|
777
|
+
raise NonCaseSwappableValueError.create(
|
|
778
|
+
model:,
|
|
779
|
+
attribute: @attribute,
|
|
780
|
+
value:,
|
|
781
|
+
)
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
allows_value_of(swapcased_value, @expected_message)
|
|
785
|
+
end
|
|
786
|
+
else
|
|
787
|
+
true
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
def should_test_case_sensitivity?
|
|
792
|
+
case_sensitivity_strategy != :ignore &&
|
|
793
|
+
existing_value_read.respond_to?(:swapcase) &&
|
|
794
|
+
!existing_value_read.empty?
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
def model_class?(model_name)
|
|
798
|
+
model_name.constantize.ancestors.include?(::ActiveRecord::Base)
|
|
799
|
+
rescue NameError
|
|
800
|
+
false
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
def matches_uniqueness_with_scopes?
|
|
804
|
+
expected_scopes.none? ||
|
|
805
|
+
all_scopes_are_booleans? ||
|
|
806
|
+
expected_scopes.all? do |scope|
|
|
807
|
+
setting_next_value_for(scope) do
|
|
808
|
+
allows_value_of(existing_value_read, @expected_message)
|
|
809
|
+
end
|
|
810
|
+
end
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def does_not_match_uniqueness_with_scopes?
|
|
814
|
+
expected_scopes.any? &&
|
|
815
|
+
!all_scopes_are_booleans? &&
|
|
816
|
+
expected_scopes.any? do |scope|
|
|
817
|
+
setting_next_value_for(scope) do
|
|
818
|
+
@failure_reason = nil
|
|
819
|
+
disallows_value_of(existing_value_read, @expected_message)
|
|
820
|
+
end
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
def setting_next_value_for(scope)
|
|
825
|
+
previous_value = @all_records.map(&scope).compact.max
|
|
826
|
+
|
|
827
|
+
next_value =
|
|
828
|
+
if previous_value.blank?
|
|
829
|
+
dummy_value_for(scope)
|
|
830
|
+
else
|
|
831
|
+
next_value_for(scope, previous_value)
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
set_attribute_on_new_record!(scope, next_value)
|
|
835
|
+
|
|
836
|
+
yield
|
|
837
|
+
ensure
|
|
838
|
+
set_attribute_on_new_record!(scope, previous_value)
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
def dummy_value_for(scope)
|
|
842
|
+
column = column_for(scope)
|
|
843
|
+
|
|
844
|
+
if column.respond_to?(:array) && column.array
|
|
845
|
+
[dummy_scalar_value_for(column)]
|
|
846
|
+
else
|
|
847
|
+
dummy_scalar_value_for(column)
|
|
848
|
+
end
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
def dummy_scalar_value_for(column)
|
|
852
|
+
Shoulda::Matchers::Util.dummy_value_for(column.type)
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
def next_value_for(scope, previous_value)
|
|
856
|
+
if previous_value.is_a?(Array)
|
|
857
|
+
[next_scalar_value_for(scope, previous_value[0])]
|
|
858
|
+
else
|
|
859
|
+
next_scalar_value_for(scope, previous_value)
|
|
860
|
+
end
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
def next_scalar_value_for(scope, previous_value)
|
|
864
|
+
column = column_for(scope)
|
|
865
|
+
|
|
866
|
+
if column.type == :uuid
|
|
867
|
+
SecureRandom.uuid
|
|
868
|
+
elsif defined_as_enum?(scope)
|
|
869
|
+
available_values = available_enum_values_for(scope, previous_value)
|
|
870
|
+
available_values.keys.last
|
|
871
|
+
elsif polymorphic_type_attribute?(scope, previous_value)
|
|
872
|
+
Uniqueness::TestModels.create(previous_value).to_s
|
|
873
|
+
elsif @options[:alternatives] && scope == @attribute
|
|
874
|
+
alternatives = Array.wrap(@options[:alternatives])
|
|
875
|
+
alternatives.first
|
|
876
|
+
elsif previous_value.respond_to?(:next)
|
|
877
|
+
previous_value.next
|
|
878
|
+
elsif previous_value.respond_to?(:to_datetime)
|
|
879
|
+
previous_value.to_datetime.in(60).next
|
|
880
|
+
elsif boolean_value?(previous_value)
|
|
881
|
+
!previous_value
|
|
882
|
+
else
|
|
883
|
+
previous_value.to_s.next
|
|
884
|
+
end
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
def all_scopes_are_booleans?
|
|
888
|
+
@options[:scopes].all? do |scope|
|
|
889
|
+
@all_records.map(&scope).all? { |s| boolean_value?(s) }
|
|
890
|
+
end
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
def boolean_value?(value)
|
|
894
|
+
[true, false].include?(value)
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
def defined_as_enum?(scope)
|
|
898
|
+
model.respond_to?(:defined_enums) &&
|
|
899
|
+
new_record.defined_enums[scope.to_s]
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
def polymorphic_type_attribute?(scope, previous_value)
|
|
903
|
+
scope.to_s =~ /_type$/ && model_class?(previous_value)
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
def available_enum_values_for(scope, previous_value)
|
|
907
|
+
new_record.defined_enums[scope.to_s].reject do |key, _|
|
|
908
|
+
key == previous_value
|
|
909
|
+
end
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
def set_attribute_on!(record_type, record, attribute_name, value)
|
|
913
|
+
attribute_setter = build_attribute_setter(
|
|
914
|
+
record,
|
|
915
|
+
attribute_name,
|
|
916
|
+
value,
|
|
917
|
+
)
|
|
918
|
+
attribute_setter.set!
|
|
919
|
+
|
|
920
|
+
@attribute_setters[record_type] << attribute_setter
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
def set_attribute_on_existing_record!(attribute_name, value)
|
|
924
|
+
set_attribute_on!(
|
|
925
|
+
:existing_record,
|
|
926
|
+
existing_record,
|
|
927
|
+
attribute_name,
|
|
928
|
+
value,
|
|
929
|
+
)
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
def set_attribute_on_new_record!(attribute_name, value)
|
|
933
|
+
set_attribute_on!(
|
|
934
|
+
:new_record,
|
|
935
|
+
new_record,
|
|
936
|
+
attribute_name,
|
|
937
|
+
value,
|
|
938
|
+
)
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
def attribute_setter_for_existing_record
|
|
942
|
+
@attribute_setters[:existing_record].last
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
def attribute_setters_for_new_record
|
|
946
|
+
@attribute_setters[:new_record] +
|
|
947
|
+
[last_attribute_setter_used_on_new_record]
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
def attribute_names_under_test
|
|
951
|
+
[@attribute] + expected_scopes
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
def build_attribute_setter(record, attribute_name, value)
|
|
955
|
+
Shoulda::Matchers::ActiveModel::AllowValueMatcher::AttributeSetter.
|
|
956
|
+
new(
|
|
957
|
+
matcher_name: :validate_uniqueness_of,
|
|
958
|
+
object: record,
|
|
959
|
+
attribute_name:,
|
|
960
|
+
value:,
|
|
961
|
+
ignore_interference_by_writer:,
|
|
962
|
+
)
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
def existing_value_read
|
|
966
|
+
existing_record.public_send(@attribute)
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
def existing_value_written
|
|
970
|
+
if attribute_setter_for_existing_record
|
|
971
|
+
attribute_setter_for_existing_record.value_written
|
|
972
|
+
else
|
|
973
|
+
existing_value_read
|
|
974
|
+
end
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
def column_for(scope)
|
|
978
|
+
model.columns_hash[scope.to_s]
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
def column_limit_for(attribute)
|
|
982
|
+
column_for(attribute).try(:limit)
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
def model
|
|
986
|
+
@given_record.class
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
def failure_message_preface # rubocop:disable Metrics/MethodLength
|
|
990
|
+
String.new.tap do |prefix|
|
|
991
|
+
if @existing_record_created
|
|
992
|
+
prefix << "After taking the given #{model.name}"
|
|
993
|
+
|
|
994
|
+
if attribute_setter_for_existing_record
|
|
995
|
+
prefix << ', setting '
|
|
996
|
+
prefix << description_for_attribute_setter(
|
|
997
|
+
attribute_setter_for_existing_record,
|
|
998
|
+
)
|
|
999
|
+
else
|
|
1000
|
+
prefix << ", whose :#{attribute} is "
|
|
1001
|
+
prefix << "‹#{existing_value_read.inspect}›"
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
prefix << ', and saving it as the existing record, then'
|
|
1005
|
+
elsif attribute_setter_for_existing_record
|
|
1006
|
+
prefix << "Given an existing #{model.name},"
|
|
1007
|
+
prefix << ' after setting '
|
|
1008
|
+
prefix << description_for_attribute_setter(
|
|
1009
|
+
attribute_setter_for_existing_record,
|
|
1010
|
+
)
|
|
1011
|
+
prefix << ', then'
|
|
1012
|
+
else
|
|
1013
|
+
prefix << "Given an existing #{model.name} whose :#{attribute}"
|
|
1014
|
+
prefix << ' is '
|
|
1015
|
+
prefix << Shoulda::Matchers::Util.inspect_value(
|
|
1016
|
+
existing_value_read,
|
|
1017
|
+
)
|
|
1018
|
+
prefix << ', after'
|
|
1019
|
+
end
|
|
1020
|
+
|
|
1021
|
+
prefix << " making a new #{model.name} and setting "
|
|
1022
|
+
|
|
1023
|
+
prefix << descriptions_for_attribute_setters_for_new_record
|
|
1024
|
+
|
|
1025
|
+
prefix << ", the matcher expected the new #{model.name} to be"
|
|
1026
|
+
end
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
def attribute_changed_value_message
|
|
1030
|
+
<<-MESSAGE.strip
|
|
1031
|
+
As indicated in the message above,
|
|
1032
|
+
:#{last_attribute_setter_used_on_new_record.attribute_name} seems to be
|
|
1033
|
+
changing certain values as they are set, and this could have something
|
|
1034
|
+
to do with why this test is failing. If you or something else has
|
|
1035
|
+
overridden the writer method for this attribute to normalize values by
|
|
1036
|
+
changing their case in any way (for instance, ensuring that the
|
|
1037
|
+
attribute is always downcased), then try adding
|
|
1038
|
+
`ignoring_case_sensitivity` onto the end of the uniqueness matcher.
|
|
1039
|
+
Otherwise, you may need to write the test yourself, or do something
|
|
1040
|
+
different altogether.
|
|
1041
|
+
MESSAGE
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1044
|
+
def description_for_attribute_setter(
|
|
1045
|
+
attribute_setter,
|
|
1046
|
+
same_as_existing: nil
|
|
1047
|
+
)
|
|
1048
|
+
description = "its :#{attribute_setter.attribute_name} to "
|
|
1049
|
+
|
|
1050
|
+
if same_as_existing == false
|
|
1051
|
+
description << 'a different value, '
|
|
1052
|
+
end
|
|
1053
|
+
|
|
1054
|
+
description << Shoulda::Matchers::Util.inspect_value(
|
|
1055
|
+
attribute_setter.value_written,
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
if attribute_setter.attribute_changed_value?
|
|
1059
|
+
description << ' (read back as '
|
|
1060
|
+
description << Shoulda::Matchers::Util.inspect_value(
|
|
1061
|
+
attribute_setter.value_read,
|
|
1062
|
+
)
|
|
1063
|
+
description << ')'
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
if same_as_existing == true
|
|
1067
|
+
description << ' as well'
|
|
1068
|
+
end
|
|
1069
|
+
|
|
1070
|
+
description
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
def descriptions_for_attribute_setters_for_new_record
|
|
1074
|
+
attribute_setter_descriptions_for_new_record.to_sentence
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
def attribute_setter_descriptions_for_new_record
|
|
1078
|
+
attribute_setters_for_new_record.map do |attribute_setter|
|
|
1079
|
+
same_as_existing = (
|
|
1080
|
+
attribute_setter.value_written ==
|
|
1081
|
+
existing_value_written
|
|
1082
|
+
)
|
|
1083
|
+
description_for_attribute_setter(
|
|
1084
|
+
attribute_setter,
|
|
1085
|
+
same_as_existing:,
|
|
1086
|
+
)
|
|
1087
|
+
end
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
def existing_and_new_values_are_same?
|
|
1091
|
+
last_value_set_on_new_record == existing_value_written
|
|
1092
|
+
end
|
|
1093
|
+
|
|
1094
|
+
def last_attribute_setter_used_on_new_record
|
|
1095
|
+
last_submatcher_run.last_attribute_setter_used
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
def last_value_set_on_new_record
|
|
1099
|
+
last_submatcher_run.last_value_set
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1102
|
+
# @private
|
|
1103
|
+
class AttributeSetters
|
|
1104
|
+
include Enumerable
|
|
1105
|
+
|
|
1106
|
+
def initialize
|
|
1107
|
+
@attribute_setters = []
|
|
1108
|
+
end
|
|
1109
|
+
|
|
1110
|
+
def <<(given_attribute_setter)
|
|
1111
|
+
index = find_index_of(given_attribute_setter)
|
|
1112
|
+
|
|
1113
|
+
if index
|
|
1114
|
+
@attribute_setters[index] = given_attribute_setter
|
|
1115
|
+
else
|
|
1116
|
+
@attribute_setters << given_attribute_setter
|
|
1117
|
+
end
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
def +(other_attribute_setters)
|
|
1121
|
+
dup.tap do |attribute_setters|
|
|
1122
|
+
other_attribute_setters.each do |attribute_setter|
|
|
1123
|
+
attribute_setters << attribute_setter
|
|
1124
|
+
end
|
|
1125
|
+
end
|
|
1126
|
+
end
|
|
1127
|
+
|
|
1128
|
+
def each(&block)
|
|
1129
|
+
@attribute_setters.each(&block)
|
|
1130
|
+
end
|
|
1131
|
+
|
|
1132
|
+
def last
|
|
1133
|
+
@attribute_setters.last
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
private
|
|
1137
|
+
|
|
1138
|
+
def find_index_of(given_attribute_setter)
|
|
1139
|
+
@attribute_setters.find_index do |attribute_setter|
|
|
1140
|
+
attribute_setter.attribute_name ==
|
|
1141
|
+
given_attribute_setter.attribute_name
|
|
1142
|
+
end
|
|
1143
|
+
end
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
# @private
|
|
1147
|
+
class NonCaseSwappableValueError < Shoulda::Matchers::Error
|
|
1148
|
+
attr_accessor :model, :attribute, :value
|
|
1149
|
+
|
|
1150
|
+
def message
|
|
1151
|
+
Shoulda::Matchers.word_wrap <<-MESSAGE
|
|
1152
|
+
Your #{model.name} model has a uniqueness validation on :#{attribute} which is
|
|
1153
|
+
declared to be case-sensitive, but the value the uniqueness matcher used,
|
|
1154
|
+
#{value.inspect}, doesn't contain any alpha characters, so using it to
|
|
1155
|
+
test the case-sensitivity part of the validation is ineffective. There are
|
|
1156
|
+
two possible solutions for this depending on what you're trying to do here:
|
|
1157
|
+
|
|
1158
|
+
a) If you meant for the validation to be case-sensitive, then you need to give
|
|
1159
|
+
the uniqueness matcher a saved instance of #{model.name} with a value for
|
|
1160
|
+
:#{attribute} that contains alpha characters.
|
|
1161
|
+
|
|
1162
|
+
b) If you meant for the validation to be case-insensitive, then you need to
|
|
1163
|
+
add `case_sensitive: false` to the validation and add `case_insensitive` to
|
|
1164
|
+
the matcher.
|
|
1165
|
+
|
|
1166
|
+
For more information, please see:
|
|
1167
|
+
|
|
1168
|
+
https://matchers.shoulda.io/docs/v#{Shoulda::Matchers::VERSION}/file.NonCaseSwappableValueError.html
|
|
1169
|
+
MESSAGE
|
|
1170
|
+
end
|
|
1171
|
+
end
|
|
1172
|
+
|
|
1173
|
+
# @private
|
|
1174
|
+
class ExistingRecordInvalid < Shoulda::Matchers::Error
|
|
1175
|
+
include Shoulda::Matchers::ActiveModel::Helpers
|
|
1176
|
+
|
|
1177
|
+
attr_accessor :underlying_exception
|
|
1178
|
+
|
|
1179
|
+
def message
|
|
1180
|
+
<<-MESSAGE.strip
|
|
1181
|
+
validate_uniqueness_of works by matching a new record against an
|
|
1182
|
+
existing record. If there is no existing record, it will create one
|
|
1183
|
+
using the record you provide.
|
|
1184
|
+
|
|
1185
|
+
While doing this, the following error was raised:
|
|
1186
|
+
|
|
1187
|
+
#{Shoulda::Matchers::Util.indent(underlying_exception.message, 2)}
|
|
1188
|
+
|
|
1189
|
+
The best way to fix this is to provide the matcher with a record where
|
|
1190
|
+
any required attributes are filled in with valid values beforehand.
|
|
1191
|
+
MESSAGE
|
|
1192
|
+
end
|
|
1193
|
+
end
|
|
1194
|
+
end
|
|
1195
|
+
end
|
|
1196
|
+
end
|
|
1197
|
+
end
|