shoulda-matchers 6.0.0 → 6.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -9
  3. data/lib/shoulda/matchers/action_controller/permit_matcher.rb +6 -8
  4. data/lib/shoulda/matchers/action_controller/set_session_or_flash_matcher.rb +13 -15
  5. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +37 -1
  6. data/lib/shoulda/matchers/active_model/comparison_matcher.rb +1 -6
  7. data/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +7 -0
  8. data/lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb +0 -5
  9. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +0 -6
  10. data/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +11 -13
  11. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +20 -8
  12. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +9 -11
  13. data/lib/shoulda/matchers/active_model/validation_matcher/build_description.rb +6 -7
  14. data/lib/shoulda/matchers/active_record/association_matcher.rb +543 -15
  15. data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +9 -1
  16. data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +1 -0
  17. data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +4 -0
  18. data/lib/shoulda/matchers/active_record/association_matchers/optional_matcher.rb +23 -19
  19. data/lib/shoulda/matchers/active_record/association_matchers/required_matcher.rb +27 -23
  20. data/lib/shoulda/matchers/active_record/encrypt_matcher.rb +174 -0
  21. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +10 -10
  22. data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +1 -1
  23. data/lib/shoulda/matchers/active_record/normalize_matcher.rb +1 -1
  24. data/lib/shoulda/matchers/active_record/uniqueness/model.rb +1 -1
  25. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +82 -70
  26. data/lib/shoulda/matchers/active_record.rb +1 -0
  27. data/lib/shoulda/matchers/doublespeak/double_collection.rb +2 -6
  28. data/lib/shoulda/matchers/doublespeak/world.rb +2 -6
  29. data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +12 -14
  30. data/lib/shoulda/matchers/integrations/libraries/action_controller.rb +7 -5
  31. data/lib/shoulda/matchers/integrations/libraries/routing.rb +5 -3
  32. data/lib/shoulda/matchers/util.rb +17 -19
  33. data/lib/shoulda/matchers/version.rb +1 -1
  34. metadata +4 -3
@@ -13,7 +13,11 @@ module Shoulda
13
13
  end
14
14
 
15
15
  def associated_class
16
- reflection.klass
16
+ if polymorphic?
17
+ subject
18
+ else
19
+ reflection.klass
20
+ end
17
21
  end
18
22
 
19
23
  def polymorphic?
@@ -70,6 +74,10 @@ module Shoulda
70
74
  reflection.options[:through]
71
75
  end
72
76
 
77
+ def strict_loading?
78
+ reflection.options.fetch(:strict_loading, subject.strict_loading_by_default)
79
+ end
80
+
73
81
  protected
74
82
 
75
83
  attr_reader :reflection, :subject
@@ -8,6 +8,7 @@ module Shoulda
8
8
  :associated_class,
9
9
  :association_foreign_key,
10
10
  :foreign_key,
11
+ :foreign_type,
11
12
  :has_and_belongs_to_many_name,
12
13
  :join_table_name,
13
14
  :polymorphic?,
@@ -122,6 +122,10 @@ module Shoulda
122
122
  reflector.associated_class
123
123
  end
124
124
 
125
+ def actual_value_for_strict_loading
126
+ reflection.strict_loading?
127
+ end
128
+
125
129
  def actual_value_for_option(name)
126
130
  option_value = reflection.options[name]
127
131
 
@@ -22,44 +22,48 @@ module Shoulda
22
22
  if submatcher_passes?(subject)
23
23
  true
24
24
  else
25
- @missing_option = 'and for the record '
25
+ @missing_option = build_missing_option
26
26
 
27
- missing_option <<
27
+ false
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :attribute_name, :optional, :submatcher
34
+
35
+ def submatcher_passes?(subject)
36
+ if optional
37
+ submatcher.matches?(subject)
38
+ else
39
+ submatcher.does_not_match?(subject)
40
+ end
41
+ end
42
+
43
+ def build_missing_option
44
+ String.new('and for the record ').tap do |missing_option_string|
45
+ missing_option_string <<
28
46
  if optional
29
47
  'not to '
30
48
  else
31
49
  'to '
32
50
  end
33
51
 
