shoulda-matchers 3.1.3 → 4.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.hound/ruby.yml +336 -316
  3. data/.python-version +1 -0
  4. data/.rubocop.yml +3 -1
  5. data/.travis.yml +7 -6
  6. data/Appraisals +76 -44
  7. data/CONTRIBUTING.md +137 -66
  8. data/Gemfile +5 -5
  9. data/Gemfile.lock +30 -35
  10. data/MAINTAINING.md +250 -0
  11. data/MIT-LICENSE +1 -1
  12. data/NEWS.md +176 -4
  13. data/README.md +138 -200
  14. data/Rakefile +7 -0
  15. data/bin/setup +190 -0
  16. data/doc_config/yard/templates/default/fulldoc/html/css/global.css +4 -0
  17. data/doc_config/yard/templates/default/fulldoc/html/full_list.erb +0 -6
  18. data/doc_config/yard/templates/default/fulldoc/html/js/app.js +0 -17
  19. data/doc_config/yard/templates/default/fulldoc/html/setup.rb +27 -0
  20. data/gemfiles/4.2.gemfile +21 -20
  21. data/gemfiles/4.2.gemfile.lock +143 -140
  22. data/gemfiles/5.0.gemfile +37 -0
  23. data/gemfiles/5.0.gemfile.lock +238 -0
  24. data/gemfiles/5.1.gemfile +38 -0
  25. data/gemfiles/5.1.gemfile.lock +254 -0
  26. data/gemfiles/5.2.gemfile +40 -0
  27. data/gemfiles/5.2.gemfile.lock +273 -0
  28. data/lib/shoulda/matchers/action_controller/callback_matcher.rb +18 -6
  29. data/lib/shoulda/matchers/action_controller/permit_matcher.rb +6 -1
  30. data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +1 -1
  31. data/lib/shoulda/matchers/action_controller/route_matcher.rb +87 -27
  32. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +1 -0
  33. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter.rb +0 -4
  34. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +5 -0
  35. data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +5 -0
  36. data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +26 -11
  37. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +39 -4
  38. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +116 -47
  39. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +127 -38
  40. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +55 -37
  41. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +30 -1
  42. data/lib/shoulda/matchers/active_model/validation_matcher.rb +11 -4
  43. data/lib/shoulda/matchers/active_model/validation_matcher/build_description.rb +11 -6
  44. data/lib/shoulda/matchers/active_record.rb +3 -0
  45. data/lib/shoulda/matchers/active_record/association_matcher.rb +172 -22
  46. data/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb +1 -1
  47. data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +11 -6
  48. data/lib/shoulda/matchers/active_record/association_matchers/optional_matcher.rb +46 -0
  49. data/lib/shoulda/matchers/active_record/association_matchers/required_matcher.rb +51 -0
  50. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +268 -38
  51. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +1 -1
  52. data/lib/shoulda/matchers/active_record/have_secure_token_matcher.rb +111 -0
  53. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +207 -79
  54. data/lib/shoulda/matchers/doublespeak/object_double.rb +5 -1
  55. data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +100 -21
  56. data/lib/shoulda/matchers/rails_shim.rb +133 -52
  57. data/lib/shoulda/matchers/routing.rb +2 -2
  58. data/lib/shoulda/matchers/util.rb +23 -1
  59. data/lib/shoulda/matchers/util/word_wrap.rb +6 -2
  60. data/lib/shoulda/matchers/version.rb +1 -1
  61. data/script/install_gems_in_all_appraisals +3 -1
  62. data/script/run_all_tests +3 -1
  63. data/script/supported_ruby_versions +7 -0
  64. data/script/update_gem_in_all_appraisals +3 -1
  65. data/script/update_gems_in_all_appraisals +3 -1
  66. data/shoulda-matchers.gemspec +3 -3
  67. data/spec/acceptance/independent_matchers_spec.rb +2 -2
  68. data/spec/acceptance/multiple_libraries_integration_spec.rb +1 -1
  69. data/spec/acceptance/rails_integration_spec.rb +2 -2
  70. data/spec/spec_helper.rb +2 -3
  71. data/spec/support/acceptance/helpers.rb +2 -0
  72. data/spec/support/acceptance/helpers/command_helpers.rb +17 -4
  73. data/spec/support/acceptance/helpers/rails_migration_helpers.rb +21 -0
  74. data/spec/support/acceptance/helpers/step_helpers.rb +1 -1
  75. data/spec/support/tests/current_bundle.rb +3 -9
  76. data/spec/support/tests/filesystem.rb +2 -2
  77. data/spec/support/unit/attribute.rb +0 -2
  78. data/spec/support/unit/capture.rb +9 -3
  79. data/spec/support/unit/helpers/action_pack_versions.rb +22 -0
  80. data/spec/support/unit/helpers/active_model_versions.rb +4 -0
  81. data/spec/support/unit/helpers/active_record_versions.rb +22 -2
  82. data/spec/support/unit/helpers/active_resource_builder.rb +2 -2
  83. data/spec/support/unit/helpers/controller_builder.rb +1 -1
  84. data/spec/support/unit/helpers/message_helpers.rb +19 -0
  85. data/spec/support/unit/helpers/rails_versions.rb +14 -0
  86. data/spec/support/unit/matchers/fail_with_message_matcher.rb +7 -5
  87. data/spec/support/unit/matchers/print_warning_including.rb +21 -13
  88. data/spec/support/unit/model_creation_strategies/active_record.rb +1 -1
  89. data/spec/support/unit/model_creators/active_record.rb +0 -1
  90. data/spec/support/unit/model_creators/basic.rb +7 -2
  91. data/spec/support/unit/rails_application.rb +25 -0
  92. data/spec/support/unit/record_validating_confirmation_builder.rb +5 -2
  93. data/spec/support/unit/validation_matcher_scenario.rb +0 -2
  94. data/spec/unit/shoulda/matchers/action_controller/callback_matcher_spec.rb +18 -18
  95. data/spec/unit/shoulda/matchers/action_controller/permit_matcher_spec.rb +33 -5
  96. data/spec/unit/shoulda/matchers/action_controller/render_template_matcher_spec.rb +1 -1
  97. data/spec/unit/shoulda/matchers/active_model/allow_mass_assignment_of_matcher_spec.rb +80 -78
  98. data/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb +7 -9
  99. data/spec/unit/shoulda/matchers/active_model/validate_absence_of_matcher_spec.rb +28 -4
  100. data/spec/unit/shoulda/matchers/active_model/validate_acceptance_of_matcher_spec.rb +19 -1
  101. data/spec/unit/shoulda/matchers/active_model/validate_confirmation_of_matcher_spec.rb +27 -4
  102. data/spec/unit/shoulda/matchers/active_model/validate_exclusion_of_matcher_spec.rb +62 -5
  103. data/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb +52 -18
  104. data/spec/unit/shoulda/matchers/active_model/validate_length_of_matcher_spec.rb +51 -4
  105. data/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +99 -71
  106. data/spec/unit/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +41 -15
  107. data/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb +445 -15
  108. data/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb +615 -93
  109. data/spec/unit/shoulda/matchers/active_record/have_secure_token_matcher_spec.rb +169 -0
  110. data/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb +167 -97
  111. data/spec/unit/shoulda/matchers/doublespeak/world_spec.rb +2 -4
  112. data/spec/unit/shoulda/matchers/independent/delegate_method_matcher_spec.rb +152 -19
  113. data/spec/unit/shoulda/matchers/routing/route_matcher_spec.rb +258 -94
  114. data/spec/unit_spec_helper.rb +9 -1
  115. data/zeus.json +1 -1
  116. metadata +31 -16
  117. data/gemfiles/4.0.0.gemfile +0 -38
  118. data/gemfiles/4.0.0.gemfile.lock +0 -223
  119. data/gemfiles/4.0.1.gemfile +0 -38
  120. data/gemfiles/4.0.1.gemfile.lock +0 -225
  121. data/gemfiles/4.1.gemfile +0 -38
  122. data/gemfiles/4.1.gemfile.lock +0 -220
  123. data/script/SUPPORTED_VERSIONS +0 -1
