iknow_view_models 2.8.4

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 (92) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +115 -0
  3. data/.gitignore +36 -0
  4. data/.travis.yml +31 -0
  5. data/Appraisals +9 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +19 -0
  9. data/Rakefile +21 -0
  10. data/appveyor.yml +22 -0
  11. data/gemfiles/rails_5_2.gemfile +15 -0
  12. data/gemfiles/rails_6_0_beta.gemfile +15 -0
  13. data/iknow_view_models.gemspec +49 -0
  14. data/lib/iknow_view_models.rb +12 -0
  15. data/lib/iknow_view_models/railtie.rb +8 -0
  16. data/lib/iknow_view_models/version.rb +5 -0
  17. data/lib/view_model.rb +333 -0
  18. data/lib/view_model/access_control.rb +154 -0
  19. data/lib/view_model/access_control/composed.rb +216 -0
  20. data/lib/view_model/access_control/open.rb +13 -0
  21. data/lib/view_model/access_control/read_only.rb +13 -0
  22. data/lib/view_model/access_control/tree.rb +264 -0
  23. data/lib/view_model/access_control_error.rb +10 -0
  24. data/lib/view_model/active_record.rb +383 -0
  25. data/lib/view_model/active_record/association_data.rb +178 -0
  26. data/lib/view_model/active_record/association_manipulation.rb +389 -0
  27. data/lib/view_model/active_record/cache.rb +265 -0
  28. data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
  29. data/lib/view_model/active_record/cloner.rb +113 -0
  30. data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
  31. data/lib/view_model/active_record/controller.rb +77 -0
  32. data/lib/view_model/active_record/controller_base.rb +185 -0
  33. data/lib/view_model/active_record/nested_controller_base.rb +93 -0
  34. data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
  35. data/lib/view_model/active_record/update_context.rb +252 -0
  36. data/lib/view_model/active_record/update_data.rb +749 -0
  37. data/lib/view_model/active_record/update_operation.rb +810 -0
  38. data/lib/view_model/active_record/visitor.rb +77 -0
  39. data/lib/view_model/after_transaction_runner.rb +29 -0
  40. data/lib/view_model/callbacks.rb +219 -0
  41. data/lib/view_model/changes.rb +62 -0
  42. data/lib/view_model/config.rb +29 -0
  43. data/lib/view_model/controller.rb +142 -0
  44. data/lib/view_model/deserialization_error.rb +437 -0
  45. data/lib/view_model/deserialize_context.rb +16 -0
  46. data/lib/view_model/error.rb +191 -0
  47. data/lib/view_model/error_view.rb +35 -0
  48. data/lib/view_model/record.rb +367 -0
  49. data/lib/view_model/record/attribute_data.rb +48 -0
  50. data/lib/view_model/reference.rb +31 -0
  51. data/lib/view_model/references.rb +48 -0
  52. data/lib/view_model/registry.rb +73 -0
  53. data/lib/view_model/schemas.rb +45 -0
  54. data/lib/view_model/serialization_error.rb +10 -0
  55. data/lib/view_model/serialize_context.rb +118 -0
  56. data/lib/view_model/test_helpers.rb +103 -0
  57. data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
  58. data/lib/view_model/traversal_context.rb +126 -0
  59. data/lib/view_model/utils.rb +24 -0
  60. data/lib/view_model/utils/collections.rb +49 -0
  61. data/test/helpers/arvm_test_models.rb +59 -0
  62. data/test/helpers/arvm_test_utilities.rb +187 -0
  63. data/test/helpers/callback_tracer.rb +27 -0
  64. data/test/helpers/controller_test_helpers.rb +270 -0
  65. data/test/helpers/match_enumerator.rb +58 -0
  66. data/test/helpers/query_logging.rb +71 -0
  67. data/test/helpers/test_access_control.rb +56 -0
  68. data/test/helpers/viewmodel_spec_helpers.rb +326 -0
  69. data/test/unit/view_model/access_control_test.rb +769 -0
  70. data/test/unit/view_model/active_record/alias_test.rb +35 -0
  71. data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
  72. data/test/unit/view_model/active_record/cache_test.rb +351 -0
  73. data/test/unit/view_model/active_record/cloner_test.rb +313 -0
  74. data/test/unit/view_model/active_record/controller_test.rb +561 -0
  75. data/test/unit/view_model/active_record/counter_test.rb +80 -0
  76. data/test/unit/view_model/active_record/customization_test.rb +388 -0
  77. data/test/unit/view_model/active_record/has_many_test.rb +957 -0
  78. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
  79. data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
  80. data/test/unit/view_model/active_record/has_one_test.rb +334 -0
  81. data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
  82. data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
  83. data/test/unit/view_model/active_record/poly_test.rb +320 -0
  84. data/test/unit/view_model/active_record/shared_test.rb +285 -0
  85. data/test/unit/view_model/active_record/version_test.rb +121 -0
  86. data/test/unit/view_model/active_record_test.rb +542 -0
  87. data/test/unit/view_model/callbacks_test.rb +582 -0
  88. data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
  89. data/test/unit/view_model/record_test.rb +524 -0
  90. data/test/unit/view_model/traversal_context_test.rb +371 -0
  91. data/test/unit/view_model_test.rb +62 -0
  92. metadata +490 -0