34
- missing_option << (
52
+ missing_option_string << (
35
53
  'fail validation if '\
36
54
  ":#{attribute_name} is unset; i.e., either the association "\
37
55
  'should have been defined with `optional: '\
38
56
  "#{optional.inspect}`, or there "
39
57
  )
40
58
 
41
- missing_option <<
59
+ missing_option_string <<
42
60
  if optional
43
61
  'should not '
44
62
  else
45
63
  'should '
46
64
  end
47
65
 
48
- missing_option << "be a presence validation on :#{attribute_name}"
49
-
50
- false
51
- end
52
- end
53
-
54
- private
55
-
56
- attr_reader :attribute_name, :optional, :submatcher
57
-
58
- def submatcher_passes?(subject)
59
- if optional
60
- submatcher.matches?(subject)
61
- else
62
- submatcher.does_not_match?(subject)
66
+ missing_option_string << "be a presence validation on :#{attribute_name}"
63
67
  end
64
68
  end
65
69
  end
@@ -23,50 +23,54 @@ module Shoulda
23
23
  if submatcher_passes?(subject)
24
24
  true
25
25
  else
26
- @missing_option = 'and for the record '
26
+ @missing_option = build_missing_option
27
27
 
28
- missing_option <<
28
+ false
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :attribute_name, :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
+ :required
46
+ end
47
+
48
+ def build_missing_option
49
+ String.new('and for the record ').tap do |missing_option_string|
50
+ missing_option_string <<
29
51
  if required
30
52
  'to '
31
53
  else
32
54
  'not to '
33
55
  end
34
56
 
35
- missing_option << (
57
+ missing_option_string << (
36
58
  'fail validation if '\
37
59
  ":#{attribute_name} is unset; i.e., either the association "\
38
60
  'should have been defined with `required: '\
39
61
  "#{required.inspect}`, or there "
40
62
  )
41
63
 
42
- missing_option <<
64
+ missing_option_string <<
43
65
  if required
44
66
  'should '
45
67
  else
46
68
  'should not '
47
69
  end
48
70
 
49
- missing_option << "be a presence validation on :#{attribute_name}"
50
-
51
- false
71
+ missing_option_string << "be a presence validation on :#{attribute_name}"
52
72
  end
53
73
  end
54
-
55
- private
56
-
57
- attr_reader :attribute_name, :required, :submatcher
58
-
59
- def submatcher_passes?(subject)
60
- if required
61
- submatcher.matches?(subject)
62
- else
63
- submatcher.does_not_match?(subject)
64
- end
65
- end
66
-
67
- def validation_message_key
68
- :required
69
- end
70
74
  end
71
75
  end
72
76
  end
@@ -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
@@ -152,18 +152,18 @@ module Shoulda
152
152
  end
153
153
 
154
154
  def description
155
- description = 'have '
156
-
157
- description <<
158
- if qualifiers.include?(:unique)
159
- "#{Shoulda::Matchers::Util.a_or_an(index_type)} "
160
- else
161
- 'an '
162
- end
155
+ String.new('have ').tap do |description|
156
+ description <<
157
+ if qualifiers.include?(:unique)
158
+ "#{Shoulda::Matchers::Util.a_or_an(index_type)} "
159
+ else
160
+ 'an '
161
+ end
163
162
 
164
- description << 'index on '
163
+ description << 'index on '
165
164
 
166
- description << inspected_expected_columns
165
+ description << inspected_expected_columns
166
+ end
167
167
  end
168
168
 
169
169
  private
@@ -59,7 +59,7 @@ module Shoulda
59
59
  private
60
60
 
61
61
  def readonly_attributes
62
- @_readonly_attributes ||= (@subject.class.readonly_attributes || [])
62
+ @_readonly_attributes ||= @subject.class.readonly_attributes || []
63
63
  end
64
64
 
65
65
  def class_name
@@ -40,7 +40,7 @@ module Shoulda
40
40
  #
41
41
  # # Minitest (Shoulda)
42
42
  # class User < ActiveSupport::TestCase
43
- # should normalize(:email, handle).from(" Example\n").to("example")
43
+ # should normalize(:email, :handle).from(" Example\n").to("example")
44
44
  # end
45
45
  #
46
46
  # If the normalization accepts nil values with the `apply_to_nil` option,
@@ -29,7 +29,7 @@ module Shoulda
29
29
  end
30
30
 
31
31
  def symlink_to(parent)
32
- namespace.set(name, parent.dup)
32
+ namespace.set(name, Class.new(parent))
33
33
  end
34
34
 
35
35
  def to_s
@@ -411,9 +411,29 @@ module Shoulda
411
411
  if scopes_match?
412
412
  true
413
413
  else
414
- @failure_reason = 'Expected the validation '
414
+ @failure_reason = String.new('Expected the validation ').tap do |failure_reason_string|
415
+ failure_reason_string <<
416
+ if expected_scopes.empty?
417
+ 'not to be scoped to anything, '
418
+ else
419
+ "to be scoped to #{inspected_expected_scopes}, "
420
+ end
421
+
422
+ if actual_sets_of_scopes.any?
423
+ failure_reason_string << 'but it was scoped to '
424
+ failure_reason_string << "#{inspected_actual_scopes} instead."
425
+ else
426
+ failure_reason_string << 'but it was not scoped to anything.'
427
+ end
428
+ end
415
429
 
416
- @failure_reason <<
430
+ false
431
+ end
432
+ end
433
+
434
+ def build_failure_reason
435
+ String.new('Expected the validation ').tap do |failure_reason_string|
436
+ failure_reason_string <<
417
437
  if expected_scopes.empty?
418
438
  'not to be scoped to anything, '
419
439
  else
@@ -421,27 +441,25 @@ module Shoulda
421
441
  end
422
442
 
423
443
  if actual_sets_of_scopes.any?
424
- @failure_reason << 'but it was scoped to '
425
- @failure_reason << "#{inspected_actual_scopes} instead."
444
+ failure_reason_string << 'but it was scoped to '
445
+ failure_reason_string << "#{inspected_actual_scopes} instead."
426
446
  else
427
- @failure_reason << 'but it was not scoped to anything.'
447
+ failure_reason_string << 'but it was not scoped to anything.'
428
448
  end
429
-
430
- false
431
449
  end
432
450
  end
433
451
 
434
452
  def does_not_match_scopes_configuration?
435
453
  if scopes_match?
436
- @failure_reason = 'Expected the validation '
437
-
438
- if expected_scopes.empty?
439
- @failure_reason << 'to be scoped to nothing, '
440
- @failure_reason << 'but it was scoped to '
441
- @failure_reason << "#{inspected_actual_scopes} instead."
442
- else
443
- @failure_reason << 'not to be scoped to '
444
- @failure_reason << inspected_expected_scopes
454
+ @failure_reason = String.new('Expected the validation ').tap do |failure_reason|
455
+ if expected_scopes.empty?
456
+ failure_reason << 'to be scoped to nothing, '
457
+ failure_reason << 'but it was scoped to '
458
+ failure_reason << "#{inspected_actual_scopes} instead."
459
+ else
460
+ failure_reason << 'not to be scoped to '
461
+ failure_reason << inspected_expected_scopes
462
+ end
445
463
  end
446
464
 
447
465
  false
@@ -618,20 +636,18 @@ module Shoulda
618
636
  else
619
637
  inspected_scopes = scopes_missing_on_model.map(&:inspect)
620
638
 
621
- reason = ''
622
-
623
- reason << inspected_scopes.to_sentence
624
-
625
- reason <<
626
- if inspected_scopes.many?
627
- ' do not seem to be attributes'
628
- else
629
- ' does not seem to be an attribute'
630
- end
639
+ @failure_reason = String.new.tap do |reason|
640
+ reason << inspected_scopes.to_sentence
631
641
 
632
- reason << " on #{model.name}."
642
+ reason <<
643
+ if inspected_scopes.many?
644
+ ' do not seem to be attributes'
645
+ else
646
+ ' does not seem to be an attribute'
647
+ end
633
648
 
634
- @failure_reason = reason
649
+ reason << " on #{model.name}."
650
+ end
635
651
 
636
652
  false
637
653
  end
@@ -643,20 +659,18 @@ module Shoulda
643
659
  else
644
660
  inspected_scopes = scopes_present_on_model.map(&:inspect)
645
661
 
646
- reason = ''
647
-
648
- reason << inspected_scopes.to_sentence
649
-
650
- reason <<
651
- if inspected_scopes.many?
652
- ' seem to be attributes'
653
- else
654
- ' seems to be an attribute'
655
- end
662
+ @failure_reason = String.new.tap do |reason|
663
+ reason << inspected_scopes.to_sentence
656
664
 
657
- reason << " on #{model.name}."
665
+ reason <<
666
+ if inspected_scopes.many?
667
+ ' seem to be attributes'
668
+ else
669
+ ' seems to be an attribute'
670
+ end
658
671
 
659
- @failure_reason = reason
672
+ reason << " on #{model.name}."
673
+ end
660
674
 
661
675
  false
662
676
  end
@@ -936,45 +950,43 @@ module Shoulda
936
950
  end
937
951
 
938
952
  def failure_message_preface # rubocop:disable Metrics/MethodLength
939
- prefix = ''
940
-
941
- if @existing_record_created
942
- prefix << "After taking the given #{model.name}"
953
+ String.new.tap do |prefix|
954
+ if @existing_record_created
955
+ prefix << "After taking the given #{model.name}"
956
+
957
+ if attribute_setter_for_existing_record
958
+ prefix << ', setting '
959
+ prefix << description_for_attribute_setter(
960
+ attribute_setter_for_existing_record,
961
+ )
962
+ else
963
+ prefix << ", whose :#{attribute} is "
964
+ prefix << "‹#{existing_value_read.inspect}›"
965
+ end
943
966
 
944
- if attribute_setter_for_existing_record
945
- prefix << ', setting '
967
+ prefix << ', and saving it as the existing record, then'
968
+ elsif attribute_setter_for_existing_record
969
+ prefix << "Given an existing #{model.name},"
970
+ prefix << ' after setting '
946
971
  prefix << description_for_attribute_setter(
947
972
  attribute_setter_for_existing_record,
948
973
  )
974
+ prefix << ', then'
949
975
  else
950
- prefix << ", whose :#{attribute} is "
951
- prefix << "‹#{existing_value_read.inspect}›"
976
+ prefix << "Given an existing #{model.name} whose :#{attribute}"
977
+ prefix << ' is '
978
+ prefix << Shoulda::Matchers::Util.inspect_value(
979
+ existing_value_read,
980
+ )
981
+ prefix << ', after'
952
982
  end
953
983
 
954
- prefix << ', and saving it as the existing record, then'
955
- elsif attribute_setter_for_existing_record
956
- prefix << "Given an existing #{model.name},"
957
- prefix << ' after setting '
958
- prefix << description_for_attribute_setter(
959
- attribute_setter_for_existing_record,
960
- )
961
- prefix << ', then'
962
- else
963
- prefix << "Given an existing #{model.name} whose :#{attribute}"
964
- prefix << ' is '
965
- prefix << Shoulda::Matchers::Util.inspect_value(
966
- existing_value_read,
967
- )
968
- prefix << ', after'
969
- end
970
-
971
- prefix << " making a new #{model.name} and setting "
984
+ prefix << " making a new #{model.name} and setting "
972
985
 
973
- prefix << descriptions_for_attribute_setters_for_new_record
986
+ prefix << descriptions_for_attribute_setters_for_new_record
974
987
 
975
- prefix << ", the matcher expected the new #{model.name} to be"
976
-
977
- prefix
988
+ prefix << ", the matcher expected the new #{model.name} to be"
989
+ end
978
990
  end
979
991
 
980
992
  def attribute_changed_value_message
@@ -25,6 +25,7 @@ require 'shoulda/matchers/active_record/uniqueness'
25
25
  require 'shoulda/matchers/active_record/validate_uniqueness_of_matcher'
26
26
  require 'shoulda/matchers/active_record/have_attached_matcher'
27
27
  require 'shoulda/matchers/active_record/normalize_matcher'
28
+ require 'shoulda/matchers/active_record/encrypt_matcher'
28
29
 
29
30
  module Shoulda
30
31
  module Matchers
@@ -18,15 +18,11 @@ module Shoulda
18
18
  end
19
19
 
20
20
  def activate
21
- doubles_by_method_name.each do |_method_name, double|
22
- double.activate
23
- end
21
+ doubles_by_method_name.each_value(&:activate)
24
22
  end
25
23
 
26
24
  def deactivate
27
- doubles_by_method_name.each do |_method_name, double|
28
- double.deactivate
29
- end
25
+ doubles_by_method_name.each_value(&:deactivate)
30
26
  end
31
27
 
32
28
  def calls_by_method_name
@@ -39,15 +39,11 @@ module Shoulda
39
39
  private
40
40
 
41
41
  def activate
42
- double_collections_by_class.each do |_klass, double_collection|
43
- double_collection.activate
44
- end
42
+ double_collections_by_class.each_value(&:activate)
45
43
  end
46
44
 
47
45
  def deactivate
48
- double_collections_by_class.each do |_klass, double_collection|
49
- double_collection.deactivate
50
- end
46
+ double_collections_by_class.each_value(&:deactivate)
51
47
  end
52
48
 
53
49
  def double_collections_by_class