@@ -0,0 +1,51 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveRecord
4
+ module AssociationMatchers
5
+ # @private
6
+ class RequiredMatcher
7
+ attr_reader :missing_option
8
+
9
+ def initialize(attribute_name, required)
10
+ @required = required
11
+ @submatcher = ActiveModel::DisallowValueMatcher.new(nil).
12
+ for(attribute_name).
13
+ with_message(validation_message_key)
14
+ @missing_option = ''
15
+ end
16
+
17
+ def description
18
+ "required: #{required}"
19
+ end
20
+
21
+ def matches?(subject)
22
+ if submatcher_passes?(subject)
23
+ true
24
+ else
25
+ @missing_option =
26
+ 'the association should have been defined ' +
27
+ "with `required: #{required}`, but was not"
28
+ false
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :required, :submatcher
35
+
36
+ def submatcher_passes?(subject)
37
+ if required
38
+ submatcher.matches?(subject)
39
+ else
40
+ submatcher.does_not_match?(subject)
41
+ end
42
+ end
43
+
44
+ def validation_message_key
45
+ RailsShim.validation_message_key_for_association_required_option
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -20,10 +20,10 @@ module Shoulda
20
20
  #
21
21
  # #### Qualifiers
22
22
  #
23
- # ##### with
23
+ # ##### with_values
24
24
  #
