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.
Files changed (140) hide show
  1. checksums.yaml +7 -0
  2. data/shoulda-matchers-8.0.1/LICENSE +22 -0
  3. data/shoulda-matchers-8.0.1/README.md +567 -0
  4. data/shoulda-matchers-8.0.1/docs/errors/NonCaseSwappableValueError.md +111 -0
  5. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/callback_matcher.rb +121 -0
  6. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +70 -0
  7. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/flash_store.rb +86 -0
  8. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/permit_matcher.rb +445 -0
  9. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +96 -0
  10. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/render_template_matcher.rb +84 -0
  11. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +145 -0
  12. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/rescue_from_matcher.rb +115 -0
  13. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +148 -0
  14. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/route_matcher.rb +207 -0
  15. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/route_params.rb +62 -0
  16. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/session_store.rb +34 -0
  17. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/set_flash_matcher.rb +228 -0
  18. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/set_session_matcher.rb +163 -0
  19. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller/set_session_or_flash_matcher.rb +107 -0
  20. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/action_controller.rb +24 -0
  21. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_changed_value_error.rb +45 -0
  22. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_does_not_exist_error.rb +23 -0
  23. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter.rb +232 -0
  24. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter_and_validator.rb +62 -0
  25. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters.rb +40 -0
  26. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb +48 -0
  27. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/successful_check.rb +14 -0
  28. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher/successful_setting.rb +14 -0
  29. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/allow_value_matcher.rb +695 -0
  30. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/comparison_matcher.rb +157 -0
  31. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +78 -0
  32. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/errors.rb +52 -0
  33. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +125 -0
  34. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/helpers.rb +37 -0
  35. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +45 -0
  36. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +67 -0
  37. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +45 -0
  38. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +44 -0
  39. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/numericality_matchers/range_matcher.rb +71 -0
  40. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb +53 -0
  41. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/numericality_matchers.rb +9 -0
  42. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/qualifiers/allow_blank.rb +26 -0
  43. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/qualifiers/allow_nil.rb +26 -0
  44. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/qualifiers/ignore_interference_by_writer.rb +101 -0
  45. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/qualifiers/ignoring_interference_by_writer.rb +21 -0
  46. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/qualifiers.rb +14 -0
  47. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +180 -0
  48. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +128 -0
  49. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +567 -0
  50. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +185 -0
  51. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +272 -0
  52. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +664 -0
  53. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +575 -0
  54. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +711 -0
  55. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +428 -0
  56. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validation_matcher/build_description.rb +62 -0
  57. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validation_matcher.rb +195 -0
  58. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validation_message_finder.rb +63 -0
  59. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model/validator.rb +127 -0
  60. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_model.rb +96 -0
  61. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb +217 -0
  62. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matcher.rb +2134 -0
  63. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb +74 -0
  64. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb +62 -0
  65. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/inverse_of_matcher.rb +41 -0
  66. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb +105 -0
  67. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +101 -0
  68. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +89 -0
  69. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +142 -0
  70. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/optional_matcher.rb +71 -0
  71. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb +41 -0
  72. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/required_matcher.rb +76 -0
  73. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb +42 -0
  74. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers/through_matcher.rb +65 -0
  75. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/association_matchers.rb +9 -0
  76. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +784 -0
  77. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/encrypt_matcher.rb +174 -0
  78. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/have_attached_matcher.rb +343 -0
  79. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +366 -0
  80. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +287 -0
  81. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +104 -0
  82. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +95 -0
  83. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/have_rich_text_matcher.rb +83 -0
  84. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/have_secure_token_matcher.rb +132 -0
  85. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/normalize_matcher.rb +151 -0
  86. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/serialize_matcher.rb +203 -0
  87. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/uniqueness/model.rb +58 -0
  88. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/uniqueness/namespace.rb +39 -0
  89. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/uniqueness/test_model_creator.rb +49 -0
  90. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/uniqueness/test_models.rb +55 -0
  91. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/uniqueness.rb +14 -0
  92. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +1197 -0
  93. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/active_record.rb +37 -0
  94. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/configuration.rb +31 -0
  95. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/double.rb +103 -0
  96. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/double_collection.rb +60 -0
  97. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/double_implementation_registry.rb +31 -0
  98. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/method_call.rb +44 -0
  99. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/object_double.rb +43 -0
  100. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/proxy_implementation.rb +32 -0
  101. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/stub_implementation.rb +31 -0
  102. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak/world.rb +63 -0
  103. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/doublespeak.rb +38 -0
  104. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/error.rb +29 -0
  105. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/independent/delegate_method_matcher/target_not_defined_error.rb +15 -0
  106. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/independent/delegate_method_matcher.rb +540 -0
  107. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/independent.rb +11 -0
  108. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/configuration.rb +70 -0
  109. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/configuration_error.rb +9 -0
  110. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/inclusion.rb +20 -0
  111. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/libraries/action_controller.rb +33 -0
  112. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/libraries/active_model.rb +26 -0
  113. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/libraries/active_record.rb +26 -0
  114. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/libraries/missing_library.rb +19 -0
  115. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/libraries/rails.rb +31 -0
  116. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/libraries/routing.rb +29 -0
  117. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/libraries.rb +16 -0
  118. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/rails.rb +12 -0
  119. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/registry.rb +28 -0
  120. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/test_frameworks/active_support_test_case.rb +37 -0
  121. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/test_frameworks/minitest_4.rb +36 -0
  122. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/test_frameworks/minitest_5.rb +37 -0
  123. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/test_frameworks/missing_test_framework.rb +40 -0
  124. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/test_frameworks/rspec.rb +29 -0
  125. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/test_frameworks/test_unit.rb +36 -0
  126. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations/test_frameworks.rb +16 -0
  127. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/integrations.rb +43 -0
  128. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/matcher_collection.rb +99 -0
  129. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/matcher_context.rb +36 -0
  130. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/rails_shim.rb +190 -0
  131. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/routing.rb +10 -0
  132. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/util/word_wrap.rb +211 -0
  133. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/util.rb +110 -0
  134. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/version.rb +6 -0
  135. data/shoulda-matchers-8.0.1/lib/shoulda/matchers/warn.rb +28 -0
  136. data/shoulda-matchers-8.0.1/lib/shoulda/matchers.rb +25 -0
  137. data/shoulda-matchers-8.0.1/lib/shoulda-matchers.rb +1 -0
  138. data/shoulda-matchers-8.0.1/shoulda-matchers.gemspec +43 -0
  139. data/ultra-fast-box.gemspec +12 -0
  140. metadata +179 -0
@@ -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