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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +853 -0
  4. data/Rakefile +29 -0
  5. data/app/params/account_params.rb.example +38 -0
  6. data/app/params/application_params.rb +16 -0
  7. data/lib/durable_parameters/adapters/hanami.rb +138 -0
  8. data/lib/durable_parameters/adapters/rage.rb +124 -0
  9. data/lib/durable_parameters/adapters/rails.rb +280 -0
  10. data/lib/durable_parameters/adapters/sinatra.rb +91 -0
  11. data/lib/durable_parameters/core/application_params.rb +334 -0
  12. data/lib/durable_parameters/core/configuration.rb +83 -0
  13. data/lib/durable_parameters/core/forbidden_attributes_protection.rb +48 -0
  14. data/lib/durable_parameters/core/parameters.rb +643 -0
  15. data/lib/durable_parameters/core/params_registry.rb +110 -0
  16. data/lib/durable_parameters/core.rb +15 -0
  17. data/lib/durable_parameters/log_subscriber.rb +34 -0
  18. data/lib/durable_parameters/railtie.rb +65 -0
  19. data/lib/durable_parameters/version.rb +7 -0
  20. data/lib/durable_parameters.rb +41 -0
  21. data/lib/generators/rails/USAGE +12 -0
  22. data/lib/generators/rails/durable_parameters_controller_generator.rb +17 -0
  23. data/lib/generators/rails/templates/controller.rb +94 -0
  24. data/lib/legacy/action_controller/application_params.rb +235 -0
  25. data/lib/legacy/action_controller/parameters.rb +524 -0
  26. data/lib/legacy/action_controller/params_registry.rb +108 -0
  27. data/lib/legacy/active_model/forbidden_attributes_protection.rb +40 -0
  28. data/test/action_controller_required_params_test.rb +36 -0
  29. data/test/action_controller_tainted_params_test.rb +29 -0
  30. data/test/active_model_mass_assignment_taint_protection_test.rb +25 -0
  31. data/test/application_params_array_test.rb +245 -0
  32. data/test/application_params_edge_cases_test.rb +361 -0
  33. data/test/application_params_test.rb +893 -0
  34. data/test/controller_generator_test.rb +31 -0
  35. data/test/core_parameters_test.rb +2376 -0
  36. data/test/durable_parameters_test.rb +115 -0
  37. data/test/enhanced_error_messages_test.rb +120 -0
  38. data/test/gemfiles/Gemfile.rails-3.0.x +14 -0
  39. data/test/gemfiles/Gemfile.rails-3.1.x +14 -0
  40. data/test/gemfiles/Gemfile.rails-3.2.x +14 -0
  41. data/test/log_on_unpermitted_params_test.rb +49 -0
  42. data/test/metadata_validation_test.rb +294 -0
  43. data/test/multi_parameter_attributes_test.rb +38 -0
  44. data/test/parameters_core_methods_test.rb +503 -0
  45. data/test/parameters_integration_test.rb +553 -0
  46. data/test/parameters_permit_test.rb +491 -0
  47. data/test/parameters_require_test.rb +9 -0
  48. data/test/parameters_taint_test.rb +98 -0
  49. data/test/params_registry_concurrency_test.rb +422 -0
  50. data/test/params_registry_test.rb +112 -0
  51. data/test/permit_by_model_test.rb +227 -0
  52. data/test/raise_on_unpermitted_params_test.rb +32 -0
  53. data/test/test_helper.rb +38 -0
  54. data/test/transform_params_edge_cases_test.rb +526 -0
  55. data/test/transformation_test.rb +360 -0
  56. 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