25
- # Use `with` to test that the enum has been defined with a certain set of
26
- # known values.
25
+ # Use `with_values` to test that the attribute has been defined with a
26
+ # certain set of possible values.
27
27
  #
28
28
  # class Process < ActiveRecord::Base
29
29
  # enum status: [:running, :stopped, :suspended]
@@ -33,14 +33,101 @@ module Shoulda
33
33
  # RSpec.describe Process, type: :model do
34
34
  # it do
35
35
  # should define_enum_for(:status).
36
- # with([:running, :stopped, :suspended])
36
+ # with_values([:running, :stopped, :suspended])
37
37
  # end
38
38
  # end
39
39
  #
40
40
  # # Minitest (Shoulda)
41
41
  # class ProcessTest < ActiveSupport::TestCase
42
42
  # should define_enum_for(:status).
43
- # with([:running, :stopped, :suspended])
43
+ # with_values([:running, :stopped, :suspended])
44
+ # end
45
+ #
46
+ # ##### backed_by_column_of_type
47
+ #
48
+ # Use `backed_by_column_of_type` to test that the attribute is of a
49
+ # certain column type. (The default is `:integer`.)
50
+ #
51
+ # class LoanApplication < ActiveRecord::Base
52
+ # enum status: {
53
+ # active: "active",
54
+ # pending: "pending",
55
+ # rejected: "rejected"
56
+ # }
57
+ # end
58
+ #
59
+ # # RSpec
60
+ # RSpec.describe LoanApplication, type: :model do
61
+ # it do
62
+ # should define_enum_for(:status).
63
+ # with_values(
64
+ # active: "active",
65
+ # pending: "pending",
66
+ # rejected: "rejected"
67
+ # ).
68
+ # backed_by_column_of_type(:string)
69
+ # end
70
+ # end
71
+ #
72
+ # # Minitest (Shoulda)
73
+ # class LoanApplicationTest < ActiveSupport::TestCase
74
+ # should define_enum_for(:status).
75
+ # with_values(
76
+ # active: "active",
77
+ # pending: "pending",
78
+ # rejected: "rejected"
79
+ # ).
80
+ # backed_by_column_of_type(:string)
81
+ # end
82
+ #
83
+ ## ##### with_prefix
84
+ #
85
+ # Use `with_prefix` to test that the enum is defined with a `_prefix`
86
+ # option (Rails 5 only). Can take either a boolean or a symbol:
87
+ #
88
+ # class Issue < ActiveRecord::Base
89
+ # enum status: [:open, :closed], _prefix: :old
90
+ # end
91
+ #
92
+ # # RSpec
93
+ # RSpec.describe Issue, type: :model do
94
+ # it do
95
+ # should define_enum_for(:status).
96
+ # with_values([:open, :closed]).
97
+ # with_prefix(:old)
98
+ # end
99
+ # end
100
+ #
101
+ # # Minitest (Shoulda)
102
+ # class ProcessTest < ActiveSupport::TestCase
103
+ # should define_enum_for(:status).
104
+ # with_values([:open, :closed]).
105
+ # with_prefix(:old)
106
+ # end
107
+ #
108
+ # ##### with_suffix
109
+ #
110
+ # Use `with_suffix` to test that the enum is defined with a `_suffix`
111
+ # option (Rails 5 only). Can take either a boolean or a symbol:
112
+ #
113
+ # class Issue < ActiveRecord::Base
114
+ # enum status: [:open, :closed], _suffix: true
115
+ # end
116
+ #
117
+ # # RSpec
118
+ # RSpec.describe Issue, type: :model do
119
+ # it do
120
+ # should define_enum_for(:status).
121
+ # with_values([:open, :closed]).
122
+ # with_suffix
123
+ # end
124
+ # end
125
+ #
126
+ # # Minitest (Shoulda)
127
+ # class ProcessTest < ActiveSupport::TestCase
128
+ # should define_enum_for(:status).
129
+ # with_values([:open, :closed]).
130
+ # with_suffix
44
131
  # end
