durable_parameters 0.2.3
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +853 -0
- data/Rakefile +29 -0
- data/app/params/account_params.rb.example +38 -0
- data/app/params/application_params.rb +16 -0
- data/lib/durable_parameters/adapters/hanami.rb +138 -0
- data/lib/durable_parameters/adapters/rage.rb +124 -0
- data/lib/durable_parameters/adapters/rails.rb +280 -0
- data/lib/durable_parameters/adapters/sinatra.rb +91 -0
- data/lib/durable_parameters/core/application_params.rb +334 -0
- data/lib/durable_parameters/core/configuration.rb +83 -0
- data/lib/durable_parameters/core/forbidden_attributes_protection.rb +48 -0
- data/lib/durable_parameters/core/parameters.rb +643 -0
- data/lib/durable_parameters/core/params_registry.rb +110 -0
- data/lib/durable_parameters/core.rb +15 -0
- data/lib/durable_parameters/log_subscriber.rb +34 -0
- data/lib/durable_parameters/railtie.rb +65 -0
- data/lib/durable_parameters/version.rb +7 -0
- data/lib/durable_parameters.rb +41 -0
- data/lib/generators/rails/USAGE +12 -0
- data/lib/generators/rails/durable_parameters_controller_generator.rb +17 -0
- data/lib/generators/rails/templates/controller.rb +94 -0
- data/lib/legacy/action_controller/application_params.rb +235 -0
- data/lib/legacy/action_controller/parameters.rb +524 -0
- data/lib/legacy/action_controller/params_registry.rb +108 -0
- data/lib/legacy/active_model/forbidden_attributes_protection.rb +40 -0
- data/test/action_controller_required_params_test.rb +36 -0
- data/test/action_controller_tainted_params_test.rb +29 -0
- data/test/active_model_mass_assignment_taint_protection_test.rb +25 -0
- data/test/application_params_array_test.rb +245 -0
- data/test/application_params_edge_cases_test.rb +361 -0
- data/test/application_params_test.rb +893 -0
- data/test/controller_generator_test.rb +31 -0
- data/test/core_parameters_test.rb +2376 -0
- data/test/durable_parameters_test.rb +115 -0
- data/test/enhanced_error_messages_test.rb +120 -0
- data/test/gemfiles/Gemfile.rails-3.0.x +14 -0
- data/test/gemfiles/Gemfile.rails-3.1.x +14 -0
- data/test/gemfiles/Gemfile.rails-3.2.x +14 -0
- data/test/log_on_unpermitted_params_test.rb +49 -0
- data/test/metadata_validation_test.rb +294 -0
- data/test/multi_parameter_attributes_test.rb +38 -0
- data/test/parameters_core_methods_test.rb +503 -0
- data/test/parameters_integration_test.rb +553 -0
- data/test/parameters_permit_test.rb +491 -0
- data/test/parameters_require_test.rb +9 -0
- data/test/parameters_taint_test.rb +98 -0
- data/test/params_registry_concurrency_test.rb +422 -0
- data/test/params_registry_test.rb +112 -0
- data/test/permit_by_model_test.rb +227 -0
- data/test/raise_on_unpermitted_params_test.rb +32 -0
- data/test/test_helper.rb +38 -0
- data/test/transform_params_edge_cases_test.rb +526 -0
- data/test/transformation_test.rb +360 -0
- metadata +223 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
class ApplicationParamsTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
# Clear the registry before each test
|
|
6
|
+
ActionController::ParamsRegistry.clear!
|
|
7
|
+
|
|
8
|
+
# Define test params classes
|
|
9
|
+
@user_params_class = Class.new(ActionController::ApplicationParams) do
|
|
10
|
+
def self.name
|
|
11
|
+
'UserParams'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
allow :first_name
|
|
15
|
+
allow :last_name
|
|
16
|
+
allow :email
|
|
17
|
+
deny :is_admin
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@account_params_class = Class.new(ActionController::ApplicationParams) do
|
|
21
|
+
def self.name
|
|
22
|
+
'AccountParams'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
allow :name
|
|
26
|
+
allow :description
|
|
27
|
+
flag :require_approval, true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def teardown
|
|
32
|
+
ActionController::ParamsRegistry.clear!
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_allow_adds_attribute_to_allowed_list
|
|
36
|
+
assert_equal [:first_name, :last_name, :email], @user_params_class.allowed_attributes
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_deny_adds_attribute_to_denied_list
|
|
40
|
+
assert_equal [:is_admin], @user_params_class.denied_attributes
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_flag_sets_flag_value
|
|
44
|
+
assert_equal true, @account_params_class.flag?(:require_approval)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_flag_returns_nil_for_unset_flag
|
|
48
|
+
assert_nil @user_params_class.flag?(:nonexistent_flag)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_allowed_returns_true_for_allowed_attribute
|
|
52
|
+
assert @user_params_class.allowed?(:first_name)
|
|
53
|
+
assert @user_params_class.allowed?('last_name')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_allowed_returns_false_for_non_allowed_attribute
|
|
57
|
+
assert !@user_params_class.allowed?(:age)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_allowed_returns_false_for_denied_attribute
|
|
61
|
+
assert !@user_params_class.allowed?(:is_admin)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_denied_returns_true_for_denied_attribute
|
|
65
|
+
assert @user_params_class.denied?(:is_admin)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_permitted_attributes_returns_allowed_list
|
|
69
|
+
permitted = @user_params_class.permitted_attributes
|
|
70
|
+
assert_equal [:first_name, :last_name, :email], permitted
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_allow_with_only_option
|
|
74
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
75
|
+
allow :title, only: [:create, :update]
|
|
76
|
+
allow :body, except: :destroy
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# With action: :create, should include title
|
|
80
|
+
assert_includes test_class.permitted_attributes(action: :create), :title
|
|
81
|
+
|
|
82
|
+
# With action: :show, should not include title
|
|
83
|
+
refute_includes test_class.permitted_attributes(action: :show), :title
|
|
84
|
+
|
|
85
|
+
# With action: :destroy, should not include body
|
|
86
|
+
refute_includes test_class.permitted_attributes(action: :destroy), :body
|
|
87
|
+
|
|
88
|
+
# With action: :update, should include body
|
|
89
|
+
assert_includes test_class.permitted_attributes(action: :update), :body
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_inheritance_copies_parent_attributes
|
|
93
|
+
parent_class = Class.new(ActionController::ApplicationParams) do
|
|
94
|
+
allow :name
|
|
95
|
+
allow :email
|
|
96
|
+
flag :inherited_flag, true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
child_class = Class.new(parent_class) do
|
|
100
|
+
allow :age
|
|
101
|
+
deny :email
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Child should have parent's allowed attributes
|
|
105
|
+
assert_includes child_class.allowed_attributes, :name
|
|
106
|
+
assert_includes child_class.allowed_attributes, :email
|
|
107
|
+
assert_includes child_class.allowed_attributes, :age
|
|
108
|
+
|
|
109
|
+
# Child should have its own denied attributes
|
|
110
|
+
assert_includes child_class.denied_attributes, :email
|
|
111
|
+
|
|
112
|
+
# Child should have parent's flags
|
|
113
|
+
assert_equal true, child_class.flag?(:inherited_flag)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def test_attribute_options
|
|
117
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
118
|
+
allow :field1, only: :create
|
|
119
|
+
allow :field2
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
opts = test_class.attribute_options(:field1)
|
|
123
|
+
assert_equal [:create], opts[:only]
|
|
124
|
+
|
|
125
|
+
opts2 = test_class.attribute_options(:field2)
|
|
126
|
+
assert_equal({}, opts2)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def test_allow_with_invalid_options_stores_them
|
|
130
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
131
|
+
allow :field, invalid_option: :value
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
opts = test_class.attribute_options(:field)
|
|
135
|
+
assert_equal({ invalid_option: :value }, opts)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_deny_does_not_accept_options
|
|
139
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
140
|
+
deny :field
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# deny doesn't store options, so attribute_options should be empty
|
|
144
|
+
opts = test_class.attribute_options(:field)
|
|
145
|
+
assert_equal({}, opts)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def test_flag_with_default_value
|
|
149
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
150
|
+
flag :enabled
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
assert_equal true, test_class.flag?(:enabled)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def test_allowed_with_invalid_input
|
|
157
|
+
# Test with nil
|
|
158
|
+
assert !@user_params_class.allowed?(nil)
|
|
159
|
+
# Test with empty string
|
|
160
|
+
assert !@user_params_class.allowed?('')
|
|
161
|
+
# Test with array
|
|
162
|
+
assert !@user_params_class.allowed?([:first_name])
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def test_permitted_attributes_with_invalid_action
|
|
166
|
+
# Should return all permitted attributes regardless of invalid action
|
|
167
|
+
permitted = @user_params_class.permitted_attributes(action: :invalid)
|
|
168
|
+
assert_equal [:first_name, :last_name, :email], permitted
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def test_empty_application_params_class
|
|
172
|
+
empty_class = Class.new(ActionController::ApplicationParams)
|
|
173
|
+
|
|
174
|
+
assert_empty empty_class.allowed_attributes
|
|
175
|
+
assert_empty empty_class.denied_attributes
|
|
176
|
+
assert_empty empty_class.permitted_attributes
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def test_multi_level_inheritance
|
|
180
|
+
grandparent_class = Class.new(ActionController::ApplicationParams) do
|
|
181
|
+
allow :name
|
|
182
|
+
flag :inherited, true
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
parent_class = Class.new(grandparent_class) do
|
|
186
|
+
allow :email
|
|
187
|
+
flag :parent_flag, 'value'
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
child_class = Class.new(parent_class) do
|
|
191
|
+
allow :age
|
|
192
|
+
deny :name
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Child should inherit from grandparent and parent
|
|
196
|
+
assert_includes child_class.allowed_attributes, :name
|
|
197
|
+
assert_includes child_class.allowed_attributes, :email
|
|
198
|
+
assert_includes child_class.allowed_attributes, :age
|
|
199
|
+
assert_includes child_class.denied_attributes, :name
|
|
200
|
+
|
|
201
|
+
assert_equal true, child_class.flag?(:inherited)
|
|
202
|
+
assert_equal 'value', child_class.flag?(:parent_flag)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def test_attribute_options_for_nonexistent_attribute
|
|
206
|
+
opts = @user_params_class.attribute_options(:nonexistent)
|
|
207
|
+
assert_equal({}, opts)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def test_flag_for_nonexistent_flag
|
|
211
|
+
assert_nil @user_params_class.flag?(:nonexistent)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def test_complex_attribute_options
|
|
215
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
216
|
+
allow :field1, only: [:create, :update], custom_option: 'value'
|
|
217
|
+
allow :field2, except: :destroy
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
opts1 = test_class.attribute_options(:field1)
|
|
221
|
+
assert_equal [:create, :update], opts1[:only]
|
|
222
|
+
assert_equal 'value', opts1[:custom_option]
|
|
223
|
+
|
|
224
|
+
opts2 = test_class.attribute_options(:field2)
|
|
225
|
+
assert_equal [:destroy], opts2[:except]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def test_metadata_declares_allowed_metadata_keys
|
|
229
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
230
|
+
metadata :ip_address, :role
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
assert test_class.metadata_allowed?(:ip_address)
|
|
234
|
+
assert test_class.metadata_allowed?(:role)
|
|
235
|
+
assert !test_class.metadata_allowed?(:unknown)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def test_current_user_always_allowed
|
|
239
|
+
test_class = Class.new(ActionController::ApplicationParams)
|
|
240
|
+
|
|
241
|
+
assert test_class.metadata_allowed?(:current_user)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def test_metadata_allowed_with_invalid_inputs
|
|
245
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
246
|
+
metadata :ip_address
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Test with nil
|
|
250
|
+
assert !test_class.metadata_allowed?(nil)
|
|
251
|
+
# Test with empty string
|
|
252
|
+
assert !test_class.metadata_allowed?('')
|
|
253
|
+
# Test with array
|
|
254
|
+
assert !test_class.metadata_allowed?([:ip_address])
|
|
255
|
+
# Test with integer
|
|
256
|
+
assert !test_class.metadata_allowed?(123)
|
|
257
|
+
# Test with hash
|
|
258
|
+
assert !test_class.metadata_allowed?({ip: 'value'})
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def test_transform_applies_transformation
|
|
262
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
263
|
+
allow :email
|
|
264
|
+
transform :email do |value, metadata|
|
|
265
|
+
value&.downcase
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
params = { 'email' => 'TEST@EXAMPLE.COM' }
|
|
270
|
+
result = test_class.apply_transformations(params)
|
|
271
|
+
assert_equal 'test@example.com', result['email']
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def test_transform_with_metadata
|
|
275
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
276
|
+
allow :role
|
|
277
|
+
transform :role do |value, metadata|
|
|
278
|
+
metadata[:current_user]&.admin? ? value : 'user'
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
params = { 'role' => 'admin' }
|
|
283
|
+
mock_user = Minitest::Mock.new
|
|
284
|
+
mock_user.expect :admin?, true
|
|
285
|
+
metadata = { current_user: mock_user }
|
|
286
|
+
result = test_class.apply_transformations(params, metadata)
|
|
287
|
+
assert_equal 'admin', result['role']
|
|
288
|
+
mock_user.verify
|
|
289
|
+
|
|
290
|
+
mock_user2 = Minitest::Mock.new
|
|
291
|
+
mock_user2.expect :admin?, false
|
|
292
|
+
metadata2 = { current_user: mock_user2 }
|
|
293
|
+
result2 = test_class.apply_transformations(params, metadata2)
|
|
294
|
+
assert_equal 'user', result2['role']
|
|
295
|
+
mock_user2.verify
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def test_allow_with_array_option
|
|
299
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
300
|
+
allow :tags, array: true
|
|
301
|
+
allow :name
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
permitted = test_class.permitted_attributes
|
|
305
|
+
assert_includes permitted, :name
|
|
306
|
+
assert_includes permitted, { tags: [] }
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def test_allow_with_array_option_and_actions
|
|
310
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
311
|
+
allow :tags, array: true, only: :create
|
|
312
|
+
allow :categories, array: true, except: :update
|
|
313
|
+
allow :name
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# For create action
|
|
317
|
+
permitted_create = test_class.permitted_attributes(action: :create)
|
|
318
|
+
assert_includes permitted_create, :name
|
|
319
|
+
assert_includes permitted_create, { tags: [] }
|
|
320
|
+
assert_includes permitted_create, { categories: [] }
|
|
321
|
+
|
|
322
|
+
# For update action
|
|
323
|
+
permitted_update = test_class.permitted_attributes(action: :update)
|
|
324
|
+
assert_includes permitted_update, :name
|
|
325
|
+
refute_includes permitted_update, { tags: [] }
|
|
326
|
+
refute_includes permitted_update, { categories: [] }
|
|
327
|
+
|
|
328
|
+
# For show action
|
|
329
|
+
permitted_show = test_class.permitted_attributes(action: :show)
|
|
330
|
+
assert_includes permitted_show, :name
|
|
331
|
+
refute_includes permitted_show, { tags: [] }
|
|
332
|
+
assert_includes permitted_show, { categories: [] }
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def test_permitted_attributes_caching
|
|
336
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
337
|
+
allow :name
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
first = test_class.permitted_attributes
|
|
341
|
+
second = test_class.permitted_attributes
|
|
342
|
+
assert_equal first, second
|
|
343
|
+
assert first.object_id == second.object_id # frozen
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def test_apply_transformations_with_parameters_object
|
|
347
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
348
|
+
allow :email
|
|
349
|
+
transform :email do |value|
|
|
350
|
+
value&.upcase
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Mock a Parameters-like object
|
|
355
|
+
params_class = Class.new do
|
|
356
|
+
def to_unsafe_h
|
|
357
|
+
{ 'email' => 'test' }
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
params = params_class.new
|
|
361
|
+
|
|
362
|
+
result = test_class.apply_transformations(params)
|
|
363
|
+
assert_equal 'TEST', result['email']
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def test_apply_transformations_with_to_h_method
|
|
367
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
368
|
+
allow :email
|
|
369
|
+
transform :email do |value|
|
|
370
|
+
value&.upcase
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Mock an object that has to_h but not to_unsafe_h
|
|
375
|
+
params_class = Class.new do
|
|
376
|
+
def to_h
|
|
377
|
+
{ 'email' => 'test' }
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
params = params_class.new
|
|
381
|
+
|
|
382
|
+
result = test_class.apply_transformations(params)
|
|
383
|
+
assert_equal 'TEST', result['email']
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def test_apply_transformations_with_plain_hash
|
|
387
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
388
|
+
allow :email
|
|
389
|
+
transform :email do |value|
|
|
390
|
+
value&.upcase
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
params = { 'email' => 'test' }
|
|
395
|
+
result = test_class.apply_transformations(params)
|
|
396
|
+
assert_equal 'TEST', result['email']
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def test_transform_requires_block
|
|
400
|
+
assert_raises ArgumentError do
|
|
401
|
+
Class.new(ActionController::ApplicationParams) do
|
|
402
|
+
transform :email
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def test_apply_transformations_no_transformations
|
|
408
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
409
|
+
allow :name
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
params = { 'name' => 'John' }
|
|
413
|
+
result = test_class.apply_transformations(params)
|
|
414
|
+
assert_equal params, result
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def test_transformation_error_handling
|
|
418
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
419
|
+
allow :email
|
|
420
|
+
transform :email do |value, metadata|
|
|
421
|
+
raise "Transformation error"
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
params = { 'email' => 'test@example.com' }
|
|
426
|
+
assert_raises(RuntimeError, "Transformation error") do
|
|
427
|
+
test_class.apply_transformations(params)
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def test_apply_transformations_with_nested_hashes
|
|
432
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
433
|
+
allow :user
|
|
434
|
+
transform :user do |value, metadata|
|
|
435
|
+
if value.is_a?(Hash)
|
|
436
|
+
value.merge('processed' => true)
|
|
437
|
+
else
|
|
438
|
+
value
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
params = { 'user' => { 'name' => 'John', 'email' => 'john@example.com' } }
|
|
444
|
+
result = test_class.apply_transformations(params)
|
|
445
|
+
expected = { 'user' => { 'name' => 'John', 'email' => 'john@example.com', 'processed' => true } }
|
|
446
|
+
assert_equal expected, result
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def test_apply_transformations_with_arrays
|
|
450
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
451
|
+
allow :tags
|
|
452
|
+
transform :tags do |value, metadata|
|
|
453
|
+
if value.is_a?(Array)
|
|
454
|
+
value.map(&:upcase)
|
|
455
|
+
else
|
|
456
|
+
value
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
params = { 'tags' => ['ruby', 'rails'] }
|
|
462
|
+
result = test_class.apply_transformations(params)
|
|
463
|
+
assert_equal ['RUBY', 'RAILS'], result['tags']
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def test_metadata_validation_in_transformations
|
|
467
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
468
|
+
allow :role
|
|
469
|
+
metadata :current_user # current_user is always allowed, but let's declare it
|
|
470
|
+
transform :role do |value, metadata|
|
|
471
|
+
metadata[:current_user]&.admin? ? value : 'user'
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
params = { 'role' => 'admin' }
|
|
476
|
+
mock_user = Minitest::Mock.new
|
|
477
|
+
mock_user.expect :admin?, true
|
|
478
|
+
# current_user is always allowed, so this should work
|
|
479
|
+
result = test_class.apply_transformations(params, current_user: mock_user)
|
|
480
|
+
assert_equal 'admin', result['role']
|
|
481
|
+
mock_user.verify
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def test_complex_action_filtering
|
|
485
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
486
|
+
allow :title
|
|
487
|
+
allow :body
|
|
488
|
+
allow :published, only: [:create, :update]
|
|
489
|
+
allow :archived, only: :archive
|
|
490
|
+
allow :view_count, except: [:create, :edit]
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Test create action
|
|
494
|
+
permitted_create = test_class.permitted_attributes(action: :create)
|
|
495
|
+
assert_includes permitted_create, :title
|
|
496
|
+
assert_includes permitted_create, :body
|
|
497
|
+
assert_includes permitted_create, :published
|
|
498
|
+
refute_includes permitted_create, :archived
|
|
499
|
+
refute_includes permitted_create, :view_count
|
|
500
|
+
|
|
501
|
+
# Test update action
|
|
502
|
+
permitted_update = test_class.permitted_attributes(action: :update)
|
|
503
|
+
assert_includes permitted_update, :title
|
|
504
|
+
assert_includes permitted_update, :body
|
|
505
|
+
assert_includes permitted_update, :published
|
|
506
|
+
refute_includes permitted_update, :archived
|
|
507
|
+
assert_includes permitted_update, :view_count
|
|
508
|
+
|
|
509
|
+
# Test archive action
|
|
510
|
+
permitted_archive = test_class.permitted_attributes(action: :archive)
|
|
511
|
+
assert_includes permitted_archive, :title
|
|
512
|
+
assert_includes permitted_archive, :body
|
|
513
|
+
refute_includes permitted_archive, :published
|
|
514
|
+
assert_includes permitted_archive, :archived
|
|
515
|
+
assert_includes permitted_archive, :view_count
|
|
516
|
+
|
|
517
|
+
# Test show action (default)
|
|
518
|
+
permitted_show = test_class.permitted_attributes(action: :show)
|
|
519
|
+
assert_includes permitted_show, :title
|
|
520
|
+
assert_includes permitted_show, :body
|
|
521
|
+
refute_includes permitted_show, :published
|
|
522
|
+
refute_includes permitted_show, :archived
|
|
523
|
+
assert_includes permitted_show, :view_count
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def test_permitted_attributes_with_nil_action_and_only_except
|
|
527
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
528
|
+
allow :name
|
|
529
|
+
allow :email
|
|
530
|
+
allow :admin_only, only: :admin
|
|
531
|
+
allow :not_create, except: :create
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# With action: nil, all allowed attributes should be included regardless of only/except
|
|
535
|
+
permitted_nil = test_class.permitted_attributes(action: nil)
|
|
536
|
+
assert_includes permitted_nil, :name
|
|
537
|
+
assert_includes permitted_nil, :email
|
|
538
|
+
assert_includes permitted_nil, :admin_only # included even with only: :admin
|
|
539
|
+
assert_includes permitted_nil, :not_create # included even with except: :create
|
|
540
|
+
|
|
541
|
+
# Compare with no action (should be same as action: nil)
|
|
542
|
+
permitted_no_action = test_class.permitted_attributes
|
|
543
|
+
assert_equal permitted_nil, permitted_no_action
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def test_allowed_denied_edge_cases
|
|
547
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
548
|
+
allow :name
|
|
549
|
+
deny :admin
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Test with nil
|
|
553
|
+
assert !test_class.allowed?(nil)
|
|
554
|
+
assert !test_class.denied?(nil)
|
|
555
|
+
|
|
556
|
+
# Test with empty string
|
|
557
|
+
assert !test_class.allowed?('')
|
|
558
|
+
assert !test_class.denied?('')
|
|
559
|
+
|
|
560
|
+
# Test with integer (doesn't respond to to_sym)
|
|
561
|
+
assert !test_class.allowed?(123)
|
|
562
|
+
assert !test_class.denied?(123)
|
|
563
|
+
|
|
564
|
+
# Test with array
|
|
565
|
+
assert !test_class.allowed?([:name])
|
|
566
|
+
assert !test_class.denied?([:admin])
|
|
567
|
+
|
|
568
|
+
# Test with hash
|
|
569
|
+
assert !test_class.allowed?({name: 'test'})
|
|
570
|
+
assert !test_class.denied?({admin: true})
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def test_permitted_attributes_caching_detailed
|
|
574
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
575
|
+
allow :name
|
|
576
|
+
allow :email
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# First call should compute and cache
|
|
580
|
+
first_call = test_class.permitted_attributes
|
|
581
|
+
assert_equal [:name, :email], first_call
|
|
582
|
+
|
|
583
|
+
# Second call should return cached result
|
|
584
|
+
second_call = test_class.permitted_attributes
|
|
585
|
+
assert_equal first_call, second_call
|
|
586
|
+
assert first_call.object_id == second_call.object_id # Same object from cache
|
|
587
|
+
|
|
588
|
+
# Call with action should cache separately
|
|
589
|
+
action_call = test_class.permitted_attributes(action: :create)
|
|
590
|
+
assert_equal [:name, :email], action_call
|
|
591
|
+
|
|
592
|
+
# Call again with same action should return cached
|
|
593
|
+
action_call2 = test_class.permitted_attributes(action: :create)
|
|
594
|
+
assert action_call.object_id == action_call2.object_id
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def test_permitted_attributes_cache_invalidation_on_allow_deny
|
|
598
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
599
|
+
allow :name
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# First call caches
|
|
603
|
+
first_call = test_class.permitted_attributes
|
|
604
|
+
assert_equal [:name], first_call
|
|
605
|
+
|
|
606
|
+
# Second call returns cached
|
|
607
|
+
second_call = test_class.permitted_attributes
|
|
608
|
+
assert first_call.object_id == second_call.object_id
|
|
609
|
+
|
|
610
|
+
# Call allow again, should clear cache
|
|
611
|
+
test_class.allow :email
|
|
612
|
+
|
|
613
|
+
# Now permitted should include email and cache should be new
|
|
614
|
+
third_call = test_class.permitted_attributes
|
|
615
|
+
assert_equal [:name, :email], third_call
|
|
616
|
+
refute_equal first_call.object_id, third_call.object_id # New cache
|
|
617
|
+
|
|
618
|
+
# Call deny, should clear cache
|
|
619
|
+
test_class.deny :name
|
|
620
|
+
|
|
621
|
+
fourth_call = test_class.permitted_attributes
|
|
622
|
+
assert_equal [:email], fourth_call
|
|
623
|
+
refute_equal third_call.object_id, fourth_call.object_id
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def test_inheritance_with_transformations_and_metadata
|
|
627
|
+
parent_class = Class.new(ActionController::ApplicationParams) do
|
|
628
|
+
allow :name
|
|
629
|
+
metadata :current_user
|
|
630
|
+
transform :name do |value, metadata|
|
|
631
|
+
value&.capitalize
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
child_class = Class.new(parent_class) do
|
|
636
|
+
allow :email
|
|
637
|
+
metadata :ip_address
|
|
638
|
+
transform :email do |value, metadata|
|
|
639
|
+
value&.downcase
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# Child should inherit parent's allowed attributes and metadata
|
|
644
|
+
assert_includes child_class.allowed_attributes, :name
|
|
645
|
+
assert_includes child_class.allowed_attributes, :email
|
|
646
|
+
assert child_class.metadata_allowed?(:current_user)
|
|
647
|
+
assert child_class.metadata_allowed?(:ip_address)
|
|
648
|
+
|
|
649
|
+
# Test transformations
|
|
650
|
+
params = { 'name' => 'john', 'email' => 'JOHN@EXAMPLE.COM' }
|
|
651
|
+
result = child_class.apply_transformations(params)
|
|
652
|
+
assert_equal 'John', result['name']
|
|
653
|
+
assert_equal 'john@example.com', result['email']
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def test_inheritance_copies_attribute_options
|
|
657
|
+
parent_class = Class.new(ActionController::ApplicationParams) do
|
|
658
|
+
allow :name, only: :create
|
|
659
|
+
allow :email, except: :update
|
|
660
|
+
allow :tags, array: true, only: [:create, :update]
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
child_class = Class.new(parent_class) do
|
|
664
|
+
allow :age
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# Child should inherit parent's attribute options
|
|
668
|
+
assert_equal({ only: [:create] }, child_class.attribute_options(:name))
|
|
669
|
+
assert_equal({ except: [:update] }, child_class.attribute_options(:email))
|
|
670
|
+
assert_equal({ array: true, only: [:create, :update] }, child_class.attribute_options(:tags))
|
|
671
|
+
assert_equal({}, child_class.attribute_options(:age))
|
|
672
|
+
|
|
673
|
+
# And permitted attributes should reflect inherited options
|
|
674
|
+
permitted_create = child_class.permitted_attributes(action: :create)
|
|
675
|
+
assert_includes permitted_create, :name
|
|
676
|
+
assert_includes permitted_create, :email
|
|
677
|
+
assert_includes permitted_create, { tags: [] }
|
|
678
|
+
assert_includes permitted_create, :age
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def test_flag_with_various_values
|
|
682
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
683
|
+
flag :enabled, true
|
|
684
|
+
flag :count, 42
|
|
685
|
+
flag :name, 'test'
|
|
686
|
+
flag :data, { key: 'value' }
|
|
687
|
+
flag :disabled, false
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
assert_equal true, test_class.flag?(:enabled)
|
|
691
|
+
assert_equal 42, test_class.flag?(:count)
|
|
692
|
+
assert_equal 'test', test_class.flag?(:name)
|
|
693
|
+
assert_equal({ key: 'value' }, test_class.flag?(:data))
|
|
694
|
+
assert_equal false, test_class.flag?(:disabled)
|
|
695
|
+
assert_nil test_class.flag?(:nonexistent)
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def test_flag_with_invalid_inputs
|
|
699
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
700
|
+
flag :enabled, true
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
# Test with nil
|
|
704
|
+
assert_nil test_class.flag?(nil)
|
|
705
|
+
# Test with empty string
|
|
706
|
+
assert_nil test_class.flag?('')
|
|
707
|
+
# Test with array
|
|
708
|
+
assert_nil test_class.flag?([:enabled])
|
|
709
|
+
# Test with integer
|
|
710
|
+
assert_nil test_class.flag?(123)
|
|
711
|
+
# Test with hash
|
|
712
|
+
assert_nil test_class.flag?({enabled: true})
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def test_apply_transformations_with_empty_hash
|
|
718
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
719
|
+
allow :email
|
|
720
|
+
transform :email do |value|
|
|
721
|
+
value&.upcase
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
result = test_class.apply_transformations({})
|
|
726
|
+
assert_equal({}, result)
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
def test_apply_transformations_with_non_hash_params
|
|
730
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
731
|
+
allow :email
|
|
732
|
+
transform :email do |value|
|
|
733
|
+
value&.upcase
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
result = test_class.apply_transformations("not a hash")
|
|
738
|
+
assert_equal "not a hash", result
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def test_multiple_transformations
|
|
742
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
743
|
+
allow :email
|
|
744
|
+
allow :name
|
|
745
|
+
transform :email do |value|
|
|
746
|
+
value&.downcase
|
|
747
|
+
end
|
|
748
|
+
transform :name do |value|
|
|
749
|
+
value&.capitalize
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
params = { 'email' => 'TEST@EXAMPLE.COM', 'name' => 'john doe' }
|
|
754
|
+
result = test_class.apply_transformations(params)
|
|
755
|
+
assert_equal 'test@example.com', result['email']
|
|
756
|
+
assert_equal 'John doe', result['name']
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def test_redefining_transformation_overwrites
|
|
760
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
761
|
+
allow :email
|
|
762
|
+
transform :email do |value|
|
|
763
|
+
value&.upcase
|
|
764
|
+
end
|
|
765
|
+
# Redefine
|
|
766
|
+
transform :email do |value|
|
|
767
|
+
value&.downcase
|
|
768
|
+
end
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
params = { 'email' => 'TEST@EXAMPLE.COM' }
|
|
772
|
+
result = test_class.apply_transformations(params)
|
|
773
|
+
# Should use the last definition
|
|
774
|
+
assert_equal 'test@example.com', result['email']
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
def test_deeply_nested_hash_transformations
|
|
778
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
779
|
+
allow :user
|
|
780
|
+
transform :user do |value, metadata|
|
|
781
|
+
if value.is_a?(Hash) && value['profile'].is_a?(Hash)
|
|
782
|
+
value.merge('profile' => value['profile'].merge('processed' => true))
|
|
783
|
+
else
|
|
784
|
+
value
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
params = { 'user' => { 'name' => 'John', 'profile' => { 'age' => 30, 'city' => 'NYC' } } }
|
|
790
|
+
result = test_class.apply_transformations(params)
|
|
791
|
+
expected = { 'user' => { 'name' => 'John', 'profile' => { 'age' => 30, 'city' => 'NYC', 'processed' => true } } }
|
|
792
|
+
assert_equal expected, result
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
def test_transformation_with_nonexistent_attribute
|
|
796
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
797
|
+
allow :email
|
|
798
|
+
transform :nonexistent do |value|
|
|
799
|
+
'transformed'
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
params = { 'email' => 'test@example.com' }
|
|
804
|
+
result = test_class.apply_transformations(params)
|
|
805
|
+
# Should not modify params since transformation key doesn't exist in params
|
|
806
|
+
assert_equal params, result
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def test_transformation_error_handling_detailed
|
|
812
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
813
|
+
allow :email
|
|
814
|
+
transform :email do |value, metadata|
|
|
815
|
+
raise StandardError, "Custom transformation error"
|
|
816
|
+
end
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
params = { 'email' => 'test@example.com' }
|
|
820
|
+
assert_raises(StandardError, "Custom transformation error") do
|
|
821
|
+
test_class.apply_transformations(params)
|
|
822
|
+
end
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def test_apply_transformations_preserves_original_nested_hash
|
|
826
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
827
|
+
allow :user
|
|
828
|
+
transform :user do |value, metadata|
|
|
829
|
+
# This attempts to modify the nested hash in place
|
|
830
|
+
if value.is_a?(Hash) && value['profile'].is_a?(Hash)
|
|
831
|
+
value['profile']['modified'] = true
|
|
832
|
+
value
|
|
833
|
+
else
|
|
834
|
+
value
|
|
835
|
+
end
|
|
836
|
+
end
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
original_profile = { 'age' => 30, 'city' => 'NYC' }
|
|
840
|
+
params = { 'user' => { 'name' => 'John', 'profile' => original_profile } }
|
|
841
|
+
|
|
842
|
+
result = test_class.apply_transformations(params)
|
|
843
|
+
|
|
844
|
+
# The original params hash should not be modified
|
|
845
|
+
refute params['user']['profile'].key?('modified')
|
|
846
|
+
# The result should have the modification
|
|
847
|
+
assert_equal true, result['user']['profile']['modified']
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def test_metadata_validation_in_transformations_detailed
|
|
851
|
+
test_class = Class.new(ActionController::ApplicationParams) do
|
|
852
|
+
allow :role
|
|
853
|
+
metadata :current_user, :ip_address
|
|
854
|
+
transform :role do |value, metadata|
|
|
855
|
+
if metadata[:current_user]&.admin? && metadata[:ip_address] == '127.0.0.1'
|
|
856
|
+
'super_admin'
|
|
857
|
+
elsif metadata[:current_user]&.admin?
|
|
858
|
+
'admin'
|
|
859
|
+
else
|
|
860
|
+
'user'
|
|
861
|
+
end
|
|
862
|
+
end
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
params = { 'role' => 'requested_role' }
|
|
866
|
+
|
|
867
|
+
# Test with admin user and local IP
|
|
868
|
+
mock_user = Minitest::Mock.new
|
|
869
|
+
mock_user.expect :admin?, true
|
|
870
|
+
metadata = { current_user: mock_user, ip_address: '127.0.0.1' }
|
|
871
|
+
result = test_class.apply_transformations(params, metadata)
|
|
872
|
+
assert_equal 'super_admin', result['role']
|
|
873
|
+
mock_user.verify
|
|
874
|
+
|
|
875
|
+
# Test with admin user and remote IP
|
|
876
|
+
mock_user2 = Minitest::Mock.new
|
|
877
|
+
mock_user2.expect :admin?, true
|
|
878
|
+
mock_user2.expect :admin?, true # Called twice in the transformation
|
|
879
|
+
metadata2 = { current_user: mock_user2, ip_address: '192.168.1.1' }
|
|
880
|
+
result2 = test_class.apply_transformations(params, metadata2)
|
|
881
|
+
assert_equal 'admin', result2['role']
|
|
882
|
+
mock_user2.verify
|
|
883
|
+
|
|
884
|
+
# Test with non-admin user
|
|
885
|
+
mock_user3 = Minitest::Mock.new
|
|
886
|
+
mock_user3.expect :admin?, false
|
|
887
|
+
mock_user3.expect :admin?, false # Called twice in the transformation
|
|
888
|
+
metadata3 = { current_user: mock_user3, ip_address: '127.0.0.1' }
|
|
889
|
+
result3 = test_class.apply_transformations(params, metadata3)
|
|
890
|
+
assert_equal 'user', result3['role']
|
|
891
|
+
mock_user3.verify
|
|
892
|
+
end
|
|
893
|
+
end
|