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.
- checksums.yaml +4 -4
- data/.hound/ruby.yml +336 -316
- data/.python-version +1 -0
- data/.rubocop.yml +3 -1
- data/.travis.yml +7 -6
- data/Appraisals +76 -44
- data/CONTRIBUTING.md +137 -66
- data/Gemfile +5 -5
- data/Gemfile.lock +30 -35
- data/MAINTAINING.md +250 -0
- data/MIT-LICENSE +1 -1
- data/NEWS.md +176 -4
- data/README.md +138 -200
- data/Rakefile +7 -0
- data/bin/setup +190 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/global.css +4 -0
- data/doc_config/yard/templates/default/fulldoc/html/full_list.erb +0 -6
- data/doc_config/yard/templates/default/fulldoc/html/js/app.js +0 -17
- data/doc_config/yard/templates/default/fulldoc/html/setup.rb +27 -0
- data/gemfiles/4.2.gemfile +21 -20
- data/gemfiles/4.2.gemfile.lock +143 -140
- data/gemfiles/5.0.gemfile +37 -0
- data/gemfiles/5.0.gemfile.lock +238 -0
- data/gemfiles/5.1.gemfile +38 -0
- data/gemfiles/5.1.gemfile.lock +254 -0
- data/gemfiles/5.2.gemfile +40 -0
- data/gemfiles/5.2.gemfile.lock +273 -0
- data/lib/shoulda/matchers/action_controller/callback_matcher.rb +18 -6
- data/lib/shoulda/matchers/action_controller/permit_matcher.rb +6 -1
- data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +1 -1
- data/lib/shoulda/matchers/action_controller/route_matcher.rb +87 -27
- data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +1 -0
- data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter.rb +0 -4
- data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +5 -0
- data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +5 -0
- data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +26 -11
- data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +39 -4
- data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +116 -47
- data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +127 -38
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +55 -37
- data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +30 -1
- data/lib/shoulda/matchers/active_model/validation_matcher.rb +11 -4
- data/lib/shoulda/matchers/active_model/validation_matcher/build_description.rb +11 -6
- data/lib/shoulda/matchers/active_record.rb +3 -0
- data/lib/shoulda/matchers/active_record/association_matcher.rb +172 -22
- data/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb +1 -1
- data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +11 -6
- data/lib/shoulda/matchers/active_record/association_matchers/optional_matcher.rb +46 -0
- data/lib/shoulda/matchers/active_record/association_matchers/required_matcher.rb +51 -0
- data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +268 -38
- data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +1 -1
- data/lib/shoulda/matchers/active_record/have_secure_token_matcher.rb +111 -0
- data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +207 -79
- data/lib/shoulda/matchers/doublespeak/object_double.rb +5 -1
- data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +100 -21
- data/lib/shoulda/matchers/rails_shim.rb +133 -52
- data/lib/shoulda/matchers/routing.rb +2 -2
- data/lib/shoulda/matchers/util.rb +23 -1
- data/lib/shoulda/matchers/util/word_wrap.rb +6 -2
- data/lib/shoulda/matchers/version.rb +1 -1
- data/script/install_gems_in_all_appraisals +3 -1
- data/script/run_all_tests +3 -1
- data/script/supported_ruby_versions +7 -0
- data/script/update_gem_in_all_appraisals +3 -1
- data/script/update_gems_in_all_appraisals +3 -1
- data/shoulda-matchers.gemspec +3 -3
- data/spec/acceptance/independent_matchers_spec.rb +2 -2
- data/spec/acceptance/multiple_libraries_integration_spec.rb +1 -1
- data/spec/acceptance/rails_integration_spec.rb +2 -2
- data/spec/spec_helper.rb +2 -3
- data/spec/support/acceptance/helpers.rb +2 -0
- data/spec/support/acceptance/helpers/command_helpers.rb +17 -4
- data/spec/support/acceptance/helpers/rails_migration_helpers.rb +21 -0
- data/spec/support/acceptance/helpers/step_helpers.rb +1 -1
- data/spec/support/tests/current_bundle.rb +3 -9
- data/spec/support/tests/filesystem.rb +2 -2
- data/spec/support/unit/attribute.rb +0 -2
- data/spec/support/unit/capture.rb +9 -3
- data/spec/support/unit/helpers/action_pack_versions.rb +22 -0
- data/spec/support/unit/helpers/active_model_versions.rb +4 -0
- data/spec/support/unit/helpers/active_record_versions.rb +22 -2
- data/spec/support/unit/helpers/active_resource_builder.rb +2 -2
- data/spec/support/unit/helpers/controller_builder.rb +1 -1
- data/spec/support/unit/helpers/message_helpers.rb +19 -0
- data/spec/support/unit/helpers/rails_versions.rb +14 -0
- data/spec/support/unit/matchers/fail_with_message_matcher.rb +7 -5
- data/spec/support/unit/matchers/print_warning_including.rb +21 -13
- data/spec/support/unit/model_creation_strategies/active_record.rb +1 -1
- data/spec/support/unit/model_creators/active_record.rb +0 -1
- data/spec/support/unit/model_creators/basic.rb +7 -2
- data/spec/support/unit/rails_application.rb +25 -0
- data/spec/support/unit/record_validating_confirmation_builder.rb +5 -2
- data/spec/support/unit/validation_matcher_scenario.rb +0 -2
- data/spec/unit/shoulda/matchers/action_controller/callback_matcher_spec.rb +18 -18
- data/spec/unit/shoulda/matchers/action_controller/permit_matcher_spec.rb +33 -5
- data/spec/unit/shoulda/matchers/action_controller/render_template_matcher_spec.rb +1 -1
- data/spec/unit/shoulda/matchers/active_model/allow_mass_assignment_of_matcher_spec.rb +80 -78
- data/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb +7 -9
- data/spec/unit/shoulda/matchers/active_model/validate_absence_of_matcher_spec.rb +28 -4
- data/spec/unit/shoulda/matchers/active_model/validate_acceptance_of_matcher_spec.rb +19 -1
- data/spec/unit/shoulda/matchers/active_model/validate_confirmation_of_matcher_spec.rb +27 -4
- data/spec/unit/shoulda/matchers/active_model/validate_exclusion_of_matcher_spec.rb +62 -5
- data/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb +52 -18
- data/spec/unit/shoulda/matchers/active_model/validate_length_of_matcher_spec.rb +51 -4
- data/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +99 -71
- data/spec/unit/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +41 -15
- data/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb +445 -15
- data/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb +615 -93
- data/spec/unit/shoulda/matchers/active_record/have_secure_token_matcher_spec.rb +169 -0
- data/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb +167 -97
- data/spec/unit/shoulda/matchers/doublespeak/world_spec.rb +2 -4
- data/spec/unit/shoulda/matchers/independent/delegate_method_matcher_spec.rb +152 -19
- data/spec/unit/shoulda/matchers/routing/route_matcher_spec.rb +258 -94
- data/spec/unit_spec_helper.rb +9 -1
- data/zeus.json +1 -1
- metadata +31 -16
- data/gemfiles/4.0.0.gemfile +0 -38
- data/gemfiles/4.0.0.gemfile.lock +0 -223
- data/gemfiles/4.0.1.gemfile +0 -38
- data/gemfiles/4.0.1.gemfile.lock +0 -225
- data/gemfiles/4.1.gemfile +0 -38
- data/gemfiles/4.1.gemfile.lock +0 -220
- 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
|
-
# #####
|
23
|
+
# ##### with_values
|
24
24
|
#
|
25
|
-
# Use `
|
26
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
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
|
-
|
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
|
-
"
|
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
|
-
|
83
|
-
desc << " with #{options[:expected_enum_values]}"
|
84
|
-
end
|
231
|
+
private
|
85
232
|
|
86
|
-
|
233
|
+
attr_reader :attribute_name, :options, :record, :failure_reason
|
87
234
|
|
88
|
-
|
235
|
+
def expectation
|
236
|
+
description
|
89
237
|
end
|
90
238
|
|
91
|
-
|
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
|
-
|
247
|
+
def normalized_expected_enum_values
|
248
|
+
to_hash(expected_enum_values)
|
249
|
+
end
|
94
250
|
|
95
|
-
def
|
96
|
-
|
251
|
+
def expected_enum_value_names
|
252
|
+
to_array(expected_enum_values)
|
97
253
|
end
|
98
254
|
|
99
255
|
def expected_enum_values
|
100
|
-
|
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
|
-
|
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
|
116
|
-
column.type ==
|
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
|
128
|
-
|
129
|
-
|
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
|
133
|
-
|
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
|
-
|
136
|
-
|
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
|
-
|
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
|
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
|