45
132
  #
46
133
  # @return [DefineEnumForMatcher]
@@ -53,51 +140,132 @@ module Shoulda
53
140
  class DefineEnumForMatcher
54
141
  def initialize(attribute_name)
55
142
  @attribute_name = attribute_name
56
- @options = {}
143
+ @options = { expected_enum_values: [] }
57
144
  end
58
145
 
59
- def with(expected_enum_values)
146
+ def description
147
+ description = "define :#{attribute_name} as an enum, backed by "
148
+ description << Shoulda::Matchers::Util.a_or_an(expected_column_type)
149
+
150
+ if options[:expected_prefix]
151
+ description << ', using a prefix of '
152
+ description << "#{options[:expected_prefix].inspect}"
153
+ end
154
+
155
+ if options[:expected_suffix]
156
+ if options[:expected_prefix]
157
+ description << ' and'
158
+ else
159
+ description << ', using'
160
+ end
161
+
162
+ description << ' a suffix of '
163
+
164
+ description << "#{options[:expected_suffix].inspect}"
165
+ end
166
+
167
+ if presented_expected_enum_values.any?
168
+ description << ', with possible values '
169
+ description << Shoulda::Matchers::Util.inspect_value(
170
+ presented_expected_enum_values,
171
+ )
172
+ end
173
+
174
+ description
175
+ end
176
+
177
+ def with_values(expected_enum_values)
60
178
  options[:expected_enum_values] = expected_enum_values
61
179
  self
62
180
  end
63
181
 
182
+ def with(expected_enum_values)
183
+ Shoulda::Matchers.warn_about_deprecated_method(
184
+ 'The `with` qualifier on `define_enum_for`',
185
+ '`with_values`',
186
+ )
187
+ with_values(expected_enum_values)
188
+ end
189
+
190
+ def with_prefix(expected_prefix = attribute_name)
191
+ options[:expected_prefix] = expected_prefix
192
+ self
193
+ end
194
+
195
+ def with_suffix(expected_suffix = attribute_name)
196
+ options[:expected_suffix] = expected_suffix
197
+ self
198
+ end
199
+
200
+ def backed_by_column_of_type(expected_column_type)
201
+ options[:expected_column_type] = expected_column_type
202
+ self
203
+ end
204
+
64
205
  def matches?(subject)
65
206
  @record = subject
66
- enum_defined? && enum_values_match? && column_type_is_integer?
207
+
208
+ enum_defined? &&
209
+ enum_values_match? &&
210
+ column_type_matches? &&
211
+ enum_value_methods_exist?
67
212
  end
68
213
 
69
214
  def failure_message
70
- "Expected #{expectation}"
215
+ message = "Expected #{model} to #{expectation}"
216
+
217
+ if failure_reason
218
+ message << ". However, #{failure_reason}"
219
+ end
220
+
221
+ message << '.'
222
+
223
+ Shoulda::Matchers.word_wrap(message)
71
224
  end
72
- alias :failure_message_for_should :failure_message
73
225
 
74
226
  def failure_message_when_negated
75
- "Did not expect #{expectation}"
227
+ message = "Expected #{model} not to #{expectation}, but it did."
228
+ Shoulda::Matchers.word_wrap(message)
76
229
  end
77
- alias :failure_message_for_should_not :failure_message_when_negated
78
-
79
- def description
80
- desc = "define :#{attribute_name} as an enum"
81
230
 
82
- if options[:expected_enum_values]
83
- desc << " with #{options[:expected_enum_values]}"
84
- end
231
+ private
85
232
 
86
- desc << " and store the value in a column with an integer type"
233
+ attr_reader :attribute_name, :options, :record, :failure_reason
87
234
 
