shoulda-matchers 5.1.0 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +41 -18
- data/lib/shoulda/matchers/action_controller/permit_matcher.rb +7 -9
- data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +1 -1
- data/lib/shoulda/matchers/action_controller/set_session_or_flash_matcher.rb +13 -15
- data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +46 -1
- data/lib/shoulda/matchers/active_model/comparison_matcher.rb +157 -0
- data/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +7 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +3 -5
- data/lib/shoulda/matchers/active_model/numericality_matchers/range_matcher.rb +71 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb +53 -0
- data/lib/shoulda/matchers/active_model/qualifiers/allow_blank.rb +26 -0
- data/lib/shoulda/matchers/active_model/qualifiers.rb +1 -0
- data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +2 -7
- data/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +532 -0
- data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +5 -5
- data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +29 -14
- data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +65 -10
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +76 -86
- data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +29 -4
- data/lib/shoulda/matchers/active_model/validation_matcher/build_description.rb +6 -7
- data/lib/shoulda/matchers/active_model/validation_matcher.rb +6 -0
- data/lib/shoulda/matchers/active_model/validator.rb +4 -0
- data/lib/shoulda/matchers/active_model.rb +4 -1
- data/lib/shoulda/matchers/active_record/association_matcher.rb +543 -15
- data/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb +34 -4
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +9 -1
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +1 -0
- data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +4 -0
- data/lib/shoulda/matchers/active_record/association_matchers/optional_matcher.rb +23 -19
- data/lib/shoulda/matchers/active_record/association_matchers/required_matcher.rb +27 -23
- data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +338 -30
- data/lib/shoulda/matchers/active_record/encrypt_matcher.rb +174 -0
- data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +46 -6
- data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +24 -13
- data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +3 -5
- data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +1 -1
- data/lib/shoulda/matchers/active_record/normalize_matcher.rb +151 -0
- data/lib/shoulda/matchers/active_record/uniqueness/model.rb +13 -1
- data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +82 -70
- data/lib/shoulda/matchers/active_record.rb +2 -0
- data/lib/shoulda/matchers/doublespeak/double_collection.rb +2 -6
- data/lib/shoulda/matchers/doublespeak/world.rb +2 -6
- data/lib/shoulda/matchers/doublespeak.rb +0 -1
- data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +13 -15
- data/lib/shoulda/matchers/integrations/libraries/action_controller.rb +7 -5
- data/lib/shoulda/matchers/integrations/libraries/routing.rb +5 -3
- data/lib/shoulda/matchers/rails_shim.rb +22 -6
- data/lib/shoulda/matchers/util/word_wrap.rb +2 -2
- data/lib/shoulda/matchers/util.rb +18 -20
- data/lib/shoulda/matchers/version.rb +1 -1
- data/lib/shoulda/matchers.rb +2 -2
- data/shoulda-matchers.gemspec +2 -2
- metadata +15 -9
- data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +0 -157
@@ -116,7 +116,7 @@ module Shoulda
|
|
116
116
|
## ##### with_prefix
|
117
117
|
#
|
118
118
|
# Use `with_prefix` to test that the enum is defined with a `_prefix`
|
119
|
-
# option (Rails
|
119
|
+
# option (Rails 6+ only). Can take either a boolean or a symbol:
|
120
120
|
#
|
121
121
|
# class Issue < ActiveRecord::Base
|
122
122
|
# enum status: [:open, :closed], _prefix: :old
|
@@ -163,8 +163,119 @@ module Shoulda
|
|
163
163
|
# with_suffix
|
164
164
|
# end
|
165
165
|
#
|
166
|
-
#
|
166
|
+
# ##### without_scopes
|
167
|
+
#
|
168
|
+
# Use `without_scopes` to test that the enum is defined with
|
169
|
+
# '_scopes: false' option (Rails 5 only). Can take either a boolean or a
|
170
|
+
# symbol:
|
171
|
+
#
|
172
|
+
# class Issue < ActiveRecord::Base
|
173
|
+
# enum status: [:open, :closed], _scopes: false
|
174
|
+
# end
|
175
|
+
#
|
176
|
+
# # RSpec
|
177
|
+
# RSpec.describe Issue, type: :model do
|
178
|
+
# it do
|
179
|
+
# should define_enum_for(:status).
|
180
|
+
# without_scopes
|
181
|
+
# end
|
182
|
+
# end
|
183
|
+
#
|
184
|
+
# # Minitest (Shoulda)
|
185
|
+
# class ProcessTest < ActiveSupport::TestCase
|
186
|
+
# should define_enum_for(:status).
|
187
|
+
# without_scopes
|
188
|
+
# end
|
189
|
+
#
|
190
|
+
# ##### with_default
|
191
|
+
#
|
192
|
+
# Use `with_default` to test that the enum is defined with a
|
193
|
+
# default value. A proc can also be passed, and will be called once each
|
194
|
+
# time a new value is needed. (If using Time or Date, it's recommended to
|
195
|
+
# freeze time or date to avoid flaky tests):
|
196
|
+
#
|
197
|
+
# class Issue < ActiveRecord::Base
|
198
|
+
# enum status: [:open, :closed], default: :closed
|
199
|
+
# end
|
200
|
+
#
|
201
|
+
# # RSpec
|
202
|
+
# RSpec.describe Issue, type: :model do
|
203
|
+
# it do
|
204
|
+
# should define_enum_for(:status).
|
205
|
+
# with_default(:closed)
|
206
|
+
# end
|
207
|
+
# end
|
208
|
+
#
|
209
|
+
# # Minitest (Shoulda)
|
210
|
+
# class ProcessTest < ActiveSupport::TestCase
|
211
|
+
# should define_enum_for(:status).
|
212
|
+
# with_default(:closed)
|
213
|
+
# end
|
214
|
+
#
|
215
|
+
# ##### validating
|
216
|
+
#
|
217
|
+
# Use `validating` to test that the enum is being validated.
|
218
|
+
# Can take a boolean value and an allowing_nil keyword argument:
|
219
|
+
#
|
220
|
+
# class Issue < ActiveRecord::Base
|
221
|
+
# enum status: [:open, :closed], validate: true
|
222
|
+
# end
|
223
|
+
#
|
224
|
+
# # RSpec
|
225
|
+
# RSpec.describe Issue, type: :model do
|
226
|
+
# it do
|
227
|
+
# should define_enum_for(:status).
|
228
|
+
# validating
|
229
|
+
# end
|
230
|
+
# end
|
231
|
+
#
|
232
|
+
# # Minitest (Shoulda)
|
233
|
+
# class ProcessTest < ActiveSupport::TestCase
|
234
|
+
# should define_enum_for(:status).
|
235
|
+
# validating
|
236
|
+
# end
|
237
|
+
#
|
238
|
+
# class Issue < ActiveRecord::Base
|
239
|
+
# enum status: [:open, :closed], validate: { allow_nil: true }
|
240
|
+
# end
|
241
|
+
#
|
242
|
+
# # RSpec
|
243
|
+
# RSpec.describe Issue, type: :model do
|
244
|
+
# it do
|
245
|
+
# should define_enum_for(:status).
|
246
|
+
# validating(allowing_nil: true)
|
247
|
+
# end
|
248
|
+
# end
|
167
249
|
#
|
250
|
+
# # Minitest (Shoulda)
|
251
|
+
# class ProcessTest < ActiveSupport::TestCase
|
252
|
+
# should define_enum_for(:status).
|
253
|
+
# validating(allowing_nil: true)
|
254
|
+
# end
|
255
|
+
#
|
256
|
+
# ##### without_instance_methods
|
257
|
+
#
|
258
|
+
# Use `without_instance_methods` to exclude the check for instance methods.
|
259
|
+
#
|
260
|
+
# class Issue < ActiveRecord::Base
|
261
|
+
# enum status: [:open, :closed], instance_methods: false
|
262
|
+
# end
|
263
|
+
#
|
264
|
+
# # RSpec
|
265
|
+
# RSpec.describe Issue, type: :model do
|
266
|
+
# it do
|
267
|
+
# should define_enum_for(:status).
|
268
|
+
# without_instance_methods
|
269
|
+
# end
|
270
|
+
# end
|
271
|
+
#
|
272
|
+
# # Minitest (Shoulda)
|
273
|
+
# class ProcessTest < ActiveSupport::TestCase
|
274
|
+
# should define_enum_for(:status).
|
275
|
+
# without_instance_methods
|
276
|
+
# end
|
277
|
+
#
|
278
|
+
# @return [DefineEnumForMatcher]
|
168
279
|
def define_enum_for(attribute_name)
|
169
280
|
DefineEnumForMatcher.new(attribute_name)
|
170
281
|
end
|
@@ -173,7 +284,7 @@ module Shoulda
|
|
173
284
|
class DefineEnumForMatcher
|
174
285
|
def initialize(attribute_name)
|
175
286
|
@attribute_name = attribute_name
|
176
|
-
@options = { expected_enum_values: [] }
|
287
|
+
@options = { expected_enum_values: [], scopes: true, instance_methods: true }
|
177
288
|
end
|
178
289
|
|
179
290
|
def description
|
@@ -198,17 +309,15 @@ module Shoulda
|
|
198
309
|
description
|
199
310
|
end
|
200
311
|
|
201
|
-
def
|
202
|
-
options[:
|
312
|
+
def validating(value = true, allowing_nil: false)
|
313
|
+
options[:validating] = value
|
314
|
+
options[:allowing_nil] = allowing_nil
|
203
315
|
self
|
204
316
|
end
|
205
317
|
|
206
|
-
def
|
207
|
-
|
208
|
-
|
209
|
-
'`with_values`',
|
210
|
-
)
|
211
|
-
with_values(expected_enum_values)
|
318
|
+
def with_values(expected_enum_values)
|
319
|
+
options[:expected_enum_values] = expected_enum_values
|
320
|
+
self
|
212
321
|
end
|
213
322
|
|
214
323
|
def with_prefix(expected_prefix = true)
|
@@ -226,13 +335,31 @@ module Shoulda
|
|
226
335
|
self
|
227
336
|
end
|
228
337
|
|
338
|
+
def without_scopes
|
339
|
+
options[:scopes] = false
|
340
|
+
self
|
341
|
+
end
|
342
|
+
|
343
|
+
def without_instance_methods
|
344
|
+
options[:instance_methods] = false
|
345
|
+
self
|
346
|
+
end
|
347
|
+
|
348
|
+
def with_default(default_value)
|
349
|
+
options[:default] = default_value
|
350
|
+
self
|
351
|
+
end
|
352
|
+
|
229
353
|
def matches?(subject)
|
230
354
|
@record = subject
|
231
355
|
|
232
356
|
enum_defined? &&
|
233
357
|
enum_values_match? &&
|
234
358
|
column_type_matches? &&
|
235
|
-
enum_value_methods_exist?
|
359
|
+
enum_value_methods_exist? &&
|
360
|
+
scope_presence_matches? &&
|
361
|
+
default_value_matches? &&
|
362
|
+
validating_matches?
|
236
363
|
end
|
237
364
|
|
238
365
|
def failure_message
|
@@ -255,6 +382,30 @@ module Shoulda
|
|
255
382
|
|
256
383
|
private
|
257
384
|
|
385
|
+
def validating_matches?
|
386
|
+
return true if options[:validating].nil?
|
387
|
+
|
388
|
+
validator = find_enum_validator
|
389
|
+
|
390
|
+
if expected_validating? == !!validator
|
391
|
+
if validator&.options&.dig(:allow_nil).present? == expected_allowing_nil?
|
392
|
+
true
|
393
|
+
else
|
394
|
+
@failure_message_continuation =
|
395
|
+
"However, #{attribute_name.inspect} is allowing nil values"
|
396
|
+
false
|
397
|
+
end
|
398
|
+
else
|
399
|
+
@failure_message_continuation =
|
400
|
+
if expected_validating?
|
401
|
+
"However, #{attribute_name.inspect} is not being validated"
|
402
|
+
else
|
403
|
+
"However, #{attribute_name.inspect} is being validated"
|
404
|
+
end
|
405
|
+
false
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
258
409
|
attr_reader :attribute_name, :options, :record,
|
259
410
|
:failure_message_continuation
|
260
411
|
|
@@ -270,6 +421,21 @@ module Shoulda
|
|
270
421
|
)
|
271
422
|
end
|
272
423
|
|
424
|
+
if options[:default].present?
|
425
|
+
expectation << ', with a default value of '
|
426
|
+
expectation << Shoulda::Matchers::Util.inspect_value(expected_default_value)
|
427
|
+
end
|
428
|
+
|
429
|
+
if expected_validating?
|
430
|
+
expectation << ', and being validated '
|
431
|
+
expectation <<
|
432
|
+
if expected_allowing_nil?
|
433
|
+
'allowing nil values'
|
434
|
+
else
|
435
|
+
'not allowing nil values'
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
273
439
|
if expected_prefix
|
274
440
|
expectation <<
|
275
441
|
if expected_suffix
|
@@ -294,6 +460,10 @@ module Shoulda
|
|
294
460
|
expectation << "_#{expected_suffix}".inspect
|
295
461
|
end
|
296
462
|
|
463
|
+
if exclude_scopes?
|
464
|
+
expectation << ' with no scopes'
|
465
|
+
end
|
466
|
+
|
297
467
|
expectation
|
298
468
|
else
|
299
469
|
simple_description
|
@@ -387,37 +557,139 @@ module Shoulda
|
|
387
557
|
end
|
388
558
|
|
389
559
|
def enum_value_methods_exist?
|
390
|
-
|
391
|
-
|
560
|
+
if options[:instance_methods]
|
561
|
+
return true if instance_methods_exist?
|
562
|
+
|
563
|
+
message = missing_methods_message
|
564
|
+
message << " (we can't tell which)" if [expected_prefix, expected_suffix].any?
|
565
|
+
|
566
|
+
@failure_message_continuation = message
|
567
|
+
|
568
|
+
false
|
569
|
+
elsif instance_methods_exist?
|
570
|
+
message = "#{attribute_name.inspect} does map to these values"
|
571
|
+
message << ' with instance methods, but expected no instance methods'
|
572
|
+
|
573
|
+
@failure_message_continuation = message
|
574
|
+
|
575
|
+
false
|
576
|
+
else
|
577
|
+
true
|
392
578
|
end
|
579
|
+
end
|
393
580
|
|
394
|
-
|
581
|
+
def scope_presence_matches?
|
582
|
+
if exclude_scopes?
|
583
|
+
if singleton_methods_exist?
|
584
|
+
message = "#{attribute_name.inspect} does map to these values "
|
585
|
+
message << 'but class scope methods were present'
|
586
|
+
|
587
|
+
@failure_message_continuation = message
|
588
|
+
|
589
|
+
false
|
590
|
+
else
|
591
|
+
true
|
592
|
+
end
|
593
|
+
elsif singleton_methods_exist?
|
395
594
|
true
|
396
595
|
else
|
397
|
-
|
398
|
-
|
596
|
+
if enum_defined?
|
597
|
+
message = 'But the class scope methods are not present'
|
598
|
+
else
|
599
|
+
message = missing_methods_message
|
399
600
|
|
400
|
-
|
401
|
-
|
402
|
-
message << 'configured with either a different prefix or '
|
403
|
-
message << 'suffix, or no prefix or suffix at all'
|
404
|
-
else
|
405
|
-
message << 'configured with either a different prefix or no '
|
406
|
-
message << 'prefix at all'
|
407
|
-
end
|
408
|
-
elsif expected_suffix
|
409
|
-
message << 'configured with either a different suffix or no '
|
410
|
-
message << 'suffix at all'
|
601
|
+
message << 'or the class scope methods are not present'
|
602
|
+
message << " (we can't tell which)"
|
411
603
|
end
|
412
604
|
|
413
|
-
message << " (we can't tell which)"
|
414
|
-
|
415
605
|
@failure_message_continuation = message
|
416
606
|
|
417
607
|
false
|
418
608
|
end
|
419
609
|
end
|
420
610
|
|
611
|
+
def missing_methods_message
|
612
|
+
message = "#{attribute_name.inspect} does map to these "
|
613
|
+
message << 'values, but the enum is '
|
614
|
+
|
615
|
+
if expected_prefix
|
616
|
+
if expected_suffix
|
617
|
+
message << 'configured with either a different prefix or '
|
618
|
+
message << 'suffix, or no prefix or suffix at all'
|
619
|
+
else
|
620
|
+
message << 'configured with either a different prefix or no '
|
621
|
+
message << 'prefix at all'
|
622
|
+
end
|
623
|
+
elsif expected_suffix
|
624
|
+
message << 'configured with either a different suffix or no '
|
625
|
+
message << 'suffix at all'
|
626
|
+
elsif expected_instance_methods?
|
627
|
+
message << 'configured with no instance methods'
|
628
|
+
else
|
629
|
+
''
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
def default_value_matches?
|
634
|
+
return true if options[:default].blank?
|
635
|
+
|
636
|
+
if actual_default_value.nil?
|
637
|
+
@failure_message_continuation = 'However, no default value was set'
|
638
|
+
return false
|
639
|
+
end
|
640
|
+
|
641
|
+
if actual_default_value == expected_default_value
|
642
|
+
true
|
643
|
+
else
|
644
|
+
String.new.tap do |message|
|
645
|
+
message << 'However, the default value is '
|
646
|
+
message << Shoulda::Matchers::Util.inspect_value(actual_default_value)
|
647
|
+
@failure_message_continuation = message
|
648
|
+
end
|
649
|
+
false
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
def expected_default_value
|
654
|
+
options[:default].respond_to?(:call) ? options[:default].call : options[:default]
|
655
|
+
end
|
656
|
+
|
657
|
+
def actual_default_value
|
658
|
+
attribute_schema = if model.respond_to?(:_default_attributes)
|
659
|
+
model._default_attributes[attribute_name.to_s]
|
660
|
+
else
|
661
|
+
model.attributes_to_define_after_schema_loads[attribute_name.to_s]
|
662
|
+
end
|
663
|
+
|
664
|
+
if Kernel.const_defined?('ActiveModel::Attribute::UserProvidedDefault') &&
|
665
|
+
attribute_schema.is_a?(::ActiveModel::Attribute::UserProvidedDefault)
|
666
|
+
attribute_schema = attribute_schema.marshal_dump
|
667
|
+
end
|
668
|
+
|
669
|
+
value = case attribute_schema
|
670
|
+
in [_, { default: default_value } ]
|
671
|
+
default_value
|
672
|
+
in [_, default_value, *]
|
673
|
+
default_value
|
674
|
+
in [_, default_value]
|
675
|
+
default_value
|
676
|
+
end
|
677
|
+
|
678
|
+
value.respond_to?(:call) ? value.call : value
|
679
|
+
end
|
680
|
+
|
681
|
+
def singleton_methods_exist?
|
682
|
+
expected_singleton_methods.all? do |method|
|
683
|
+
model.singleton_methods.include?(method)
|
684
|
+
end
|
685
|
+
end
|
686
|
+
|
687
|
+
def instance_methods_exist?
|
688
|
+
expected_instance_methods.all? do |method|
|
689
|
+
record.methods.include?(method)
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
421
693
|
def expected_singleton_methods
|
422
694
|
expected_enum_value_names.map do |name|
|
423
695
|
[expected_prefix, name, expected_suffix].
|
@@ -427,6 +699,22 @@ module Shoulda
|
|
427
699
|
end
|
428
700
|
end
|
429
701
|
|
702
|
+
def expected_instance_methods
|
703
|
+
methods = expected_enum_value_names.map do |name|
|
704
|
+
[expected_prefix, name, expected_suffix].
|
705
|
+
select(&:present?).
|
706
|
+
join('_')
|
707
|
+
end
|
708
|
+
|
709
|
+
methods.flat_map do |m|
|
710
|
+
["#{m}?".to_sym, "#{m}!".to_sym]
|
711
|
+
end
|
712
|
+
end
|
713
|
+
|
714
|
+
def expected_instance_methods?
|
715
|
+
options[:instance_methods]
|
716
|
+
end
|
717
|
+
|
430
718
|
def expected_prefix
|
431
719
|
if options.include?(:prefix)
|
432
720
|
if options[:prefix] == true
|
@@ -447,6 +735,26 @@ module Shoulda
|
|
447
735
|
end
|
448
736
|
end
|
449
737
|
|
738
|
+
def expected_validating?
|
739
|
+
options[:validating].present?
|
740
|
+
end
|
741
|
+
|
742
|
+
def expected_allowing_nil?
|
743
|
+
options[:allowing_nil].present?
|
744
|
+
end
|
745
|
+
|
746
|
+
def find_enum_validator
|
747
|
+
record.class.validators.detect do |validator|
|
748
|
+
validator.kind == :inclusion &&
|
749
|
+
validator.attributes.include?(attribute_name.to_s) &&
|
750
|
+
validator.options[:in] == expected_enum_value_names
|
751
|
+
end
|
752
|
+
end
|
753
|
+
|
754
|
+
def exclude_scopes?
|
755
|
+
!options[:scopes]
|
756
|
+
end
|
757
|
+
|
450
758
|
def to_hash(value)
|
451
759
|
if value.is_a?(Array)
|
452
760
|
value.each_with_index.inject({}) do |hash, (item, index)|
|
@@ -0,0 +1,174 @@
|
|
1
|
+
module Shoulda
|
2
|
+
module Matchers
|
3
|
+
module ActiveRecord
|
4
|
+
# The `encrypt` matcher tests usage of the
|
5
|
+
# `encrypts` macro (Rails 7+ only).
|
6
|
+
#
|
7
|
+
# class Survey < ActiveRecord::Base
|
8
|
+
# encrypts :access_code
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# # RSpec
|
12
|
+
# RSpec.describe Survey, type: :model do
|
13
|
+
# it { should encrypt(:access_code) }
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# # Minitest (Shoulda)
|
17
|
+
# class SurveyTest < ActiveSupport::TestCase
|
18
|
+
# should encrypt(:access_code)
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# #### Qualifiers
|
22
|
+
#
|
23
|
+
# ##### deterministic
|
24
|
+
#
|
25
|
+
# class Survey < ActiveRecord::Base
|
26
|
+
# encrypts :access_code, deterministic: true
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# # RSpec
|
30
|
+
# RSpec.describe Survey, type: :model do
|
31
|
+
# it { should encrypt(:access_code).deterministic(true) }
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# # Minitest (Shoulda)
|
35
|
+
# class SurveyTest < ActiveSupport::TestCase
|
36
|
+
# should encrypt(:access_code).deterministic(true)
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# ##### downcase
|
40
|
+
#
|
41
|
+
# class Survey < ActiveRecord::Base
|
42
|
+
# encrypts :access_code, downcase: true
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# # RSpec
|
46
|
+
# RSpec.describe Survey, type: :model do
|
47
|
+
# it { should encrypt(:access_code).downcase(true) }
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# # Minitest (Shoulda)
|
51
|
+
# class SurveyTest < ActiveSupport::TestCase
|
52
|
+
# should encrypt(:access_code).downcase(true)
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# ##### ignore_case
|
56
|
+
#
|
57
|
+
# class Survey < ActiveRecord::Base
|
58
|
+
# encrypts :access_code, deterministic: true, ignore_case: true
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# # RSpec
|
62
|
+
# RSpec.describe Survey, type: :model do
|
63
|
+
# it { should encrypt(:access_code).ignore_case(true) }
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# # Minitest (Shoulda)
|
67
|
+
# class SurveyTest < ActiveSupport::TestCase
|
68
|
+
# should encrypt(:access_code).ignore_case(true)
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# @return [EncryptMatcher]
|
72
|
+
#
|
73
|
+
def encrypt(value)
|
74
|
+
EncryptMatcher.new(value)
|
75
|
+
end
|
76
|
+
|
77
|
+
# @private
|
78
|
+
class EncryptMatcher
|
79
|
+
def initialize(attribute)
|
80
|
+
@attribute = attribute.to_sym
|
81
|
+
@options = {}
|
82
|
+
end
|
83
|
+
|
84
|
+
attr_reader :failure_message, :failure_message_when_negated
|
85
|
+
|
86
|
+
def deterministic(deterministic)
|
87
|
+
with_option(:deterministic, deterministic)
|
88
|
+
end
|
89
|
+
|
90
|
+
def downcase(downcase)
|
91
|
+
with_option(:downcase, downcase)
|
92
|
+
end
|
93
|
+
|
94
|
+
def ignore_case(ignore_case)
|
95
|
+
with_option(:ignore_case, ignore_case)
|
96
|
+
end
|
97
|
+
|
98
|
+
def matches?(subject)
|
99
|
+
@subject = subject
|
100
|
+
result = encrypted_attributes_included? &&
|
101
|
+
options_correct?(
|
102
|
+
:deterministic,
|
103
|
+
:downcase,
|
104
|
+
:ignore_case,
|
105
|
+
)
|
106
|
+
|
107
|
+
if result
|
108
|
+
@failure_message_when_negated = "Did not expect to #{description} of #{class_name}"
|
109
|
+
if @options.present?
|
110
|
+
@failure_message_when_negated += "
|
111
|
+
using "
|
112
|
+
@failure_message_when_negated += @options.map { |opt, expected|
|
113
|
+
":#{opt} option as ‹#{expected}›"
|
114
|
+
}.join(' and
|
115
|
+
')
|
116
|
+
end
|
117
|
+
|
118
|
+
@failure_message_when_negated += ",
|
119
|
+
but it did"
|
120
|
+
end
|
121
|
+
|
122
|
+
result
|
123
|
+
end
|
124
|
+
|
125
|
+
def description
|
126
|
+
"encrypt :#{@attribute}"
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def encrypted_attributes_included?
|
132
|
+
if encrypted_attributes.include?(@attribute)
|
133
|
+
true
|
134
|
+
else
|
135
|
+
@failure_message = "Expected to #{description} of #{class_name}, but it did not"
|
136
|
+
false
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def with_option(option_name, value)
|
141
|
+
@options[option_name] = value
|
142
|
+
self
|
143
|
+
end
|
144
|
+
|
145
|
+
def options_correct?(*opts)
|
146
|
+
opts.all? do |opt|
|
147
|
+
next true unless @options.key?(opt)
|
148
|
+
|
149
|
+
expected = @options[opt]
|
150
|
+
actual = encrypted_attribute_scheme.send("#{opt}?")
|
151
|
+
next true if expected == actual
|
152
|
+
|
153
|
+
@failure_message = "Expected to #{description} of #{class_name} using :#{opt} option
|
154
|
+
as ‹#{expected}›, but got ‹#{actual}›"
|
155
|
+
|
156
|
+
false
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def encrypted_attributes
|
161
|
+
@_encrypted_attributes ||= @subject.class.encrypted_attributes || []
|
162
|
+
end
|
163
|
+
|
164
|
+
def encrypted_attribute_scheme
|
165
|
+
@subject.class.type_for_attribute(@attribute).scheme
|
166
|
+
end
|
167
|
+
|
168
|
+
def class_name
|
169
|
+
@subject.class.name
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|