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,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