88
- desc
235
+ def expectation
236
+ description
89
237
  end
90
238
 
91
- protected
239
+ def presented_expected_enum_values
240
+ if expected_enum_values.is_a?(Hash)
241
+ expected_enum_values.symbolize_keys
242
+ else
243
+ expected_enum_values
244
+ end
245
+ end
92
246
 
93
- attr_reader :record, :attribute_name, :options
247
+ def normalized_expected_enum_values
248
+ to_hash(expected_enum_values)
249
+ end
94
250
 
95
- def expectation
96
- "#{model.name} to #{description}"
251
+ def expected_enum_value_names
252
+ to_array(expected_enum_values)
97
253
  end
98
254
 
99
255
  def expected_enum_values
100
- hashify(options[:expected_enum_values]).with_indifferent_access
256
+ options[:expected_enum_values]
257
+ end
258
+
259
+ def presented_actual_enum_values
260
+ if expected_enum_values.is_a?(Array)
261
+ to_array(actual_enum_values)
262
+ else
263
+ to_hash(actual_enum_values).symbolize_keys
264
+ end
265
+ end
266
+
267
+ def normalized_actual_enum_values
268
+ to_hash(actual_enum_values)
101
269
  end
102
270
 
103
271
  def actual_enum_values
@@ -105,15 +273,45 @@ module Shoulda
105
273
  end
106
274
 
107
275
  def enum_defined?
108
- model.defined_enums.include?(attribute_name.to_s)
276
+ if model.defined_enums.include?(attribute_name.to_s)
277
+ true
278
+ else
279
+ @failure_reason = "no such enum exists in #{model}"
280
+ false
281
+ end
109
282
  end
110
283
 
111
284
  def enum_values_match?
112
- expected_enum_values.empty? || actual_enum_values == expected_enum_values
285
+ passed =
286
+ expected_enum_values.empty? ||
287
+ normalized_actual_enum_values == normalized_expected_enum_values
288
+
289
+ if passed
290
+ true
291
+ else
292
+ @failure_reason =
293
+ "the actual enum values for #{attribute_name.inspect} are " +
294
+ Shoulda::Matchers::Util.inspect_value(
295
+ presented_actual_enum_values,
296
+ )
297
+ false
298
+ end
113
299
  end
114
300
 
115
- def column_type_is_integer?
116
- column.type == :integer
301
+ def column_type_matches?
302
+ if column.type == expected_column_type.to_sym
303
+ true
304
+ else
305
+ @failure_reason =
306
+ "#{attribute_name.inspect} is " +
307
+ Shoulda::Matchers::Util.a_or_an(column.type) +
308
+ ' column'
309
+ false
310
+ end
311
+ end
312
+
313
+ def expected_column_type
314
+ options[:expected_column_type] || :integer
117
315
  end
118
316
 
119
317
  def column
@@ -124,21 +322,53 @@ module Shoulda
124
322
  record.class
125
323
  end
126
324
 
127
- def hashify(value)
128
- if value.nil?
129
- return {}
325
+ def enum_value_methods_exist?
326
+ passed = expected_singleton_methods.all? do |method|
327
+ model.singleton_methods.include?(method)
130
328
  end
131
329
 
132
- if value.is_a?(Array)
133
- new_value = {}
330
+ if passed
331
+ true
332
+ else
333
+ @failure_reason =
334
+ if options[:expected_prefix]
335
+ if options[:expected_suffix]
336
+ 'it was defined with either a different prefix, a ' +
337
+ 'different suffix, or neither one at all'
338
+ else
339
+ 'it was defined with either a different prefix or none at all'
340
+ end
341
+ elsif options[:expected_suffix]
342
+ 'it was defined with either a different suffix or none at all'
343
+ end
344
+ false
345
+ end
346
+ end
134
347
 
135
- value.each_with_index do |v, i|
136
- new_value[v] = i
348
+ def expected_singleton_methods
349
+ expected_enum_value_names.map do |name|
350
+ [options[:expected_prefix], name, options[:expected_suffix]].
351
+ select(&:present?).
352
+ join('_').
353
+ to_sym
354
+ end
355
+ end
356
+
357
+ def to_hash(value)
358
+ if value.is_a?(Array)
359
+ value.each_with_index.inject({}) do |hash, (item, index)|
360
+ hash.merge(item.to_s => index)
137
361
  end