@@ -0,0 +1,326 @@
1
+ require 'view_model'
2
+ require 'view_model/test_helpers'
3
+
4
+ require 'minitest/unit'
5
+ require 'minitest/hooks'
6
+
7
+ module ViewModelSpecHelpers
8
+ module Base
9
+ extend ActiveSupport::Concern
10
+ include Minitest::Hooks # not a concern, can I do this?
11
+
12
+ included do
13
+ around do |&block|
14
+ @builders = []
15
+ super(&block)
16
+ @builders.each do |b|
17
+ b.teardown
18
+ end
19
+ end
20
+ end
21
+
22
+ def namespace
23
+ Object
24
+ end
25
+
26
+ def viewmodel_base
27
+ ViewModelBase
28
+ end
29
+
30
+ def model_base
31
+ ApplicationRecord
32
+ end
33
+
34
+ def model_class
35
+ viewmodel_class.model_class
36
+ end
37
+
38
+ def child_model_class
39
+ child_viewmodel_class.model_class
40
+ end
41
+
42
+ def view_name
43
+ viewmodel_class.view_name
44
+ end
45
+
46
+ def child_view_name
47
+ child_viewmodel_class.view_name
48
+ end
49
+
50
+ def viewmodel_class
51
+ @viewmodel_class ||= define_viewmodel_class(
52
+ :Model,
53
+ spec: model_attributes,
54
+ namespace: namespace,
55
+ viewmodel_base: viewmodel_base,
56
+ model_base: model_base).tap { |klass| yield(klass) if block_given? }
57
+ end
58
+
59
+ def child_viewmodel_class
60
+ @child_viewmodel_class ||= define_viewmodel_class(
61
+ :Child,
62
+ spec: child_attributes,
63
+ namespace: namespace,
64
+ viewmodel_base: viewmodel_base,
65
+ model_base: model_base).tap { |klass| yield(klass) if block_given? }
66
+ end
67
+
68
+ def create_viewmodel!
69
+ viewmodel_class.new(create_model!)
70
+ end
71
+
72
+ def create_model!
73
+ new_model.tap { |m| m.save! }
74
+ end
75
+
76
+ def model_attributes
77
+ ViewModel::TestHelpers::ARVMBuilder::Spec.new(
78
+ schema: ->(t) { t.string :name },
79
+ model: ->(m) {},
80
+ viewmodel: ->(v) { attribute :name }
81
+ )
82
+ end
83
+
84
+ def child_attributes
85
+ ViewModel::TestHelpers::ARVMBuilder::Spec.new(
86
+ schema: ->(t) { t.string :name },
87
+ model: ->(m) {},
88
+ viewmodel: ->(v) { attribute :name }
89
+ )
90
+ end
91
+
92
+ def define_viewmodel_class(name, **args, &block)
93
+ builder = ViewModel::TestHelpers::ARVMBuilder.new(name, **args, &block)
94
+ @builders << builder
95
+ builder.viewmodel
96
+ end
97
+
98
+ def subject_association
99
+ raise RuntimeError.new('Test model does not have a child association')
100
+ end
101
+
102
+ def subject_association_name
103
+ subject_association.association_name
104
+ end
105
+ end
106
+
107
+ module Single
108
+ extend ActiveSupport::Concern
109
+ include ViewModelSpecHelpers::Base
110
+ end
111
+
112
+ module ParentAndBelongsToChild
113
+ extend ActiveSupport::Concern
114
+ include ViewModelSpecHelpers::Base
115
+
116
+ def model_attributes
117
+ super.merge(schema: ->(t) { t.references :child, foreign_key: true },
118
+ model: ->(m) { belongs_to :child, inverse_of: :model, dependent: :destroy },
119
+ viewmodel: ->(v) { association :child })
120
+ end
121
+
122
+ def child_attributes
123
+ super.merge(model: ->(m) { has_one :model, inverse_of: :child })
124
+ end
125
+
126
+ # parent depends on child, ensure it's touched first
127
+ def viewmodel_class
128
+ child_viewmodel_class
129
+ super
130
+ end
131
+
132
+ def subject_association
133
+ viewmodel_class._association_data('child')
134
+ end
135
+ end
136
+
137
+ module List
138
+ extend ActiveSupport::Concern
139
+ include ViewModelSpecHelpers::Base
140
+
141
+ def model_attributes
142
+ super.merge(schema: ->(t) { t.integer :next_id },
143
+ model: ->(m) {
144
+ belongs_to :next, class_name: self.name, inverse_of: :previous, dependent: :destroy
145
+ has_one :previous, class_name: self.name, foreign_key: :next_id, inverse_of: :next
146
+ },
147
+ viewmodel: ->(v) { association :next })
148
+ end
149
+
150
+ def subject_association
151
+ viewmodel_class._association_data('next')
152
+ end
153
+ end
154
+
155
+ module ParentAndHasOneChild
156
+ extend ActiveSupport::Concern
157
+ include ViewModelSpecHelpers::Base
158
+
159
+ def model_attributes
160
+ super.merge(
161
+ model: ->(m) { has_one :child, inverse_of: :model, dependent: :destroy },
162
+ viewmodel: ->(v) { association :child }
163
+ )
164
+ end
165
+
166
+ def child_attributes
167
+ super.merge(
168
+ schema: ->(t) { t.references :model, foreign_key: true },
169
+ model: ->(m) { belongs_to :model, inverse_of: :child }
170
+ )
171
+ end
172
+
173
+ # child depends on parent, ensure it's touched first
174
+ def child_viewmodel_class
175
+ viewmodel_class
176
+ super
177
+ end
178
+
179
+ def subject_association
180
+ viewmodel_class._association_data('child')
181
+ end
182
+ end
183
+
184
+ module ParentAndHasManyChildren
185
+ extend ActiveSupport::Concern
186
+ include ViewModelSpecHelpers::Base
187
+
188
+ def model_attributes
189
+ super.merge(
190
+ model: ->(m) { has_many :children, inverse_of: :model, dependent: :destroy },
191
+ viewmodel: ->(v) { association :children }
192
+ )
193
+ end
194
+
195
+ def child_attributes
196
+ super.merge(
197
+ schema: ->(t) { t.references :model, foreign_key: true },
198
+ model: ->(m) { belongs_to :model, inverse_of: :children }
199
+ )
200
+ end
201
+
202
+ # child depends on parent, ensure it's touched first
203
+ def child_viewmodel_class
204
+ viewmodel_class
205
+ super
206
+ end
207
+
208
+ def subject_association
209
+ viewmodel_class._association_data('children')
210
+ end
211
+ end
212
+
213
+ module ParentAndOrderedChildren
214
+ extend ActiveSupport::Concern
215
+ include ViewModelSpecHelpers::Base
216
+
217
+ def model_attributes
218
+ super.merge(
219
+ model: ->(m) { has_many :children, inverse_of: :model, dependent: :destroy },
220
+ viewmodel: ->(v) { association :children },
221
+ )
222
+ end
223
+
224
+ def child_attributes
225
+ super.merge(
226
+ schema: ->(t) { t.references :model, foreign_key: true; t.float :position, null: false },
227
+ model: ->(m) { belongs_to :model, inverse_of: :children },
228
+ viewmodel: ->(v) { acts_as_list :position },
229
+ )
230
+ end
231
+
232
+ def child_viewmodel_class
233
+ # child depends on parent, ensure it's touched first
234
+ viewmodel_class
235
+
236
+ # Add a deferrable unique position constraiont
237
+ super do |klass|
238
+ model = klass.model_class
239
+ table = model.table_name
240
+ model.connection.execute <<-SQL
241
+ ALTER TABLE #{table} ADD CONSTRAINT #{table}_unique_on_model_and_position UNIQUE(model_id, position) DEFERRABLE INITIALLY DEFERRED
242
+ SQL
243
+ end
244
+ end
245
+
246
+ def subject_association
247
+ viewmodel_class._association_data('children')
248
+ end
249
+ end
250
+
251
+ module ParentAndSharedChild
252
+ extend ActiveSupport::Concern
253
+ include ViewModelSpecHelpers::Base
254
+
255
+ def model_attributes
256
+ super.merge(
257
+ schema: ->(t) { t.references :child, foreign_key: true },
258
+ model: ->(m) { belongs_to :child, inverse_of: :model, dependent: :destroy },
259
+ viewmodel: ->(v) { association :child, shared: true }
260
+ )
261
+ end
262
+
263
+ def child_attributes
264
+ super.merge(
265
+ model: ->(m) { has_one :model, inverse_of: :child }
266
+ )
267
+ end
268
+
269
+ # parent depends on child, ensure it's touched first
270
+ def viewmodel_class
271
+ child_viewmodel_class
272
+ super
273
+ end
274
+
275
+ def subject_association
276
+ viewmodel_class._association_data('child')
277
+ end
278
+ end
279
+
280
+ module ParentAndHasManyThroughChildren
281
+ extend ActiveSupport::Concern
282
+ include ViewModelSpecHelpers::Base
283
+
284
+ def model_attributes
285
+ super.merge(
286
+ model: ->(m) { has_many :model_children, inverse_of: :model, dependent: :destroy },
287
+ viewmodel: ->(v) { association :children, shared: true, through: :model_children, through_order_attr: :position }
288
+ )
289
+ end
290
+
291
+ def child_attributes
292
+ super.merge(
293
+ model: ->(m) { has_many :model_children, inverse_of: :child, dependent: :destroy }
294
+ )
295
+ end
296
+
297
+ def join_model_class
298
+ # depends on parent and child
299
+ viewmodel_class
300
+ child_viewmodel_class
301
+
302
+ @join_model_class ||=
303
+ begin
304
+ define_viewmodel_class(:ModelChild) do
305
+ define_schema do |t|
306
+ t.references :model, foreign_key: true
307
+ t.references :child, foreign_key: true
308
+ t.float :position
309
+ end
310
+
311
+ define_model do
312
+ belongs_to :model
313
+ belongs_to :child
314
+ end
315
+
316
+ no_viewmodel
317
+ end
318
+ ModelChild
319
+ end
320
+ end
321
+
322
+ def subject_association
323
+ viewmodel_class._association_data('children')
324
+ end
325
+ end
326
+ end
@@ -0,0 +1,769 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../helpers/arvm_test_utilities.rb"
4
+ require_relative "../../helpers/arvm_test_models.rb"
5
+ require_relative '../../helpers/viewmodel_spec_helpers.rb'
6
+
7
+ require "minitest/autorun"
8
+ require 'minitest/unit'
9
+
10
+ require 'rspec/expectations/minitest_integration'
11
+
12
+ require "view_model/active_record"
13
+
14
+ class ViewModel::AccessControlTest < ActiveSupport::TestCase
15
+ include ARVMTestUtilities
16
+
17
+ class ComposedTest < ActiveSupport::TestCase
18
+ include ARVMTestUtilities
19
+
20
+ def before_all
21
+ super
22
+
23
+ build_viewmodel(:List) do
24
+ define_schema do |t|
25
+ t.string :car
26
+ t.integer :cdr_id
27
+ end
28
+
29
+ define_model do
30
+ belongs_to :cdr, class_name: :List, dependent: :destroy
31
+ end
32
+
33
+ define_viewmodel do
34
+ attribute :car
35
+ association :cdr
36
+
37
+ def self.new_serialize_context(**args)
38
+ super(access_control: TestAccessControl.new, **args)
39
+ end
40
+
41
+ def self.new_deserialize_context(**args)
42
+ super(access_control: TestAccessControl.new, **args)
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ def setup
49
+ ComposedTest.const_set(:TestAccessControl, Class.new(ViewModel::AccessControl::Composed))
50
+ enable_logging!
51
+ end
52
+
53
+ def teardown
54
+ ComposedTest.send(:remove_const, :TestAccessControl)
55
+ end
56
+
57
+ def test_visible_if
58
+ TestAccessControl.visible_if!("car is visible1") do
59
+ view.car == "visible1"
60
+ end
61
+
62
+ TestAccessControl.visible_if!("car is visible2") do
63
+ view.car == "visible2"
64
+ end
65
+
66
+ assert_serializes(ListView, List.create!(car: "visible1"))
67
+ assert_serializes(ListView, List.create!(car: "visible2"))
68
+ ex = refute_serializes(ListView, List.create!(car: "bad"), /none of the possible/)
69
+ assert_equal(2, ex.reasons.count)
70
+ end
71
+
72
+ def test_visible_unless
73
+ TestAccessControl.visible_if!("always") { true }
74
+
75
+ TestAccessControl.visible_unless!("car is invisible") do
76
+ view.car == "invisible"
77
+ end
78
+
79
+ assert_serializes(ListView, List.create!(car: "ok"))
80
+ refute_serializes(ListView, List.create!(car: "invisible"), /not permitted.*car is invisible/)
81
+ end
82
+
83
+ def test_editable_if
84
+ TestAccessControl.visible_if!("always") { true }
85
+
86
+ TestAccessControl.editable_if!("car is editable1") do
87
+ view.car == "editable1"
88
+ end
89
+
90
+ TestAccessControl.editable_if!("car is editable2") do
91
+ view.car == "editable2"
92
+ end
93
+
94
+ assert_deserializes(ListView, List.create!(car: "editable1")) { |v, _| v["car"] = "unchecked" }
95
+ assert_deserializes(ListView, List.create!(car: "editable2")) { |v, _| v["car"] = "unchecked" }
96
+ assert_deserializes(ListView, List.create!(car: "forbidden")) { |v, _| v["car"] = "forbidden" } # no change so permitted
97
+ refute_deserializes(ListView, List.create!(car: "forbidden"), /none of the possible/) { |v, _| v["car"] = "unchecked" }
98
+ end
99
+
100
+ def test_editable_unless
101
+ TestAccessControl.visible_if!("always") { true }
102
+ TestAccessControl.editable_if!("always") { true }
103
+
104
+ TestAccessControl.editable_unless!("car is uneditable") do
105
+ view.car == "uneditable"
106
+ end
107
+
108
+ assert_deserializes(ListView, List.create!(car: "ok")) { |v, _| v["car"] = "unchecked" }
109
+ assert_deserializes(ListView, List.create!(car: "uneditable")) { |v, _| v["car"] = "uneditable" } # no change so permitted
110
+ refute_deserializes(ListView, List.create!(car: "uneditable"), /car is uneditable/) { |v, _| v["car"] = "unchecked" }
111
+ end
112
+
113
+ def test_edit_valid_if
114
+ TestAccessControl.visible_if!("always") { true }
115
+
116
+ TestAccessControl.edit_valid_if!("car is validedit") do
117
+ view.car == "validedit"
118
+ end
119
+
120
+ assert_deserializes(ListView, List.create!(car: "unchecked")) { |v, _| v["car"] = "validedit" }
121
+ assert_deserializes(ListView, List.create!(car: "unmodified")) { |v, _| v["car"] = "unmodified" } # no change so permitted
122
+ refute_deserializes(ListView, List.create!(car: "unchecked"), /none of the possible/) { |v, _| v["car"] = "bad" }
123
+ end
124
+
125
+ def test_edit_valid_unless
126
+ TestAccessControl.visible_if!("always") { true }
127
+ TestAccessControl.edit_valid_if!("always") { true }
128
+ TestAccessControl.edit_valid_unless!("car is invalidedit") do
129
+ view.car == "invalidedit"
130
+ end
131
+
132
+ assert_deserializes(ListView, List.create!(car: "unchecked")) { |v, _| v["car"] = "ok" }
133
+ assert_deserializes(ListView, List.create!(car: "invalidedit")) { |v, _| v["car"] = "invalidedit" }
134
+ refute_deserializes(ListView, List.create!(car: "unchecked"), /car is invalidedit/) { |v, _| v["car"] = "invalidedit" }
135
+ end
136
+
137
+ def test_editable_and_edit_valid
138
+ TestAccessControl.visible_if!("always") { true }
139
+
140
+ TestAccessControl.editable_if!("original car permits") do
141
+ view.car == "permitoriginal"
142
+ end
143
+
144
+ TestAccessControl.edit_valid_if!("resulting car permits") do
145
+ view.car == "permitresult"
146
+ end
147
+
148
+ # at least one valid
149
+ assert_deserializes(ListView, List.create!(car: "permitoriginal")) { |v, _| v["car"] = "permitresult" }
150
+ assert_deserializes(ListView, List.create!(car: "badoriginal")) { |v, _| v["car"] = "permitresult" }
151
+ assert_deserializes(ListView, List.create!(car: "permitoriginal")) { |v, _| v["car"] = "badresult" }
152
+
153
+ # no valid
154
+ ex = refute_deserializes(ListView, List.create!(car: "badoriginal"), /none of the possible/) { |v, _| v["car"] = "badresult" }
155
+
156
+ assert_equal(2, ex.reasons.count)
157
+ end
158
+
159
+ def test_inheritance
160
+ child_access_control = Class.new(ViewModel::AccessControl::Composed)
161
+ child_access_control.include_from(TestAccessControl)
162
+
163
+ TestAccessControl.visible_if!("car is ancestor") { view.car == "ancestor" }
164
+ child_access_control.visible_if!("car is descendent") { view.car == "descendent" }
165
+
166
+ s_ctx = ListView.new_serialize_context(access_control: child_access_control.new)
167
+
168
+ assert_serializes(ListView, List.create!(car: "ancestor"), serialize_context: s_ctx)
169
+ assert_serializes(ListView, List.create!(car: "descendent"), serialize_context: s_ctx)
170
+ ex = refute_serializes(ListView, List.create!(car: "foreigner"), serialize_context: s_ctx)
171
+ assert_equal(2, ex.reasons.count)
172
+ end
173
+ end
174
+
175
+ class TreeTest < ActiveSupport::TestCase
176
+ include ARVMTestUtilities
177
+
178
+ def before_all
179
+ super
180
+
181
+ # Tree1 is a root, which owns Tree2.
182
+ build_viewmodel(:Tree1) do
183
+ define_schema do |t|
184
+ t.string :val
185
+ t.integer :tree2_id
186
+ end
187
+
188
+ define_model do
189
+ belongs_to :tree2, class_name: :Tree2, dependent: :destroy
190
+ end
191
+
192
+ define_viewmodel do
193
+ attribute :val
194
+ association :tree2
195
+
196
+ def self.new_serialize_context(**args)
197
+ super(access_control: TestAccessControl.new, **args)
198
+ end
199
+
200
+ def self.new_deserialize_context(**args)
201
+ super(access_control: TestAccessControl.new, **args)
202
+ end
203
+ end
204
+ end
205
+
206
+ build_viewmodel(:Tree2) do
207
+ define_schema do |t|
208
+ t.string :val
209
+ t.integer :tree1_id
210
+ end
211
+
212
+ define_model do
213
+ belongs_to :tree1, class_name: :Tree1, dependent: :destroy
214
+ end
215
+
216
+ define_viewmodel do
217
+ attribute :val
218
+ association :tree1, shared: true, optional: false
219
+ end
220
+ end
221
+ end
222
+
223
+ def setup
224
+ TreeTest.const_set(:TestAccessControl, Class.new(ViewModel::AccessControl::Tree))
225
+ enable_logging!
226
+ end
227
+
228
+ def teardown
229
+ TreeTest.send(:remove_const, :TestAccessControl)
230
+ end
231
+
232
+ def make_tree(*vals)
233
+ tree = vals.each_slice(2).reverse_each.inject(nil) do |rest, (t1, t2)|
234
+ Tree1.new(val: t1, tree2: Tree2.new(val: t2, tree1: rest))
235
+ end
236
+ tree.save!
237
+ tree
238
+ end
239
+
240
+ def dig_tree(root, refs, attr, *rest)
241
+ raise "Root missing attribute '#{attr}'" unless root.has_key?(attr)
242
+
243
+ child = root[attr]
244
+
245
+ if (child_ref = child["_ref"])
246
+ child = refs[child_ref]
247
+ end
248
+
249
+ if rest.empty?
250
+ child
251
+ else
252
+ dig_tree(child, refs, *rest)
253
+ end
254
+ end
255
+
256
+ def test_visibility_from_root
257
+ TestAccessControl.view "Tree1" do
258
+ visible_if!("true") { true }
259
+
260
+ root_children_visible_if!("root children visible") do
261
+ view.val == "rule:visible_children"
262
+ end
263
+ end
264
+
265
+ refute_serializes(Tree1View, make_tree("arbitrary parent", "invisible child"))
266
+ assert_serializes(Tree1View, make_tree("rule:visible_children", "visible child"))
267
+
268
+ # nested root
269
+ refute_serializes(Tree1View, make_tree("rule:visible_children", "visible child", "arbitrary parent", "invisible child"))
270
+ assert_serializes(Tree1View, make_tree("rule:visible_children", "visible child", "rule:visible_children", "visible child"))
271
+ end
272
+
273
+ def test_visibility_veto_from_root
274
+ TestAccessControl.view "Tree1" do
275
+ root_children_visible_unless!("root children invisible") do
276
+ view.val == "rule:invisible_children"
277
+ end
278
+ end
279
+
280
+ TestAccessControl.always do
281
+ visible_if!("true") { true }
282
+ end
283
+
284
+ assert_serializes(Tree1View, make_tree("arbitrary parent", "visible child"))
285
+ refute_serializes(Tree1View, make_tree("rule:invisible_children", "invisible child"))
286
+
287
+ # nested root
288
+ assert_serializes(Tree1View, make_tree("arbitrary parent", "visible child", "arbitrary nested parent", "visible child"))
289
+ refute_serializes(Tree1View, make_tree("arbitrary parent", "visible child", "rule:invisible_children", "invisible child"))
290
+ end
291
+
292
+ def test_editability_from_root
293
+ TestAccessControl.always do
294
+ visible_if!("always") { true }
295
+ end
296
+
297
+ TestAccessControl.view "Tree1" do
298
+ editable_if!("true") { true }
299
+
300
+ root_children_editable_if!("root children editable") do
301
+ view.val == "rule:editable_children"
302
+ end
303
+ end
304
+
305
+ refute_deserializes(Tree1View, make_tree("arbitrary parent", "uneditable child")) { |v, r|
306
+ dig_tree(v, r, "tree2")["val"] = "change"
307
+ }
308
+
309
+ assert_deserializes(Tree1View, make_tree("rule:editable_children", "editable child")) { |v, r|
310
+ dig_tree(v, r, "tree2")["val"] = "change"
311
+ }
312
+
313
+ # nested root
314
+ refute_deserializes(Tree1View, make_tree("rule:editable_children", "editable child", "arbitrary parent", "uneditable child")) { |v, r|
315
+ dig_tree(v, r, "tree2", "tree1", "tree2")["val"] = "change"
316
+ }
317
+
318
+ assert_deserializes(Tree1View, make_tree("arbitrary parent", "uneditable child", "rule:editable_children", "editable child")) { |v, r|
319
+ dig_tree(v, r, "tree2", "tree1", "tree2")["val"] = "change"
320
+ }
321
+ end
322
+
323
+ def test_editability_veto_from_root
324
+ TestAccessControl.always do
325
+ visible_if!("always") { true }
326
+ editable_if!("always") { true }
327
+ end
328
+
329
+ TestAccessControl.view "Tree1" do
330
+ root_children_editable_unless!("root children uneditable") do
331
+ view.val == "rule:uneditable_children"
332
+ end
333
+ end
334
+
335
+ refute_deserializes(Tree1View, make_tree("rule:uneditable_children", "uneditable child")) { |v, r|
336
+ dig_tree(v, r, "tree2")["val"] = "change"
337
+ }
338
+
339
+ assert_deserializes(Tree1View, make_tree("arbitrary parent", "editable child")) { |v, r|
340
+ dig_tree(v, r, "tree2")["val"] = "change"
341
+ }
342
+
343
+ # nested root
344
+ refute_deserializes(Tree1View, make_tree("arbitrary parent", "editable child", "rule:uneditable_children", "uneditable child")) { |v, r|
345
+ dig_tree(v, r, "tree2", "tree1", "tree2")["val"] = "change"
346
+ }
347
+
348
+ assert_deserializes(Tree1View, make_tree("rule:uneditable_children", "uneditable child", "arbitrary parent", "editable child")) { |v, r|
349
+ dig_tree(v, r, "tree2", "tree1", "tree2")["val"] = "change"
350
+ }
351
+ end
352
+
353
+ def test_type_independence
354
+ TestAccessControl.view "Tree1" do
355
+ visible_if!("tree1 visible") do
356
+ view.val == "tree1visible"
357
+ end
358
+ end
359
+
360
+ TestAccessControl.view "Tree2" do
361
+ visible_if!("tree2 visible") do
362
+ view.val == "tree2visible"
363
+ end
364
+ end
365
+
366
+ refute_serializes(Tree1View, make_tree("tree1invisible", "tree2visible"))
367
+ assert_serializes(Tree1View, make_tree("tree1visible", "tree2visible"))
368
+ refute_serializes(Tree1View, make_tree("tree1visible", "tree2invisible"))
369
+ end
370
+
371
+ def test_visibility_always_composition
372
+ TestAccessControl.view "Tree1" do
373
+ visible_if!("tree1 visible") do
374
+ view.val == "tree1visible"
375
+ end
376
+ end
377
+
378
+ TestAccessControl.always do
379
+ visible_if!("tree2 visible") do
380
+ view.val == "alwaysvisible"
381
+ end
382
+ end
383
+
384
+ refute_serializes(Tree1View, Tree1.create(val: "bad"))
385
+ assert_serializes(Tree1View, Tree1.create(val: "tree1visible"))
386
+ assert_serializes(Tree1View, Tree1.create(val: "alwaysvisible"))
387
+ end
388
+
389
+ def test_editability_always_composition
390
+ TestAccessControl.view "Tree1" do
391
+ editable_if!("editable1") { view.val == "editable1" }
392
+ edit_valid_if!("editvalid1") { view.val == "editvalid1" }
393
+ end
394
+
395
+ TestAccessControl.always do
396
+ editable_if!("editable2") { view.val == "editable2" }
397
+ edit_valid_if!("editvalid2") { view.val == "editvalid2" }
398
+
399
+ visible_if!("always") { true }
400
+ end
401
+
402
+ refute_deserializes(Tree1View, Tree1.create!(val: "bad")) { |v, _| v["val"] = "alsobad" }
403
+
404
+ assert_deserializes(Tree1View, Tree1.create!(val: "editable1")) { |v, _| v["val"] = "unchecked" }
405
+ assert_deserializes(Tree1View, Tree1.create!(val: "editable2")) { |v, _| v["val"] = "unchecked" }
406
+
407
+ assert_deserializes(Tree1View, Tree1.create!(val: "unchecked")) { |v, _| v["val"] = "editvalid1" }
408
+ assert_deserializes(Tree1View, Tree1.create!(val: "unchecked")) { |v, _| v["val"] = "editvalid2" }
409
+ end
410
+
411
+ def test_ancestry
412
+ TestAccessControl.view "Tree1" do
413
+ visible_if!("parent tree1") { view.val == "parenttree1" }
414
+ end
415
+
416
+ TestAccessControl.always do
417
+ visible_if!("parent always") { view.val == "parentalways" }
418
+ end
419
+
420
+ # Child must be set up after parent is fully defined
421
+ child_access_control = Class.new(ViewModel::AccessControl::Tree)
422
+ child_access_control.include_from(TestAccessControl)
423
+
424
+ child_access_control.view "Tree1" do
425
+ visible_if!("child tree1") { view.val == "childtree1" }
426
+ end
427
+
428
+ child_access_control.always do
429
+ visible_if!("child always") { view.val == "childalways" }
430
+ end
431
+
432
+ s_ctx = Tree1View.new_serialize_context(access_control: child_access_control.new)
433
+
434
+ refute_serializes(Tree1View, Tree1.create!(val: "bad"), serialize_context: s_ctx)
435
+
436
+ assert_serializes(Tree1View, Tree1.create!(val: "parenttree1"), serialize_context: s_ctx)
437
+ assert_serializes(Tree1View, Tree1.create!(val: "parentalways"), serialize_context: s_ctx)
438
+ assert_serializes(Tree1View, Tree1.create!(val: "childtree1"), serialize_context: s_ctx)
439
+ assert_serializes(Tree1View, Tree1.create!(val: "childalways"), serialize_context: s_ctx)
440
+ end
441
+ end
442
+
443
+ # Integration-test access control, callbacks and viewmodel change tracking: do
444
+ # the edit checks get called as expected with the correct changes?
445
+ class ChangeTrackingTest < ActiveSupport::TestCase
446
+ include ARVMTestUtilities
447
+ include ViewModelSpecHelpers::List
448
+ extend Minitest::Spec::DSL
449
+
450
+ def assert_changes_match(changes, new, deleted, children, attributes, associations)
451
+ assert_equal(
452
+ changes,
453
+ ViewModel::Changes.new(
454
+ new: new,
455
+ deleted: deleted,
456
+ changed_children: children,
457
+ changed_attributes: attributes,
458
+ changed_associations: associations))
459
+ end
460
+
461
+ describe 'with parent and points-to child test models' do
462
+ include ViewModelSpecHelpers::ParentAndBelongsToChild
463
+
464
+ def new_model
465
+ model_class.new(name: 'a')
466
+ end
467
+
468
+ def new_model_with_child
469
+ model_class.new(name: 'a', child: child_model_class.new(name: 'b'))
470
+ end
471
+
472
+ it 'records a created model' do
473
+ view = {
474
+ '_type' => view_name,
475
+ 'name' => 'a',
476
+ }
477
+
478
+ ctx = viewmodel_class.new_deserialize_context
479
+ vm = viewmodel_class.deserialize_from_view(view, deserialize_context: ctx)
480
+
481
+ vm_changes = ctx.valid_edit_changes(vm.to_reference)
482
+ assert_changes_match(vm_changes, true, false, false, ['name'], [])
483
+ end
484
+
485
+ it 'records a destroyed model' do
486
+ vm = create_viewmodel!
487
+
488
+ ctx = viewmodel_class.new_deserialize_context
489
+ vm.destroy!(deserialize_context: ctx)
490
+
491
+ vm_changes = ctx.valid_edit_changes(vm.to_reference)
492
+ assert_changes_match(vm_changes, false, true, false, [], [])
493
+ end
494
+
495
+ it 'records a change to an attribute' do
496
+ vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
497
+ view['name'] = nil
498
+ end
499
+
500
+ vm_changes = ctx.valid_edit_changes(vm.to_reference)
501
+ assert_changes_match(vm_changes, false, false, false, ['name'], [])
502
+ end
503
+
504
+ it 'records a new child' do
505
+ vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
506
+ view['child'] = { '_type' => child_view_name, 'name' => 'b' }
507
+ end
508
+
509
+ vm_changes = ctx.valid_edit_changes(vm.to_reference)
510
+ assert_changes_match(vm_changes, false, false, true, [], ['child'])
511
+
512
+ c_changes = ctx.valid_edit_changes(vm.child.to_reference)
513
+ assert_changes_match(c_changes, true, false, false, ['name'], [])
514
+ end
515
+
516
+ it 'records a replaced child' do
517
+ m = new_model_with_child.tap(&:save!)
518
+ old_child = m.child
519
+
520
+ vm, ctx = alter_by_view!(viewmodel_class, m) do |view, _refs|
521
+ view['child'] = { '_type' => child_view_name, 'name' => 'c' }
522
+ end
523
+
524
+ vm_changes = ctx.valid_edit_changes(vm.to_reference)
525
+ assert_changes_match(vm_changes, false, false, true, [], ['child'])
526
+
527
+ c_changes = ctx.valid_edit_changes(vm.child.to_reference)
528
+ assert_changes_match(c_changes, true, false, false, ['name'], [])
529
+
530
+ oc_changes = ctx.valid_edit_changes(
531
+ ViewModel::Reference.new(child_viewmodel_class, old_child.id))
532
+ assert_changes_match(oc_changes, false, true, false, [], [])
533
+ end
534
+
535
+ it 'records an edited child' do
536
+ m = new_model_with_child.tap(&:save!)
537
+
538
+ vm, ctx = alter_by_view!(viewmodel_class, m) do |view, _refs|
539
+ view['child']['name'] = 'c'
540
+ end
541
+
542
+ # The parent node itself wasn't changed, so must not have been
543
+ # valid_edit checked
544
+ refute(ctx.was_edited?(vm.to_reference))
545
+ assert_changes_match(vm.previous_changes, false, false, true, [], [])
546
+
547
+ c_changes = ctx.valid_edit_changes(vm.child.to_reference)
548
+ assert_changes_match(c_changes, false, false, false, ['name'], [])
549
+ end
550
+
551
+ it 'records a deleted child' do
552
+ m = new_model_with_child.tap(&:save!)
553
+ old_child = m.child
554
+
555
+ vm, ctx = alter_by_view!(viewmodel_class, m) do |view, _refs|
556
+ view['child'] = nil
557
+ end
558
+
559
+ vm_changes = ctx.valid_edit_changes(vm.to_reference)
560
+ assert_changes_match(vm_changes, false, false, true, [], ['child'])
561
+
562
+ oc_changes = ctx.valid_edit_changes(
563
+ ViewModel::Reference.new(child_viewmodel_class, old_child.id))
564
+ assert_changes_match(oc_changes, false, true, false, [], [])
565
+ end
566
+ end
567
+
568
+ describe 'with parent and pointed-to child test models' do
569
+ include ViewModelSpecHelpers::ParentAndOrderedChildren
570
+
571
+ def new_model
572
+ model_class.new(
573
+ name: 'a',
574
+ children: [child_model_class.new(name: 'x', position: 1),
575
+ child_model_class.new(name: 'y', position: 2)])
576
+ end
577
+
578
+ it 'records new children' do
579
+ vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
580
+ view['children'].concat(
581
+ [
582
+ { '_type' => child_view_name, 'name' => 'b' },
583
+ { '_type' => child_view_name, 'name' => 'c' },
584
+ ])
585
+ end
586
+
587
+ vm_changes = ctx.valid_edit_changes(vm.to_reference)
588
+ assert_changes_match(vm_changes, false, false, true, [], ['children'])
589
+
590
+ new_children, existing_children = vm.children.partition do |c|
591
+ c.name < 'm'
592
+ end
593
+
594
+ new_children.each do |c|
595
+ c_changes = ctx.valid_edit_changes(c.to_reference)
596
+ assert_changes_match(c_changes, true, false, false, ['name'], [])
597
+ end
598
+
599
+ existing_children.each do |c|
600
+ refute(ctx.was_edited?(c.to_reference))
601
+ end
602
+ end
603
+
604
+ it 'records replaced children' do
605
+ m = create_model!
606
+ replaced_child = m.children.last
607
+
608
+ vm, ctx = alter_by_view!(viewmodel_class, m) do |view, _refs|
609
+ view['children'].pop
610
+ view['children'] << { '_type' => child_view_name, 'name' => 'b' }
611
+ end
612
+
613
+ refute(vm.children.include?(replaced_child))
614
+
615
+ vm_changes = ctx.valid_edit_changes(vm.to_reference)
616
+ assert_changes_match(vm_changes, false, false, true, [], ['children'])
617
+
618
+ new_child = vm.children.detect { |c| c.name == 'b' }
619
+ c_changes = ctx.valid_edit_changes(new_child.to_reference)
620
+ assert_changes_match(c_changes, true, false, false, ['name'], [])
621
+
622
+ oc_changes = ctx.valid_edit_changes(
623
+ ViewModel::Reference.new(child_viewmodel_class, replaced_child.id))
624
+ assert_changes_match(oc_changes, false, true, false, [], [])
625
+ end
626
+
627
+ it 'records reordered children' do
628
+ vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
629
+ view['children'].reverse!
630
+ end
631
+
632
+ vm_changes = ctx.valid_edit_changes(vm.to_reference)
633
+ assert_changes_match(vm_changes, false, false, false, [], ['children'])
634
+
635
+ vm.children.each do |c|
636
+ refute(ctx.was_edited?(c.to_reference))
637
+ end
638
+ end
639
+ end
640
+
641
+ describe 'with parent and shared child test models' do
642
+ include ViewModelSpecHelpers::ParentAndSharedChild
643
+
644
+ def new_model
645
+ model_class.new(name: 'a', child: child_model_class.new(name: 'z'))
646
+ end
647
+
648
+ it 'records an change to child without a tree change' do
649
+ vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, refs|
650
+ view['child'] = { '_ref' => 'cref' }
651
+ refs.clear['cref'] = { '_type' => child_view_name, 'name' => 'b' }
652
+ end
653
+
654
+ vm_changes = ctx.valid_edit_changes(vm.to_reference)
655
+ assert_changes_match(vm_changes, false, false, false, [], ['child'])
656
+
657
+ c_changes = ctx.valid_edit_changes(vm.child.to_reference)
658
+ assert_changes_match(c_changes, true, false, false, ['name'], [])
659
+ end
660
+
661
+ it 'records an edited child without a tree change' do
662
+ vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |_view, refs|
663
+ refs.values.first.merge!('name' => 'b')
664
+ end
665
+
666
+ refute(ctx.was_edited?(vm.to_reference))
667
+ assert_changes_match(vm.previous_changes, false, false, false, [], [])
668
+
669
+ c_changes = ctx.valid_edit_changes(vm.child.to_reference)
670
+ assert_changes_match(c_changes, false, false, false, ['name'], [])
671
+ end
672
+
673
+ it 'records a deleted child' do
674
+ vm = create_viewmodel!
675
+ old_child = vm.child
676
+
677
+ vm, ctx = alter_by_view!(viewmodel_class, vm.model) do |view, refs|
678
+ view['child'] = nil
679
+ refs.clear
680
+ end
681
+
682
+ vm_changes = ctx.valid_edit_changes(vm.to_reference)
683
+ assert_changes_match(vm_changes, false, false, false, [], ['child'])
684
+
685
+ refute(ctx.was_edited?(old_child.to_reference))
686
+ end
687
+ end
688
+
689
+ describe 'with has_many_through children test models' do
690
+ include ViewModelSpecHelpers::ParentAndHasManyThroughChildren
691
+
692
+ def new_model
693
+ model_class.new(
694
+ name: 'a',
695
+ model_children: [
696
+ join_model_class.new(position: 1, child: child_model_class.new(name: 'x')),
697
+ join_model_class.new(position: 2, child: child_model_class.new(name: 'y')),
698
+ ])
699
+ end
700
+
701
+ it 'records new children' do
702
+ vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, refs|
703
+ view['children'].concat([{ '_ref' => 'new1' }, { '_ref' => 'new2' }])
704
+ refs['new1'] = { '_type' => child_view_name, 'name' => 'b' }
705
+ refs['new2'] = { '_type' => child_view_name, 'name' => 'c' }
706
+ end
707
+
708
+ vm_changes = ctx.valid_edit_changes(vm.to_reference)
709
+ assert_changes_match(vm_changes, false, false, false, [], ['children'])
710
+
711
+ new_children, existing_children = vm.children.partition do |c|
712
+ c.name < 'm'
713
+ end
714
+
715
+ new_children.each do |c|
716
+ c_changes = ctx.valid_edit_changes(c.to_reference)
717
+ assert_changes_match(c_changes, true, false, false, ['name'], [])
718
+ end
719
+
720
+ existing_children.each do |c|
721
+ refute(ctx.was_edited?(c.to_reference))
722
+ end
723
+ end
724
+
725
+ it 'records replaced children' do
726
+ vm = create_viewmodel!
727
+ old_child = vm.children.first
728
+
729
+ vm, ctx = alter_by_view!(viewmodel_class, vm.model) do |view, refs|
730
+ refs.delete(view['children'].pop['_ref'])
731
+
732
+ view['children'] << { '_ref' => 'new1' }
733
+ refs['new1'] = { '_type' => child_view_name, 'name' => 'b' }
734
+ end
735
+
736
+ vm_changes = ctx.valid_edit_changes(vm.to_reference)
737
+ assert_changes_match(vm_changes, false, false, false, [], ['children'])
738
+
739
+ new_children, existing_children = vm.children.partition do |c|
740
+ c.name < 'm'
741
+ end
742
+
743
+ new_children.each do |c|
744
+ c_changes = ctx.valid_edit_changes(c.to_reference)
745
+ assert_changes_match(c_changes, true, false, false, ['name'], [])
746
+ end
747
+
748
+ existing_children.each do |c|
749
+ refute(ctx.was_edited?(c.to_reference))
750
+ end
751
+
752
+ refute(ctx.was_edited?(old_child.to_reference))
753
+ end
754
+
755
+ it 'records reordered children' do
756
+ vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
757
+ view['children'].reverse!
758
+ end
759
+
760
+ vm_changes = ctx.valid_edit_changes(vm.to_reference)
761
+ assert_changes_match(vm_changes, false, false, false, [], ['children'])
762
+
763
+ vm.children.each do |c|
764
+ refute(ctx.was_edited?(c.to_reference))
765
+ end
766
+ end
767
+ end
768
+ end
769
+ end