iknow_view_models 2.8.4

Sign up to get free protection for your applications and to get access to all the features.
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