362
+ else
363
+ value.stringify_keys
364
+ end
365
+ end
138
366
 
139
- new_value
367
+ def to_array(value)
368
+ if value.is_a?(Array)
369
+ value.map(&:to_s)
140
370
  else
141
- value
371
+ value.keys.map(&:to_s)
142
372
  end
143
373
  end
144
374
  end
@@ -50,7 +50,7 @@ module Shoulda
50
50
  # should have_db_index(:name).unique(true)
51
51
  # end
52
52
  #
53
- # Since it only ever makes since for `unique` to be `true`, you can also
53
+ # Since it only ever makes sense for `unique` to be `true`, you can also
54
54
  # leave off the argument to save some keystrokes:
55
55
  #
56
56
  # # RSpec
@@ -0,0 +1,111 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveRecord
4
+ # The `have_secure_token` matcher tests usage of the
5
+ # `has_secure_token` macro.
6
+ #
7
+ # #### Example
8
+ #
9
+ # class User < ActiveRecord
10
+ # attr_accessor :token
11
+ # attr_accessor :auth_token
12
+ #
13
+ # has_secure_token
14
+ # has_secure_token :auth_token
15
+ # end
16
+ #
17
+ # # RSpec
18
+ # RSpec.describe User, type: :model do
19
+ # it { should have_secure_token }
20
+ # it { should have_secure_token(:auth_token) }
21
+ # end
22
+ #
23
+ # # Minitest (Shoulda)
24
+ # class UserTest < ActiveSupport::TestCase
25
+ # should have_secure_token
26
+ # should have_secure_token(:auth_token)
27
+ # end
28
+ #
29
+ # @return [HaveSecureToken]
30
+ #
31
+
32
+ # rubocop:disable Style/PredicateName
33
+ def have_secure_token(token_attribute = :token)
34
+ HaveSecureTokenMatcher.new(token_attribute)
35
+ end
36
+ # rubocop:enable Style/PredicateName
37
+
38
+ # @private
39
+ class HaveSecureTokenMatcher
40
+ attr_reader :token_attribute
41
+
42
+ def initialize(token_attribute)
43
+ @token_attribute = token_attribute
44
+ end
45
+
46
+ def description
47
+ "have :#{token_attribute} as a secure token"
48
+ end
49
+
50
+ def failure_message
51
+ return if !@errors
52
+ "Expected #{@subject.class} to #{description} but the following " \
53
+ "errors were found: #{@errors.join(', ')}"
54
+ end
55
+
56
+ def failure_message_when_negated
57
+ return if !@errors
58
+ "Did not expect #{@subject.class} to have secure token " \
59
+ ":#{token_attribute}"
60
+ end
61
+
62
+ def matches?(subject)
63
+ @subject = subject
64
+ @errors = run_checks
65
+ @errors.empty?
66
+ end
67
+
68
+ private
69
+
70
+ def run_checks
71
+ @errors = []
72
+ if !has_expected_instance_methods?
73
+ @errors << 'missing expected class and instance methods'
74
+ end
75
+ if !has_expected_db_column?
76
+ @errors << "missing correct column #{token_attribute}:string"
77
+ end
78
+ if !has_expected_db_index?
79
+ @errors << "missing unique index for #{table_and_column}"
80
+ end
81
+ @errors
82
+ end
83
+
84
+ def has_expected_instance_methods?
85
+ @subject.respond_to?(token_attribute.to_s) &&
86
+ @subject.respond_to?("#{token_attribute}=") &&
87
+ @subject.respond_to?("regenerate_#{token_attribute}") &&
88
+ @subject.class.respond_to?(:generate_unique_secure_token)
89
+ end
90
+
91
+ def has_expected_db_column?
92
+ matcher = HaveDbColumnMatcher.new(token_attribute).of_type(:string)
93
+ matcher.matches?(@subject)
94
+ end
95
+
96
+ def has_expected_db_index?
97
+ matcher = HaveDbIndexMatcher.new(token_attribute).unique(true)
98
+ matcher.matches?(@subject)
99
+ end
100
+
101
+ def table_and_column
102
+ "#{table_name}.#{token_attribute}"
103
+ end
104
+
105
+ def table_name
106
+ @subject.class.table_name
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end