shoulda-matchers 5.3.0 → 6.1.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 +26 -9
- data/lib/shoulda/matchers/action_controller/permit_matcher.rb +7 -9
- data/lib/shoulda/matchers/action_controller/set_session_or_flash_matcher.rb +13 -15
- data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +10 -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/range_matcher.rb +1 -1
- data/lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb +16 -6
- data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +0 -6
- data/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +532 -0
- data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +3 -3
- data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +4 -3
- data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +64 -9
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +40 -96
- 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 +2 -1
- data/lib/shoulda/matchers/active_record/association_matcher.rb +31 -11
- 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 +0 -8
- 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/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/independent/delegate_method_matcher.rb +13 -15
- data/lib/shoulda/matchers/rails_shim.rb +8 -6
- data/lib/shoulda/matchers/util/word_wrap.rb +1 -1
- data/lib/shoulda/matchers/util.rb +17 -19
- data/lib/shoulda/matchers/version.rb +1 -1
- data/lib/shoulda/matchers.rb +2 -2
- data/shoulda-matchers.gemspec +1 -1
- metadata +11 -8
- data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +0 -136
@@ -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
|
@@ -48,6 +48,30 @@ module Shoulda
|
|
48
48
|
# should have_db_column(:camera_aperture).of_type(:decimal)
|
49
49
|
# end
|
50
50
|
#
|
51
|
+
# ##### of_sql_type
|
52
|
+
#
|
53
|
+
# Use `of_sql_type` to assert that a column is defined as a certain sql_type.
|
54
|
+
#
|
55
|
+
# class CreatePhones < ActiveRecord::Migration
|
56
|
+
# def change
|
57
|
+
# create_table :phones do |t|
|
58
|
+
# t.string :camera_aperture, limit: 36
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# # RSpec
|
64
|
+
# RSpec.describe Phone, type: :model do
|
65
|
+
# it do
|
66
|
+
# should have_db_column(:camera_aperture).of_sql_type('varchar(36)')
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# # Minitest (Shoulda)
|
71
|
+
# class PhoneTest < ActiveSupport::TestCase
|
72
|
+
# should have_db_column(:camera_aperture).of_sql_type('varchar(36)')
|
73
|
+
# end
|
74
|
+
#
|
51
75
|
# ##### with_options
|
52
76
|
#
|
53
77
|
# Use `with_options` to assert that a column has been defined with
|
@@ -96,6 +120,11 @@ module Shoulda
|
|
96
120
|
self
|
97
121
|
end
|
98
122
|
|
123
|
+
def of_sql_type(sql_column_type)
|
124
|
+
@options[:sql_column_type] = sql_column_type
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
99
128
|
def with_options(opts = {})
|
100
129
|
validate_options(opts)
|
101
130
|
OPTIONS.each do |attribute|
|
@@ -110,6 +139,7 @@ module Shoulda
|
|
110
139
|
@subject = subject
|
111
140
|
column_exists? &&
|
112
141
|
correct_column_type? &&
|
142
|
+
correct_sql_column_type? &&
|
113
143
|
correct_precision? &&
|
114
144
|
correct_limit? &&
|
115
145
|
correct_default? &&
|
@@ -129,12 +159,9 @@ module Shoulda
|
|
129
159
|
|
130
160
|
def description
|
131
161
|
desc = "have db column named #{@column}"
|
132
|
-
if @options.key?(:column_type)
|
133
|
-
|
134
|
-
|
135
|
-
if @options.key?(:precision)
|
136
|
-
desc << " of precision #{@options[:precision]}"
|
137
|
-
end
|
162
|
+
desc << " of type #{@options[:column_type]}" if @options.key?(:column_type)
|
163
|
+
desc << " of sql_type #{@options[:sql_column_type]}" if @options.key?(:sql_column_type)
|
164
|
+
desc << " of precision #{@options[:precision]}" if @options.key?(:precision)
|
138
165
|
desc << " of limit #{@options[:limit]}" if @options.key?(:limit)
|
139
166
|
desc << " of default #{@options[:default]}" if @options.key?(:default)
|
140
167
|
desc << " of null #{@options[:null]}" if @options.key?(:null)
|
@@ -178,6 +205,19 @@ module Shoulda
|
|
178
205
|
end
|
179
206
|
end
|
180
207
|
|
208
|
+
def correct_sql_column_type?
|
209
|
+
return true unless @options.key?(:sql_column_type)
|
210
|
+
|
211
|
+
if matched_column.sql_type.to_s == @options[:sql_column_type].to_s
|
212
|
+
true
|
213
|
+
else
|
214
|
+
@missing =
|
215
|
+
"#{model_class} has a db column named #{@column} " <<
|
216
|
+
"of sql type #{matched_column.sql_type}, not #{@options[:sql_column_type]}."
|
217
|
+
false
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
181
221
|
def correct_precision?
|
182
222
|
return true unless @options.key?(:precision)
|
183
223
|
|
@@ -152,18 +152,18 @@ module Shoulda
|
|
152
152
|
end
|
153
153
|
|
154
154
|
def description
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
163
|
+
description << 'index on '
|
165
164
|
|
166
|
-
|
165
|
+
description << inspected_expected_columns
|
166
|
+
end
|
167
167
|
end
|
168
168
|
|
169
169
|
private
|
@@ -197,17 +197,28 @@ module Shoulda
|
|
197
197
|
def matched_index
|
198
198
|
@_matched_index ||=
|
199
199
|
if expected_columns.one?
|
200
|
-
|
200
|
+
sorted_indexes.detect do |index|
|
201
201
|
Array.wrap(index.columns) == expected_columns
|
202
202
|
end
|
203
203
|
else
|
204
|
-
|
204
|
+
sorted_indexes.detect do |index|
|
205
205
|
index.columns == expected_columns
|
206
206
|
end
|
207
207
|
end
|
208
208
|
end
|
209
209
|
|
210
|
-
def
|
210
|
+
def sorted_indexes
|
211
|
+
if qualifiers.include?(:unique)
|
212
|
+
# return indexes with unique matching the qualifier first
|
213
|
+
unsorted_indexes.sort_by do |index|
|
214
|
+
index.unique == qualifiers[:unique] ? 0 : 1
|
215
|
+
end
|
216
|
+
else
|
217
|
+
unsorted_indexes
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def unsorted_indexes
|
211
222
|
model.connection.indexes(table_name)
|
212
223
|
end
|
213
224
|
|
@@ -2,7 +2,7 @@ module Shoulda
|
|
2
2
|
module Matchers
|
3
3
|
module ActiveRecord
|
4
4
|
# The `have_implicit_order_column` matcher tests that the model has `implicit_order_column`
|
5
|
-
# assigned to one of the table columns.
|
5
|
+
# assigned to one of the table columns.
|
6
6
|
#
|
7
7
|
# class Product < ApplicationRecord
|
8
8
|
# self.implicit_order_column = :created_at
|
@@ -20,10 +20,8 @@ module Shoulda
|
|
20
20
|
#
|
21
21
|
# @return [HaveImplicitOrderColumnMatcher]
|
22
22
|
#
|
23
|
-
|
24
|
-
|
25
|
-
HaveImplicitOrderColumnMatcher.new(column_name)
|
26
|
-
end
|
23
|
+
def have_implicit_order_column(column_name)
|
24
|
+
HaveImplicitOrderColumnMatcher.new(column_name)
|
27
25
|
end
|
28
26
|
|
29
27
|
# @private
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module Shoulda
|
2
|
+
module Matchers
|
3
|
+
module ActiveRecord
|
4
|
+
# The `normalize` matcher is used to ensure attribute normalizations
|
5
|
+
# are transforming attribute values as expected.
|
6
|
+
#
|
7
|
+
# Take this model for example:
|
8
|
+
#
|
9
|
+
# class User < ActiveRecord::Base
|
10
|
+
# normalizes :email, with: -> email { email.strip.downcase }
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# You can use `normalize` providing an input and defining the expected
|
14
|
+
# normalization output:
|
15
|
+
#
|
16
|
+
# # RSpec
|
17
|
+
# RSpec.describe User, type: :model do
|
18
|
+
# it do
|
19
|
+
# should normalize(:email).from(" ME@XYZ.COM\n").to("me@xyz.com")
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# # Minitest (Shoulda)
|
24
|
+
# class User < ActiveSupport::TestCase
|
25
|
+
# should normalize(:email).from(" ME@XYZ.COM\n").to("me@xyz.com")
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# You can use `normalize` to test multiple attributes at once:
|
29
|
+
#
|
30
|
+
# class User < ActiveRecord::Base
|
31
|
+
# normalizes :email, :handle, with: -> value { value.strip.downcase }
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# # RSpec
|
35
|
+
# RSpec.describe User, type: :model do
|
36
|
+
# it do
|
37
|
+
# should normalize(:email, :handle).from(" Example\n").to("example")
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# # Minitest (Shoulda)
|
42
|
+
# class User < ActiveSupport::TestCase
|
43
|
+
# should normalize(:email, handle).from(" Example\n").to("example")
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# If the normalization accepts nil values with the `apply_to_nil` option,
|
47
|
+
# you just need to use `.from(nil).to("Your expected value here")`.
|
48
|
+
#
|
49
|
+
# class User < ActiveRecord::Base
|
50
|
+
# normalizes :name, with: -> name { name&.titleize || 'Untitled' },
|
51
|
+
# apply_to_nil: true
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# # RSpec
|
55
|
+
# RSpec.describe User, type: :model do
|
56
|
+
# it { should normalize(:name).from("jane doe").to("Jane Doe") }
|
57
|
+
# it { should normalize(:name).from(nil).to("Untitled") }
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# # Minitest (Shoulda)
|
61
|
+
# class User < ActiveSupport::TestCase
|
62
|
+
# should normalize(:name).from("jane doe").to("Jane Doe")
|
63
|
+
# should normalize(:name).from(nil).to("Untitled")
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# @return [NormalizeMatcher]
|
67
|
+
#
|
68
|
+
def normalize(*attributes)
|
69
|
+
if attributes.empty?
|
70
|
+
raise ArgumentError, 'need at least one attribute'
|
71
|
+
else
|
72
|
+
NormalizeMatcher.new(*attributes)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# @private
|
77
|
+
class NormalizeMatcher
|
78
|
+
attr_reader :attributes, :from_value, :to_value, :failure_message,
|
79
|
+
:failure_message_when_negated
|
80
|
+
|
81
|
+
def initialize(*attributes)
|
82
|
+
@attributes = attributes
|
83
|
+
end
|
84
|
+
|
85
|
+
def description
|
86
|
+
%(
|
87
|
+
normalize #{attributes.to_sentence(last_word_connector: ' and ')} from
|
88
|
+
‹#{from_value.inspect}› to ‹#{to_value.inspect}›
|
89
|
+
).squish
|
90
|
+
end
|
91
|
+
|
92
|
+
def from(value)
|
93
|
+
@from_value = value
|
94
|
+
|
95
|
+
self
|
96
|
+
end
|
97
|
+
|
98
|
+
def to(value)
|
99
|
+
@to_value = value
|
100
|
+
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
def matches?(subject)
|
105
|
+
attributes.all? { |attribute| attribute_matches?(subject, attribute) }
|
106
|
+
end
|
107
|
+
|
108
|
+
def does_not_match?(subject)
|
109
|
+
attributes.all? { |attribute| attribute_does_not_match?(subject, attribute) }
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def attribute_matches?(subject, attribute)
|
115
|
+
return true if normalize_attribute?(subject, attribute)
|
116
|
+
|
117
|
+
@failure_message = build_failure_message(
|
118
|
+
attribute,
|
119
|
+
subject.class.normalize_value_for(attribute, from_value),
|
120
|
+
)
|
121
|
+
false
|
122
|
+
end
|
123
|
+
|
124
|
+
def attribute_does_not_match?(subject, attribute)
|
125
|
+
return true unless normalize_attribute?(subject, attribute)
|
126
|
+
|
127
|
+
@failure_message_when_negated = build_failure_message_when_negated(attribute)
|
128
|
+
false
|
129
|
+
end
|
130
|
+
|
131
|
+
def normalize_attribute?(subject, attribute)
|
132
|
+
subject.class.normalize_value_for(attribute, from_value) == to_value
|
133
|
+
end
|
134
|
+
|
135
|
+
def build_failure_message(attribute, attribute_value)
|
136
|
+
%(
|
137
|
+
Expected to normalize #{attribute.inspect} from ‹#{from_value.inspect}› to
|
138
|
+
‹#{to_value.inspect}› but it was normalized to ‹#{attribute_value.inspect}›
|
139
|
+
).squish
|
140
|
+
end
|
141
|
+
|
142
|
+
def build_failure_message_when_negated(attribute)
|
143
|
+
%(
|
144
|
+
Expected to not normalize #{attribute.inspect} from ‹#{from_value.inspect}› to
|
145
|
+
‹#{to_value.inspect}› but it was normalized
|
146
|
+
).squish
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|