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,957 @@
1
+ require_relative "../../../helpers/arvm_test_utilities.rb"
2
+ require_relative "../../../helpers/arvm_test_models.rb"
3
+
4
+ require "minitest/autorun"
5
+
6
+ require "view_model/active_record"
7
+
8
+ class ViewModel::ActiveRecord::HasManyTest < ActiveSupport::TestCase
9
+ include ARVMTestUtilities
10
+
11
+ def self.build_parent(arvm_test_case)
12
+ arvm_test_case.build_viewmodel(:Parent) do
13
+ define_schema do |t|
14
+ t.string :name
15
+ end
16
+
17
+ define_model do
18
+ has_many :children, dependent: :destroy, inverse_of: :parent
19
+ end
20
+
21
+ define_viewmodel do
22
+ attributes :name
23
+ associations :children
24
+ end
25
+ end
26
+ end
27
+
28
+ def self.build_child(arvm_test_case)
29
+ arvm_test_case.build_viewmodel(:Child) do
30
+ define_schema do |t|
31
+ t.references :parent, null: false, foreign_key: true
32
+ t.string :name
33
+ t.float :position
34
+ end
35
+
36
+ define_model do
37
+ belongs_to :parent, inverse_of: :children
38
+ acts_as_manual_list scope: :parent
39
+ end
40
+
41
+ define_viewmodel do
42
+ attributes :name
43
+ acts_as_list :position
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ def before_all
50
+ self.class.build_parent(self)
51
+ self.class.build_child(self)
52
+ end
53
+
54
+ def setup
55
+ super
56
+
57
+ @parent1 = Parent.new(name: "p1",
58
+ children: [Child.new(name: "p1c1", position: 1),
59
+ Child.new(name: "p1c2", position: 2),
60
+ Child.new(name: "p1c3", position: 3)])
61
+ @parent1.save!
62
+
63
+ @parent2 = Parent.new(name: "p2",
64
+ children: [Child.new(name: "p2c1").tap { |c| c.position = 1 },
65
+ Child.new(name: "p2c2").tap { |c| c.position = 2 }])
66
+
67
+ @parent2.save!
68
+
69
+ enable_logging!
70
+ end
71
+
72
+ def test_load_associated
73
+ parentview = ParentView.new(@parent1)
74
+
75
+ childviews = parentview.load_associated(:children)
76
+ assert_equal(3, childviews.size)
77
+ assert_equal(["p1c1", "p1c2", "p1c3"],
78
+ childviews.map(&:name))
79
+ end
80
+
81
+ def test_serialize_view
82
+ view, _refs = serialize_with_references(ParentView.new(@parent1))
83
+
84
+
85
+ assert_equal({ "_type" => "Parent",
86
+ "_version" => 1,
87
+ "id" => @parent1.id,
88
+ "name" => @parent1.name,
89
+ "children" => @parent1.children.map { |child| { "_type" => "Child",
90
+ "_version" => 1,
91
+ "id" => child.id,
92
+ "name" => child.name } } },
93
+ view)
94
+ end
95
+
96
+ def test_loading_batching
97
+ log_queries do
98
+ serialize(ParentView.load)
99
+ end
100
+ assert_equal(['Parent Load', 'Child Load'],
101
+ logged_load_queries)
102
+ end
103
+
104
+ def test_create_from_view
105
+ view = {
106
+ "_type" => "Parent",
107
+ "name" => "p",
108
+ "children" => [{ "_type" => "Child", "name" => "c1" },
109
+ { "_type" => "Child", "name" => "c2" }]
110
+ }
111
+
112
+ pv = ParentView.deserialize_from_view(view)
113
+ p = pv.model
114
+
115
+ assert(!p.changed?)
116
+ assert(!p.new_record?)
117
+
118
+ assert_equal("p", p.name)
119
+
120
+ assert_equal(2, p.children.count)
121
+ p.children.order(:id).each_with_index do |c, i|
122
+ assert(!c.changed?)
123
+ assert(!c.new_record?)
124
+ assert_equal("c#{i + 1}", c.name)
125
+ end
126
+ end
127
+
128
+ def test_editability_raises
129
+ no_edit_context = ParentView.new_deserialize_context(can_edit: false)
130
+
131
+ assert_raises(ViewModel::AccessControlError) do
132
+ # append child
133
+ ParentView.new(@parent1).append_associated(:children, { "_type" => "Child", "name" => "hi" }, deserialize_context: no_edit_context)
134
+ end
135
+
136
+ assert_raises(ViewModel::AccessControlError) do
137
+ # destroy child
138
+ ParentView.new(@parent1).delete_associated(:children, @parent1.children.first.id, deserialize_context: no_edit_context)
139
+ end
140
+ end
141
+
142
+ def test_create_has_many_empty
143
+ view = { '_type' => 'Parent', 'name' => 'p', 'children' => [] }
144
+ pv = ParentView.deserialize_from_view(view)
145
+ assert(pv.model.children.blank?)
146
+ end
147
+
148
+ def test_create_has_many
149
+ view = { '_type' => 'Parent',
150
+ 'name' => 'p',
151
+ 'children' => [{ '_type' => 'Child', 'name' => 'c1' },
152
+ { '_type' => 'Child', 'name' => 'c2' }] }
153
+
154
+ context = ParentView.new_deserialize_context
155
+ pv = ParentView.deserialize_from_view(view, deserialize_context: context)
156
+
157
+ assert_contains_exactly(
158
+ [pv.to_reference, pv.children[0].to_reference, pv.children[1].to_reference],
159
+ context.valid_edit_refs)
160
+
161
+ assert_equal(%w[c1 c2], pv.model.children.map(&:name))
162
+ end
163
+
164
+ def test_nil_multiple_association
165
+ view = {
166
+ "_type" => "Parent",
167
+ "children" => nil
168
+ }
169
+ ex = assert_raises(ViewModel::DeserializationError::InvalidSyntax) do
170
+ ParentView.deserialize_from_view(view)
171
+ end
172
+
173
+ assert_match(/Invalid collection update value 'nil'/, ex.message)
174
+ end
175
+
176
+ def test_non_array_multiple_association
177
+ view = {
178
+ "_type" => "Parent",
179
+ "children" => { '_type' => 'Child', 'name' => 'c1' }
180
+ }
181
+ ex = assert_raises(ViewModel::DeserializationError::InvalidSyntax) do
182
+ ParentView.deserialize_from_view(view)
183
+ end
184
+
185
+ assert_match(/Errors parsing collection functional update/, ex.message)
186
+ end
187
+
188
+ def test_replace_has_many
189
+ old_children = @parent1.children
190
+
191
+ alter_by_view!(ParentView, @parent1) do |view, refs|
192
+ view['children'] = [{ '_type' => 'Child', 'name' => 'new_child' }]
193
+ end
194
+
195
+ assert_equal(['new_child'], @parent1.children.map(&:name))
196
+ assert_equal([], Child.where(id: old_children.map(&:id)))
197
+ end
198
+
199
+ def test_replace_associated_has_many
200
+ old_children = @parent1.children
201
+
202
+ pv = ParentView.new(@parent1)
203
+ context = ParentView.new_deserialize_context
204
+
205
+ nc = pv.replace_associated(:children,
206
+ [{ '_type' => 'Child', 'name' => 'new_child' }],
207
+ deserialize_context: context)
208
+
209
+ expected_edit_checks = [pv.to_reference,
210
+ *old_children.map { |x| ViewModel::Reference.new(ChildView, x.id) },
211
+ *nc.map(&:to_reference)]
212
+
213
+ assert_contains_exactly(expected_edit_checks,
214
+ context.valid_edit_refs)
215
+
216
+ assert_equal(1, nc.size)
217
+ assert_equal('new_child', nc[0].name)
218
+
219
+ @parent1.reload
220
+ assert_equal(['new_child'], @parent1.children.map(&:name))
221
+ assert_equal([], Child.where(id: old_children.map(&:id)))
222
+ end
223
+
224
+ def test_replace_associated_has_many_functional
225
+ old_children = @parent1.children
226
+
227
+ pv = ParentView.new(@parent1)
228
+ context = ParentView.new_deserialize_context
229
+
230
+ update = build_fupdate do
231
+ append([{ '_type' => 'Child', 'name' => 'new_child' }])
232
+ remove([{ '_type' => 'Child', 'id' => old_children.last.id }])
233
+ update([{ '_type' => 'Child', 'id' => old_children.first.id, 'name' => 'renamed p1c1' }])
234
+ end
235
+
236
+ nc = pv.replace_associated(:children, update, deserialize_context: context)
237
+ new_child = nc.detect { |c| c.name == 'new_child' }
238
+
239
+ expected_edit_checks = [pv.to_reference,
240
+ ViewModel::Reference.new(ChildView, new_child.id),
241
+ ViewModel::Reference.new(ChildView, old_children.first.id),
242
+ ViewModel::Reference.new(ChildView, old_children.last.id)]
243
+
244
+ assert_contains_exactly(expected_edit_checks,
245
+ context.valid_edit_refs)
246
+
247
+ assert_equal(3, nc.size)
248
+ assert_equal('renamed p1c1', nc[0].name)
249
+
250
+ @parent1.reload
251
+ assert_equal(['renamed p1c1', 'p1c2', 'new_child'], @parent1.children.order(:position).map(&:name))
252
+ assert_equal([], Child.where(id: old_children.last.id))
253
+ end
254
+
255
+ def test_remove_has_many
256
+ old_children = @parent1.children
257
+ _, context = alter_by_view!(ParentView, @parent1) do |view, refs|
258
+ view['children'] = []
259
+ end
260
+
261
+ expected_edit_checks = [ViewModel::Reference.new(ParentView, @parent1.id)] +
262
+ old_children.map { |x| ViewModel::Reference.new(ChildView, x.id) }
263
+
264
+ assert_equal(Set.new(expected_edit_checks),
265
+ context.valid_edit_refs.to_set)
266
+
267
+ assert_equal([], @parent1.children, 'no children associated with parent1')
268
+ assert(Child.where(id: old_children.map(&:id)).blank?, 'all children deleted')
269
+ end
270
+
271
+ def test_delete_associated_has_many
272
+ c1, c2, c3 = @parent1.children.order(:position).to_a
273
+
274
+ pv = ParentView.new(@parent1)
275
+ context = ParentView.new_deserialize_context
276
+
277
+ pv.delete_associated(:children, c1.id,
278
+ deserialize_context: context)
279
+
280
+ expected_edit_checks = [ViewModel::Reference.new(ParentView, @parent1.id),
281
+ ViewModel::Reference.new(ChildView, c1.id)].to_set
282
+
283
+ assert_equal(expected_edit_checks,
284
+ context.valid_edit_refs.to_set)
285
+
286
+ @parent1.reload
287
+ assert_equal([c2, c3], @parent1.children.order(:position))
288
+ assert(Child.where(id: c1.id).blank?, 'old child deleted')
289
+ end
290
+
291
+ def test_edit_has_many
292
+ c1, c2, c3 = @parent1.children.order(:position).to_a
293
+
294
+ pv, context = alter_by_view!(ParentView, @parent1) do |view, _refs|
295
+ view['children'].shift
296
+ view['children'] << { '_type' => 'Child', 'name' => 'new_c' }
297
+ end
298
+ nc = pv.children.detect { |c| c.name == 'new_c' }
299
+
300
+ assert_contains_exactly(
301
+ [ViewModel::Reference.new(ParentView, @parent1.id),
302
+ ViewModel::Reference.new(ChildView, c1.id), # deleted child
303
+ ViewModel::Reference.new(ChildView, nc.id)], # created child
304
+ context.valid_edit_refs)
305
+
306
+ assert_equal([c2, c3, Child.find_by_name('new_c')],
307
+ @parent1.children.order(:position))
308
+ assert(Child.where(id: c1.id).blank?)
309
+ end
310
+
311
+ def test_append_associated_move_has_many
312
+ c1, c2, c3 = @parent1.children.order(:position).to_a
313
+ pv = ParentView.new(@parent1)
314
+
315
+ # insert before
316
+ pv.append_associated(:children,
317
+ { '_type' => 'Child', 'id' => c3.id },
318
+ before: ViewModel::Reference.new(ChildView, c1.id),
319
+ deserialize_context: (context = ParentView.new_deserialize_context))
320
+
321
+ expected_edit_checks = [pv.to_reference]
322
+ assert_contains_exactly(expected_edit_checks, context.valid_edit_refs)
323
+
324
+ assert_equal([c3, c1, c2],
325
+ @parent1.children.order(:position))
326
+
327
+ # insert after
328
+ pv.append_associated(:children,
329
+ { '_type' => 'Child', 'id' => c3.id },
330
+ after: ViewModel::Reference.new(ChildView, c1.id),
331
+ deserialize_context: (context = ParentView.new_deserialize_context))
332
+
333
+ assert_contains_exactly(expected_edit_checks, context.valid_edit_refs)
334
+
335
+ assert_equal([c1, c3, c2],
336
+ @parent1.children.order(:position))
337
+
338
+ # append
339
+ pv.append_associated(:children,
340
+ { '_type' => 'Child', 'id' => c3.id },
341
+ deserialize_context: (context = ParentView.new_deserialize_context))
342
+
343
+ assert_contains_exactly(expected_edit_checks, context.valid_edit_refs)
344
+
345
+ assert_equal([c1, c2, c3],
346
+ @parent1.children.order(:position))
347
+
348
+ # move from another parent
349
+ p2c1 = @parent2.children.order(:position).first
350
+
351
+ pv.append_associated(:children,
352
+ { '_type' => 'Child', 'id' => p2c1.id },
353
+ deserialize_context: (context = ParentView.new_deserialize_context))
354
+
355
+ expected_edit_checks = [ViewModel::Reference.new(ParentView, @parent1.id),
356
+ ViewModel::Reference.new(ParentView, @parent2.id)]
357
+
358
+ assert_contains_exactly(expected_edit_checks, context.valid_edit_refs)
359
+
360
+ assert_equal([c1, c2, c3, p2c1],
361
+ @parent1.children.order(:position))
362
+ end
363
+
364
+ def test_append_associated_insert_has_many
365
+ c1, c2, c3 = @parent1.children.order(:position).to_a
366
+ pv = ParentView.new(@parent1)
367
+
368
+ # insert before
369
+ pv.append_associated(:children,
370
+ { '_type' => 'Child', 'name' => 'new1' },
371
+ before: ViewModel::Reference.new(ChildView, c2.id),
372
+ deserialize_context: (context = ParentView.new_deserialize_context))
373
+
374
+ n1 = Child.find_by_name('new1')
375
+
376
+ expected_edit_checks = [ViewModel::Reference.new(ParentView, @parent1.id),
377
+ ViewModel::Reference.new(ChildView, n1.id)]
378
+
379
+ assert_contains_exactly(expected_edit_checks, context.valid_edit_refs)
380
+
381
+ assert_equal([c1, n1, c2, c3],
382
+ @parent1.children.order(:position))
383
+
384
+ # insert after
385
+ pv.append_associated(:children,
386
+ { '_type' => 'Child', 'name' => 'new2' },
387
+ after: ViewModel::Reference.new(ChildView, c2.id),
388
+ deserialize_context: (context = ParentView.new_deserialize_context))
389
+
390
+ n2 = Child.find_by_name('new2')
391
+
392
+ expected_edit_checks = [ViewModel::Reference.new(ParentView, @parent1.id),
393
+ ViewModel::Reference.new(ChildView, n2.id)]
394
+
395
+ assert_contains_exactly(expected_edit_checks, context.valid_edit_refs)
396
+
397
+ assert_equal([c1, n1, c2, n2, c3],
398
+ @parent1.children.order(:position))
399
+
400
+ # append
401
+ pv.append_associated(:children,
402
+ { '_type' => 'Child', 'name' => 'new3' },
403
+ deserialize_context: (context = ParentView.new_deserialize_context))
404
+
405
+ n3 = Child.find_by_name('new3')
406
+
407
+ expected_edit_checks = [ViewModel::Reference.new(ParentView, @parent1.id),
408
+ ViewModel::Reference.new(ChildView, n3.id)]
409
+
410
+ assert_contains_exactly(expected_edit_checks, context.valid_edit_refs)
411
+
412
+ assert_equal([c1, n1, c2, n2, c3, n3],
413
+ @parent1.children.order(:position))
414
+ end
415
+
416
+ def test_edit_implicit_list_position
417
+ c1, c2, c3 = @parent1.children.order(:position).to_a
418
+
419
+ alter_by_view!(ParentView, @parent1) do |view, refs|
420
+ view['children'].reverse!
421
+ view['children'].insert(1, { '_type' => 'Child', 'name' => 'new_c' })
422
+ end
423
+
424
+ assert_equal([c3, Child.find_by_name('new_c'), c2, c1],
425
+ @parent1.children.order(:position))
426
+ end
427
+
428
+ def test_edit_missing_child
429
+ view = {
430
+ "_type" => "Parent",
431
+ "children" => [{
432
+ "_type" => "Child",
433
+ "id" => 9999
434
+ }]
435
+ }
436
+
437
+ ex = assert_raises(ViewModel::DeserializationError::NotFound) do
438
+ ParentView.deserialize_from_view(view)
439
+ end
440
+
441
+ assert_equal(ex.nodes, [ViewModel::Reference.new(ChildView, 9999)])
442
+ end
443
+
444
+ def test_move_child_to_new
445
+ old_children = @parent1.children.order(:position)
446
+ moved_child = old_children[1]
447
+
448
+ moved_child_ref = update_hash_for(ChildView, moved_child)
449
+
450
+ view = { '_type' => 'Parent',
451
+ 'name' => 'new_p',
452
+ 'children' => [moved_child_ref,
453
+ { '_type' => 'Child', 'name' => 'new' }] }
454
+
455
+ retained_children = old_children - [moved_child]
456
+ release_view = { '_type' => 'Parent',
457
+ 'id' => @parent1.id,
458
+ 'children' => retained_children.map { |c| update_hash_for(ChildView, c) } }
459
+
460
+ pv = ParentView.deserialize_from_view([view, release_view])
461
+
462
+ new_parent = pv.first.model
463
+ new_parent.reload
464
+
465
+ # child should be removed from old parent
466
+ @parent1.reload
467
+ assert_equal(retained_children,
468
+ @parent1.children.order(:position))
469
+
470
+ # child should be added to new parent
471
+ new_children = new_parent.children.order(:position)
472
+ assert_equal(%w(p1c2 new), new_children.map(&:name))
473
+ assert_equal(moved_child, new_children.first)
474
+ end
475
+
476
+ def test_has_many_cannot_take_from_outside_tree
477
+ old_children = @parent1.children.order(:position)
478
+
479
+ assert_raises(ViewModel::DeserializationError::ParentNotFound) do
480
+ alter_by_view!(ParentView, @parent2) do |p2, _refs|
481
+ p2['children'] = old_children.map { |x| update_hash_for(ChildView, x) }
482
+ end
483
+ end
484
+ end
485
+
486
+ def test_has_many_cannot_duplicate_unreleased_children
487
+ assert_raises(ViewModel::DeserializationError::DuplicateNodes) do
488
+ alter_by_view!(ParentView, [@parent1, @parent2]) do |(p1, p2), _refs|
489
+ p2['children'] = p1['children'].deep_dup
490
+ end
491
+ end
492
+ end
493
+
494
+ def test_has_many_cannot_duplicate_implicitly_unreleased_children
495
+ assert_raises(ViewModel::DeserializationError::ParentNotFound) do
496
+ alter_by_view!(ParentView, [@parent1, @parent2]) do |(p1, p2), _refs|
497
+ p2['children'] = p1['children']
498
+ p1.delete('children')
499
+ end
500
+ end
501
+ end
502
+
503
+ def test_move_child_to_existing
504
+ old_children = @parent1.children.order(:position)
505
+ moved_child = old_children[1]
506
+
507
+ view = ParentView.new(@parent2).to_hash
508
+ view['children'] << ChildView.new(moved_child).to_hash
509
+
510
+ retained_children = old_children - [moved_child]
511
+ release_view = { '_type' => 'Parent', 'id' => @parent1.id,
512
+ 'children' => retained_children.map { |c| update_hash_for(ChildView, c) }}
513
+
514
+ ParentView.deserialize_from_view([view, release_view])
515
+
516
+ @parent1.reload
517
+ @parent2.reload
518
+
519
+ # child should be removed from old parent and positions updated
520
+ assert_equal(retained_children, @parent1.children.order(:position))
521
+
522
+ # child should be added to new parent with valid position
523
+ new_children = @parent2.children.order(:position)
524
+ assert_equal(%w(p2c1 p2c2 p1c2), new_children.map(&:name))
525
+ assert_equal(moved_child, new_children.last)
526
+ end
527
+
528
+ def test_has_many_append_child
529
+ ParentView.new(@parent1).append_associated(:children, { "_type" => "Child", "name" => "new" })
530
+
531
+ @parent1.reload
532
+
533
+ assert_equal(4, @parent1.children.size)
534
+ lc = @parent1.children.order(:position).last
535
+ assert_equal("new", lc.name)
536
+ end
537
+
538
+ def test_has_many_append_and_update_existing_association
539
+ child = @parent1.children[1]
540
+
541
+ cv = ChildView.new(child).to_hash
542
+ cv["name"] = "newname"
543
+
544
+ ParentView.new(@parent1).append_associated(:children, cv)
545
+
546
+ @parent1.reload
547
+
548
+ # Child should have been moved to the end (and edited)
549
+ assert_equal(3, @parent1.children.size)
550
+ c1, c2, c3 = @parent1.children.order(:position)
551
+ assert_equal("p1c1", c1.name)
552
+ assert_equal("p1c3", c2.name)
553
+ assert_equal(child, c3)
554
+ assert_equal("newname", c3.name)
555
+
556
+ end
557
+
558
+ def test_has_many_move_existing_association
559
+ p1c2 = @parent1.children[1]
560
+ assert_equal(2, p1c2.position)
561
+
562
+ ParentView.new(@parent2).append_associated("children", { "_type" => "Child", "id" => p1c2.id })
563
+
564
+ @parent1.reload
565
+ @parent2.reload
566
+
567
+ p1c = @parent1.children.order(:position)
568
+ assert_equal(2, p1c.size)
569
+ assert_equal(["p1c1", "p1c3"], p1c.map(&:name))
570
+
571
+ p2c = @parent2.children.order(:position)
572
+ assert_equal(3, p2c.size)
573
+ assert_equal(["p2c1", "p2c2", "p1c2"], p2c.map(&:name))
574
+ assert_equal(p1c2, p2c[2])
575
+ assert_equal(3, p2c[2].position)
576
+ end
577
+
578
+ def test_has_many_remove_existing_association
579
+ child = @parent1.children[1]
580
+
581
+ ParentView.new(@parent1).delete_associated(:children, child.id)
582
+
583
+ @parent1.reload
584
+
585
+ # Child should have been removed
586
+ assert_equal(2, @parent1.children.size)
587
+ c1, c2 = @parent1.children.order(:position)
588
+ assert_equal("p1c1", c1.name)
589
+ assert_equal("p1c3", c2.name)
590
+
591
+ assert_equal(0, Child.where(id: child.id).size)
592
+ end
593
+
594
+ def test_move_and_edit_child_to_new
595
+ child = @parent1.children[1]
596
+
597
+ child_view = ChildView.new(child).to_hash
598
+ child_view["name"] = "changed"
599
+
600
+ view = { "_type" => "Parent",
601
+ "name" => "new_p",
602
+ "children" => [child_view, { "_type" => "Child", "name" => "new" }]}
603
+
604
+ # TODO this is as awkward here as it is in the application
605
+ release_view = { "_type" => "Parent",
606
+ "id" => @parent1.id,
607
+ "children" => [{ "_type" => "Child", "id" => @parent1.children[0].id },
608
+ { "_type" => "Child", "id" => @parent1.children[2].id }]}
609
+
610
+ pv = ParentView.deserialize_from_view([view, release_view])
611
+ new_parent = pv.first.model
612
+
613
+ # child should be removed from old parent and positions updated
614
+ @parent1.reload
615
+ assert_equal(2, @parent1.children.size, "database has 2 children")
616
+ oc1, oc2 = @parent1.children.order(:position)
617
+ assert_equal("p1c1", oc1.name, "database c1 unchanged")
618
+ assert_equal("p1c3", oc2.name, "database c2 unchanged")
619
+
620
+ # child should be added to new parent with valid position
621
+ assert_equal(2, new_parent.children.size, "viewmodel has 2 children")
622
+ nc1, nc2 = new_parent.children.order(:position)
623
+ assert_equal(child, nc1)
624
+ assert_equal("changed", nc1.name)
625
+ assert_equal("new", nc2.name)
626
+ end
627
+
628
+ def test_move_and_edit_child_to_existing
629
+ old_child = @parent1.children[1]
630
+
631
+ old_child_view = ChildView.new(old_child).to_hash
632
+ old_child_view["name"] = "changed"
633
+ view = ParentView.new(@parent2).to_hash
634
+ view["children"] << old_child_view
635
+
636
+ release_view = {"_type" => "Parent", "id" => @parent1.id,
637
+ "children" => [{"_type" => "Child", "id" => @parent1.children[0].id},
638
+ {"_type" => "Child", "id" => @parent1.children[2].id}]}
639
+
640
+ ParentView.deserialize_from_view([view, release_view])
641
+
642
+ @parent1.reload
643
+ @parent2.reload
644
+
645
+ # child should be removed from old parent and positions updated
646
+ assert_equal(2, @parent1.children.size)
647
+ oc1, oc2 = @parent1.children.order(:position)
648
+
649
+ assert_equal("p1c1", oc1.name)
650
+ assert_equal("p1c3", oc2.name)
651
+
652
+ # child should be added to new parent with valid position
653
+ assert_equal(3, @parent2.children.size)
654
+ nc1, _, nc3 = @parent2.children.order(:position)
655
+ assert_equal("p2c1", nc1.name)
656
+
657
+ assert_equal("p2c1", nc1.name)
658
+
659
+ assert_equal(old_child, nc3)
660
+ assert_equal("changed", nc3.name)
661
+ end
662
+
663
+ def test_functional_update_append
664
+ children_before = @parent1.children.order(:position).pluck(:id)
665
+ fupdate = build_fupdate do
666
+ append([{ '_type' => 'Child' },
667
+ { '_type' => 'Child' }])
668
+ end
669
+
670
+ append_view = { '_type' => 'Parent',
671
+ 'id' => @parent1.id,
672
+ 'children' => fupdate }
673
+
674
+ result = ParentView.deserialize_from_view(append_view)
675
+ @parent1.reload
676
+
677
+ created_children = result.children[-2,2].map(&:id)
678
+
679
+ assert_equal(children_before + created_children,
680
+ @parent1.children.order(:position).pluck(:id))
681
+ end
682
+
683
+ def test_functional_update_append_before_mid
684
+ c1, c2, c3 = @parent1.children.order(:position)
685
+
686
+ fupdate = build_fupdate do
687
+ append([{ '_type' => 'Child', 'name' => 'new c1' },
688
+ { '_type' => 'Child', 'name' => 'new c2' }],
689
+ before: { '_type' => 'Child', 'id' => c2.id })
690
+ end
691
+
692
+ append_view = { '_type' => 'Parent',
693
+ 'id' => @parent1.id,
694
+ 'children' => fupdate }
695
+ ParentView.deserialize_from_view(append_view)
696
+ @parent1.reload
697
+
698
+ assert_equal([c1.name, 'new c1', 'new c2', c2.name, c3.name],
699
+ @parent1.children.order(:position).pluck(:name))
700
+ end
701
+
702
+ def test_functional_update_append_before_reorder
703
+ c1, c2, c3 = @parent1.children.order(:position)
704
+
705
+ fupdate = build_fupdate do
706
+ append([{ '_type' => 'Child', 'id' => c3.id }],
707
+ before: { '_type' => 'Child', 'id' => c2.id })
708
+ end
709
+
710
+ append_view = { '_type' => 'Parent',
711
+ 'id' => @parent1.id,
712
+ 'children' => fupdate }
713
+ ParentView.deserialize_from_view(append_view)
714
+ @parent1.reload
715
+
716
+ assert_equal([c1.name, c3.name, c2.name],
717
+ @parent1.children.order(:position).pluck(:name))
718
+ end
719
+
720
+ def test_functional_update_append_before_beginning
721
+ c1, c2, c3 = @parent1.children.order(:position)
722
+
723
+ fupdate = build_fupdate do
724
+ append([{ '_type' => 'Child', 'name' => 'new c1' },
725
+ { '_type' => 'Child', 'name' => 'new c2' }],
726
+ before: { '_type' => 'Child', 'id' => c1.id })
727
+ end
728
+
729
+ append_view = { '_type' => 'Parent',
730
+ 'id' => @parent1.id,
731
+ 'children' => fupdate }
732
+ ParentView.deserialize_from_view(append_view)
733
+ @parent1.reload
734
+
735
+ assert_equal(['new c1', 'new c2', c1.name, c2.name, c3.name],
736
+ @parent1.children.order(:position).pluck(:name))
737
+ end
738
+
739
+ def test_functional_update_append_before_corpse
740
+ _, c2, _ = @parent1.children.order(:position)
741
+ c2.destroy
742
+
743
+ fupdate = build_fupdate do
744
+ append([{ '_type' => 'Child', 'name' => 'new c1' },
745
+ { '_type' => 'Child', 'name' => 'new c2' }],
746
+ before: { '_type' => 'Child', 'id' => c2.id })
747
+ end
748
+
749
+ append_view = { '_type' => 'Parent',
750
+ 'id' => @parent1.id,
751
+ 'children' => fupdate }
752
+ assert_raises(ViewModel::DeserializationError::AssociatedNotFound) do
753
+ ParentView.deserialize_from_view(append_view)
754
+ end
755
+ end
756
+
757
+ def test_functional_update_append_after_mid
758
+ c1, c2, c3 = @parent1.children.order(:position)
759
+
760
+ fupdate = build_fupdate do
761
+ append([{ '_type' => 'Child', 'name' => 'new c1' },
762
+ { '_type' => 'Child', 'name' => 'new c2' }],
763
+ after: { '_type' => 'Child', 'id' => c2.id })
764
+ end
765
+
766
+ append_view = { '_type' => 'Parent',
767
+ 'id' => @parent1.id,
768
+ 'children' => fupdate }
769
+ ParentView.deserialize_from_view(append_view)
770
+ @parent1.reload
771
+
772
+ assert_equal([c1.name, c2.name, 'new c1', 'new c2', c3.name],
773
+ @parent1.children.order(:position).pluck(:name))
774
+ end
775
+
776
+ def test_functional_update_append_after_end
777
+ c1, c2, c3 = @parent1.children.order(:position)
778
+
779
+ fupdate = build_fupdate do
780
+ append([{ '_type' => 'Child', 'name' => 'new c1' },
781
+ { '_type' => 'Child', 'name' => 'new c2' }],
782
+ after: { '_type' => 'Child', 'id' => c3.id, })
783
+ end
784
+
785
+ append_view = { '_type' => 'Parent',
786
+ 'id' => @parent1.id,
787
+ 'children' => fupdate }
788
+ ParentView.deserialize_from_view(append_view)
789
+ @parent1.reload
790
+
791
+ assert_equal([c1.name, c2.name, c3.name, 'new c1', 'new c2'],
792
+ @parent1.children.order(:position).pluck(:name))
793
+ end
794
+
795
+ def test_functional_update_append_after_corpse
796
+ _, c2, _ = @parent1.children.order(:position)
797
+ c2.destroy
798
+
799
+ fupdate = build_fupdate do
800
+ append([{ '_type' => 'Child', 'name' => 'new c1' },
801
+ { '_type' => 'Child', 'name' => 'new c2' }],
802
+ after: { '_type' => 'Child', 'id' => c2.id },
803
+ )
804
+ end
805
+
806
+ append_view = { '_type' => 'Parent',
807
+ 'id' => @parent1.id,
808
+ 'children' => fupdate }
809
+ assert_raises(ViewModel::DeserializationError::AssociatedNotFound) do
810
+ ParentView.deserialize_from_view(append_view)
811
+ end
812
+ end
813
+
814
+ def test_functional_update_remove_success
815
+ c1_id, c2_id, c3_id = @parent1.children.pluck(:id)
816
+
817
+ fupdate = build_fupdate do
818
+ remove([{ '_type' => 'Child', 'id' => c2_id }])
819
+ end
820
+
821
+ remove_view = { '_type' => 'Parent',
822
+ 'id' => @parent1.id,
823
+ 'children' => fupdate }
824
+ ParentView.deserialize_from_view(remove_view)
825
+ @parent1.reload
826
+
827
+ assert_equal([c1_id, c3_id], @parent1.children.pluck(:id))
828
+ end
829
+
830
+ def test_functional_update_remove_failure
831
+ c_id = @parent1.children.pluck(:id).first
832
+
833
+ fupdate = build_fupdate do
834
+ remove([{ '_type' => 'Child',
835
+ 'id' => c_id,
836
+ 'name' => 'remove and update disallowed' }])
837
+ end
838
+
839
+ remove_view = { '_type' => 'Parent',
840
+ 'id' => @parent1.id,
841
+ 'children' => fupdate }
842
+
843
+ ex = assert_raises(ViewModel::DeserializationError::InvalidSyntax) do
844
+ ParentView.deserialize_from_view(remove_view)
845
+ end
846
+
847
+ assert_match(/Removed entities must have only _type and id fields/, ex.message)
848
+ end
849
+
850
+ def test_functional_update_update_success
851
+ c1_id, c2_id, c3_id = @parent1.children.pluck(:id)
852
+
853
+ fupdate = build_fupdate do
854
+ update([{ '_type' => 'Child',
855
+ 'id' => c2_id,
856
+ 'name' => 'Functionally Updated Child' }])
857
+ end
858
+
859
+ update_view = { '_type' => 'Parent',
860
+ 'id' => @parent1.id,
861
+ 'children' => fupdate }
862
+ ParentView.deserialize_from_view(update_view)
863
+ @parent1.reload
864
+
865
+ assert_equal([c1_id, c2_id, c3_id], @parent1.children.pluck(:id))
866
+ assert_equal('Functionally Updated Child', Child.find(c2_id).name)
867
+ end
868
+
869
+ def test_functional_update_update_failure
870
+ cnew = Child.create(parent: Parent.create).id
871
+
872
+ fupdate = build_fupdate do
873
+ update([{ '_type' => 'Child', 'id' => cnew }])
874
+ end
875
+
876
+ update_view = { '_type' => 'Parent',
877
+ 'id' => @parent1.id,
878
+ 'children' => fupdate }
879
+
880
+ assert_raises(ViewModel::DeserializationError::AssociatedNotFound) do
881
+ ParentView.deserialize_from_view(update_view)
882
+ end
883
+ end
884
+
885
+ def test_functional_update_duplicate_refs
886
+ child_id = @parent1.children.pluck(:id).first
887
+
888
+ fupdate = build_fupdate do
889
+ # remove and append the same child
890
+ remove([{ '_type' => 'Child', 'id' => child_id }])
891
+ append([{ '_type' => 'Child', 'id' => child_id }])
892
+ end
893
+
894
+ update_view = { '_type' => 'Parent',
895
+ 'id' => @parent1.id,
896
+ 'children' => fupdate }
897
+
898
+ ex = assert_raises(ViewModel::DeserializationError::InvalidStructure) do
899
+ ParentView.deserialize_from_view(update_view)
900
+ end
901
+
902
+ assert_match(/Duplicate functional update targets\b.*\bChild\b/, ex.message)
903
+ end
904
+
905
+
906
+ class RenamedTest < ActiveSupport::TestCase
907
+ include ARVMTestUtilities
908
+
909
+ def before_all
910
+ super
911
+
912
+ build_viewmodel(:Parent) do
913
+ define_schema do |t|
914
+ t.string :name
915
+ end
916
+
917
+ define_model do
918
+ has_many :children, dependent: :destroy, inverse_of: :parent
919
+ end
920
+
921
+ define_viewmodel do
922
+ attributes :name
923
+ association :children, as: :something_else
924
+ end
925
+ end
926
+
927
+ ViewModel::ActiveRecord::HasManyTest.build_child(self)
928
+ end
929
+
930
+ def setup
931
+ super
932
+
933
+ @parent = Parent.create(name: 'p1', children: [Child.new(name: 'c1')])
934
+
935
+ enable_logging!
936
+ end
937
+
938
+ def test_dependencies
939
+ root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes([{ '_type' => 'Parent', 'something_else' => [] }])
940
+ assert_equal(DeepPreloader::Spec.new('children' => DeepPreloader::Spec.new), root_updates.first.preload_dependencies)
941
+ assert_equal({ 'something_else' => {} }, root_updates.first.updated_associations)
942
+ end
943
+
944
+ def test_renamed_roundtrip
945
+ alter_by_view!(ParentView, @parent) do |view, _refs|
946
+ assert_equal([{ 'id' => @parent.children.first.id,
947
+ '_type' => 'Child',
948
+ '_version' => 1,
949
+ 'name' => 'c1' }],
950
+ view['something_else'])
951
+ view['something_else'][0]['name'] = 'new c1 name'
952
+ end
953
+
954
+ assert_equal('new c1 name', @parent.children.first.name)
955
+ end
956
+ end
957
+ end