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,313 @@
1
+ require "minitest/autorun"
2
+ require "minitest/unit"
3
+ require "minitest/hooks"
4
+
5
+ require_relative "../../../helpers/arvm_test_models.rb"
6
+ require_relative "../../../helpers/viewmodel_spec_helpers.rb"
7
+
8
+ # MiniTest::Spec.register_spec_type(/./, Minitest::HooksSpec)
9
+
10
+ require "view_model"
11
+ require "view_model/active_record"
12
+
13
+ class ViewModel::ActiveRecord
14
+ class ClonerTest < ActiveSupport::TestCase
15
+ using ViewModel::Utils::Collections
16
+ extend Minitest::Spec::DSL
17
+
18
+ let(:viewmodel) { create_viewmodel! }
19
+ let(:model) { viewmodel.model }
20
+
21
+ describe "with single model" do
22
+ include ViewModelSpecHelpers::Single
23
+
24
+ def model_attributes
25
+ super.merge(schema: ->(t) { t.string :nonview })
26
+ end
27
+
28
+ def new_model
29
+ model_class.new(name: "a", nonview: "b")
30
+ end
31
+
32
+ it "persists the test setup" do
33
+ assert(viewmodel.model.persisted?)
34
+ refute(viewmodel.model.new_record?)
35
+ end
36
+
37
+ it "can clone the model" do
38
+ clone_model = Cloner.new.clone(viewmodel)
39
+ assert(clone_model.new_record?)
40
+ assert_nil(clone_model.id)
41
+ assert_equal(model.name, clone_model.name)
42
+ assert_equal(model.nonview, clone_model.nonview)
43
+ clone_model.save!
44
+ refute_equal(model, clone_model)
45
+ end
46
+
47
+ class IgnoreParentCloner < Cloner
48
+ def visit_model_view(node, model)
49
+ ignore!
50
+ end
51
+ end
52
+
53
+ it "can ignore a model" do
54
+ clone_model = IgnoreParentCloner.new.clone(viewmodel)
55
+ assert_nil(clone_model)
56
+ end
57
+
58
+ class IgnoreAllCloner < Cloner
59
+ def pre_visit(node, model)
60
+ ignore!
61
+ end
62
+ end
63
+
64
+ it "can ignore a model in pre-visit" do
65
+ clone_model = IgnoreAllCloner.new.clone(viewmodel)
66
+ assert_nil(clone_model)
67
+ end
68
+
69
+ class AlterAttributeCloner < Cloner
70
+ def visit_model_view(node, model)
71
+ model.name = "changed"
72
+ end
73
+ end
74
+
75
+ it "can alter a model attribute" do
76
+ clone_model = AlterAttributeCloner.new.clone(viewmodel)
77
+ assert(clone_model.new_record?)
78
+ assert_nil(clone_model.id)
79
+ assert_equal("changed", clone_model.name)
80
+ refute_equal("changed", model.name)
81
+ assert_equal(model.nonview, clone_model.nonview)
82
+ clone_model.save!
83
+ refute_equal(model, clone_model)
84
+ end
85
+
86
+ class PostAlterAttributeCloner < Cloner
87
+ def end_visit_model_view(node, model)
88
+ model.name = "changed"
89
+ end
90
+ end
91
+
92
+ it "can alter a model attribute post-visit" do
93
+ clone_model = PostAlterAttributeCloner.new.clone(viewmodel)
94
+ assert(clone_model.new_record?)
95
+ assert_nil(clone_model.id)
96
+ assert_equal("changed", clone_model.name)
97
+ refute_equal("changed", model.name)
98
+ assert_equal(model.nonview, clone_model.nonview)
99
+ clone_model.save!
100
+ refute_equal(model, clone_model)
101
+ end
102
+ end
103
+
104
+ describe "with a child" do
105
+ def new_child_model
106
+ child_model_class.new(name: "b")
107
+ end
108
+
109
+ def new_model
110
+ model_class.new(name: "a", child: new_child_model)
111
+ end
112
+
113
+ module BehavesLikeConstructingAChild
114
+ extend ActiveSupport::Concern
115
+ included do
116
+ it "persists the test setup" do
117
+ assert(viewmodel.model.persisted?)
118
+ refute(viewmodel.model.new_record?)
119
+ assert(viewmodel.model.child.persisted?)
120
+ refute(viewmodel.model.child.new_record?)
121
+ end
122
+ end
123
+ end
124
+
125
+ class IgnoreChildAssociationCloner < Cloner
126
+ def visit_model_view(node, model)
127
+ ignore_association!(:child)
128
+ end
129
+ end
130
+
131
+ module BehavesLikeCloningAChild
132
+ extend ActiveSupport::Concern
133
+ included do
134
+ it "can clone the model and child" do
135
+ clone_model = Cloner.new.clone(viewmodel)
136
+
137
+ assert(clone_model.new_record?)
138
+ assert_nil(clone_model.id)
139
+ assert_equal(model.name, clone_model.name)
140
+
141
+ clone_child = clone_model.child
142
+ assert(clone_child.new_record?)
143
+ assert_nil(clone_child.id)
144
+ assert_equal(clone_child.name, model.child.name)
145
+
146
+ clone_model.save!
147
+ refute_equal(model, clone_model)
148
+ refute_equal(model.child, clone_model.child)
149
+ end
150
+
151
+ it "can ignore the child association" do
152
+ clone_model = IgnoreChildAssociationCloner.new.clone(viewmodel)
153
+
154
+ assert(clone_model.new_record?)
155
+ assert_nil(clone_model.id)
156
+ assert_equal(model.name, clone_model.name)
157
+
158
+ assert_nil(clone_model.child)
159
+ end
160
+ end
161
+ end
162
+
163
+ describe "as belongs_to" do
164
+ include ViewModelSpecHelpers::ParentAndBelongsToChild
165
+ include BehavesLikeConstructingAChild
166
+ include BehavesLikeCloningAChild
167
+ end
168
+
169
+ describe "as has_one" do
170
+ include ViewModelSpecHelpers::ParentAndHasOneChild
171
+ include BehavesLikeConstructingAChild
172
+ include BehavesLikeCloningAChild
173
+ end
174
+
175
+ describe "as belongs_to shared child" do
176
+ include ViewModelSpecHelpers::ParentAndSharedChild
177
+ include BehavesLikeConstructingAChild
178
+ it "can clone the model but not the child" do
179
+ clone_model = Cloner.new.clone(viewmodel)
180
+
181
+ assert(clone_model.new_record?)
182
+ assert_nil(clone_model.id)
183
+ assert_equal(model.name, clone_model.name)
184
+
185
+ clone_child = clone_model.child
186
+ refute(clone_child.new_record?)
187
+ assert_equal(model.child, clone_child)
188
+
189
+ clone_model.save!
190
+ refute_equal(model, clone_model)
191
+ assert_equal(model.child, clone_model.child)
192
+ end
193
+ end
194
+ end
195
+
196
+ describe "with has_many children" do
197
+ include ViewModelSpecHelpers::ParentAndHasManyChildren
198
+ def new_child_models
199
+ ["b", "c"].map { |n| child_model_class.new(name: n) }
200
+ end
201
+
202
+ def new_model
203
+ model_class.new(name: "a", children: new_child_models)
204
+ end
205
+
206
+ it "persists the test setup" do
207
+ assert(viewmodel.model.persisted?)
208
+ refute(viewmodel.model.new_record?)
209
+ assert_equal(2, viewmodel.model.children.size)
210
+ viewmodel.model.children.each do | child|
211
+ assert(child.persisted?)
212
+ refute(child.new_record?)
213
+ end
214
+ end
215
+
216
+ it "can clone the model" do
217
+ clone_model = Cloner.new.clone(viewmodel)
218
+
219
+ assert(clone_model.new_record?)
220
+ assert_nil(clone_model.id)
221
+ assert_equal(model.name, clone_model.name)
222
+
223
+ assert_equal(2, clone_model.children.size)
224
+
225
+ clone_model.children.zip(model.children) do |clone_child, child|
226
+ assert(clone_child.new_record?)
227
+ assert_nil(clone_child.id)
228
+ assert_equal(clone_child.name, child.name)
229
+ end
230
+
231
+ clone_model.save!
232
+ refute_equal(model, clone_model)
233
+ clone_model.children.zip(model.children) do |clone_child, child|
234
+ refute_equal(clone_child, child)
235
+ end
236
+ end
237
+
238
+ class IgnoreFirstChildCloner < Cloner
239
+ def initialize
240
+ @ignored_first = false
241
+ end
242
+
243
+ def visit_child_view(node, model)
244
+ unless @ignored_first
245
+ @ignored_first = true
246
+ ignore!
247
+ end
248
+ end
249
+ end
250
+
251
+ it "can ignore subset of children" do
252
+ clone_model = IgnoreFirstChildCloner.new.clone(viewmodel)
253
+
254
+ assert(clone_model.new_record?)
255
+ assert_nil(clone_model.id)
256
+ assert_equal(model.name, clone_model.name)
257
+
258
+ assert_equal(1, clone_model.children.size)
259
+ assert_equal(model.children[1].name, clone_model.children[0].name)
260
+ end
261
+ end
262
+
263
+ describe "with has_many_through shared children" do
264
+ include ViewModelSpecHelpers::ParentAndHasManyThroughChildren
265
+ def new_model_children
266
+ ["b", "c"].map.with_index do |n, i|
267
+ join_model_class.new(child: child_model_class.new(name: n), position: i)
268
+ end
269
+ end
270
+
271
+ def new_model
272
+ model_class.new( name: "a", model_children: new_model_children)
273
+ end
274
+
275
+ it "persists the test setup" do
276
+ assert(viewmodel.model.persisted?)
277
+ refute(viewmodel.model.new_record?)
278
+ assert_equal(2, viewmodel.model.model_children.size)
279
+ viewmodel.model.model_children.each do |model_child|
280
+ assert(model_child.persisted?)
281
+ refute(model_child.new_record?)
282
+
283
+ assert(model_child.child.persisted?)
284
+ refute(model_child.child.new_record?)
285
+ end
286
+ end
287
+
288
+ it "can clone the model and join model but not the child" do
289
+ clone_model = Cloner.new.clone(viewmodel)
290
+
291
+ assert(clone_model.new_record?)
292
+ assert_nil(clone_model.id)
293
+ assert_equal(model.name, clone_model.name)
294
+
295
+ assert_equal(2, clone_model.model_children.size)
296
+
297
+ clone_model.model_children.zip(model.model_children) do |clone_model_child, model_child|
298
+ assert(clone_model_child.new_record?)
299
+ assert_nil(clone_model_child.id)
300
+ assert_equal(clone_model_child.position, model_child.position)
301
+ assert_equal(clone_model_child.child, model_child.child)
302
+ end
303
+
304
+ clone_model.save!
305
+ refute_equal(model, clone_model)
306
+ clone_model.model_children.zip(model.model_children) do |clone_model_child, model_child|
307
+ refute_equal(clone_model_child, model_child)
308
+ assert_equal(clone_model_child.child, model_child.child)
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,561 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "bundler/setup"
4
+ Bundler.require
5
+
6
+ require_relative "../../../helpers/callback_tracer.rb"
7
+ require_relative "../../../helpers/controller_test_helpers.rb"
8
+
9
+ require 'byebug'
10
+
11
+ require "minitest/autorun"
12
+ require 'minitest/unit'
13
+
14
+ class ViewModel::ActiveRecord::ControllerTest < ActiveSupport::TestCase
15
+ include ARVMTestUtilities
16
+ include ControllerTestModels
17
+ include ControllerTestControllers
18
+
19
+ def visit(hook, view)
20
+ CallbackTracer::Visit.new(hook, view)
21
+ end
22
+
23
+ def each_hook_span(trace)
24
+ return enum_for(:each_hook_span, trace) unless block_given?
25
+
26
+ hook_nesting = []
27
+
28
+ trace.each_with_index do |t, i|
29
+ case t.hook
30
+ when ViewModel::Callbacks::Hook::OnChange,
31
+ ViewModel::Callbacks::Hook::BeforeValidate
32
+ # ignore
33
+ when ViewModel::Callbacks::Hook::BeforeVisit,
34
+ ViewModel::Callbacks::Hook::BeforeDeserialize
35
+ hook_nesting.push([t, i])
36
+
37
+ when ViewModel::Callbacks::Hook::AfterVisit,
38
+ ViewModel::Callbacks::Hook::AfterDeserialize
39
+ (nested_top, nested_index) = hook_nesting.pop
40
+
41
+ unless nested_top.hook.name == t.hook.name.sub(/^After/, 'Before')
42
+ raise "Invalid nesting, processing '#{t.hook.name}', expected matching '#{nested_top.hook.name}'"
43
+ end
44
+
45
+ unless nested_top.view == t.view
46
+ raise "Invalid nesting, processing '#{t.hook.name}', " \
47
+ "expected viewmodel '#{t.view}' to match '#{nested_top.view}'"
48
+ end
49
+
50
+ yield t.view, (nested_index..i), t.hook.name.sub(/^After/, '')
51
+
52
+ else
53
+ raise 'Unexpected hook type'
54
+ end
55
+ end
56
+ end
57
+
58
+ def show_span(view, range, hook)
59
+ "#{view.class.name}(#{view.id}) #{range} #{hook}"
60
+ end
61
+
62
+ def enclosing_hooks(spans, inner_range)
63
+ spans.select do |_view, range, _hook|
64
+ inner_range != range && range.cover?(inner_range.min) && range.cover?(inner_range.max)
65
+ end
66
+ end
67
+
68
+ def assert_all_hooks_nested_inside_parent_hook(trace)
69
+ spans = each_hook_span(trace).to_a
70
+
71
+ spans.reject { |view, _range, _hook| view.class == ParentView }.each do |view, range, hook|
72
+ enclosing_spans = enclosing_hooks(spans, range)
73
+
74
+ enclosing_parent_hook = enclosing_spans.detect do |other_view, _other_range, other_hook|
75
+ other_hook == hook && other_view.class == ParentView
76
+ end
77
+
78
+ next if enclosing_parent_hook
79
+
80
+ self_str = show_span(view, range, hook)
81
+ enclosing_str = enclosing_spans.map { |ov, ora, oh| show_span(ov, ora, oh) }.join("\n")
82
+ assert_not_nil(
83
+ enclosing_parent_hook,
84
+ "Invalid nesting of hook: #{self_str}\nEnclosing hooks:\n#{enclosing_str}")
85
+ end
86
+ end
87
+
88
+ def setup
89
+ super
90
+ @parent = Parent.create(name: 'p',
91
+ children: [Child.new(name: 'c1', position: 1.0),
92
+ Child.new(name: 'c2', position: 2.0)],
93
+ label: Label.new,
94
+ target: Target.new)
95
+
96
+ @parent_view = ParentView.new(@parent)
97
+
98
+ enable_logging!
99
+ end
100
+
101
+ def test_show
102
+ parentcontroller = ParentController.new(id: @parent.id)
103
+ parentcontroller.invoke(:show)
104
+
105
+ assert_equal({ 'data' => @parent_view.to_hash },
106
+ parentcontroller.hash_response)
107
+
108
+ assert_equal(200, parentcontroller.status)
109
+
110
+ assert_all_hooks_nested_inside_parent_hook(parentcontroller.hook_trace)
111
+ end
112
+
113
+ def test_index
114
+ p2 = Parent.create(name: "p2")
115
+ p2_view = ParentView.new(p2)
116
+
117
+ parentcontroller = ParentController.new
118
+ parentcontroller.invoke(:index)
119
+
120
+ assert_equal(200, parentcontroller.status)
121
+
122
+ assert_equal(parentcontroller.hash_response,
123
+ { "data" => [@parent_view.to_hash, p2_view.to_hash] })
124
+
125
+ assert_all_hooks_nested_inside_parent_hook(parentcontroller.hook_trace)
126
+ end
127
+
128
+ def test_create
129
+ data = {
130
+ '_type' => 'Parent',
131
+ 'name' => 'p2',
132
+ 'label' => { '_type' => 'Label', 'text' => 'l' },
133
+ 'target' => { '_type' => 'Target', 'text' => 't' },
134
+ 'children' => [{ '_type' => 'Child', 'name' => 'c1' },
135
+ { '_type' => 'Child', 'name' => 'c2' }]
136
+ }
137
+
138
+ parentcontroller = ParentController.new(data: data)
139
+ parentcontroller.invoke(:create)
140
+
141
+ assert_equal(200, parentcontroller.status)
142
+
143
+ p2 = Parent.where(name: 'p2').first
144
+ p2_view = ParentView.new(p2)
145
+ assert(p2.present?, 'p2 created')
146
+
147
+ context = ParentView.new_serialize_context(include: 'children')
148
+ assert_equal({ 'data' => p2_view.to_hash(serialize_context: context) },
149
+ parentcontroller.hash_response)
150
+
151
+ assert_all_hooks_nested_inside_parent_hook(parentcontroller.hook_trace)
152
+ end
153
+
154
+ def test_create_empty
155
+ parentcontroller = ParentController.new(data: [])
156
+ parentcontroller.invoke(:create)
157
+
158
+ assert_equal(400, parentcontroller.status)
159
+ end
160
+
161
+ def test_create_invalid
162
+ parentcontroller = ParentController.new(data: 42)
163
+ parentcontroller.invoke(:create)
164
+
165
+ assert_equal(400, parentcontroller.status)
166
+ end
167
+
168
+ def test_update
169
+ data = { 'id' => @parent.id,
170
+ '_type' => 'Parent',
171
+ 'name' => 'new' }
172
+
173
+ parentcontroller = ParentController.new(id: @parent.id, data: data)
174
+ parentcontroller.invoke(:create)
175
+
176
+ assert_equal(200, parentcontroller.status)
177
+
178
+ @parent.reload
179
+
180
+ assert_equal('new', @parent.name)
181
+ assert_equal({ 'data' => @parent_view.to_hash },
182
+ parentcontroller.hash_response)
183
+
184
+ assert_all_hooks_nested_inside_parent_hook(parentcontroller.hook_trace)
185
+ end
186
+
187
+ def test_destroy
188
+ parentcontroller = ParentController.new(id: @parent.id)
189
+ parentcontroller.invoke(:destroy)
190
+
191
+ assert_equal(200, parentcontroller.status)
192
+
193
+ assert(Parent.where(id: @parent.id).blank?, "record doesn't exist after delete")
194
+
195
+ assert_equal({ 'data' => nil },
196
+ parentcontroller.hash_response)
197
+
198
+ assert_all_hooks_nested_inside_parent_hook(parentcontroller.hook_trace)
199
+ end
200
+
201
+ def test_show_missing
202
+ parentcontroller = ParentController.new(id: 9999)
203
+ parentcontroller.invoke(:show)
204
+
205
+ assert_equal(404, parentcontroller.status)
206
+ assert_equal({ 'error' => {
207
+ ViewModel::TYPE_ATTRIBUTE => ViewModel::ErrorView.view_name,
208
+ ViewModel::VERSION_ATTRIBUTE => ViewModel::ErrorView.schema_version,
209
+ 'status' => 404,
210
+ 'detail' => "Couldn't find Parent(s) with id(s)=[9999]",
211
+ 'title' => nil,
212
+ 'code' => "DeserializationError.NotFound",
213
+ 'meta' => { 'nodes' => [{ '_type' => "Parent", 'id' => 9999 }]}}},
214
+ parentcontroller.hash_response)
215
+ end
216
+
217
+ def test_create_invalid_shallow_validation
218
+ data = { '_type' => 'Parent',
219
+ 'children' => [{ '_type' => 'Child',
220
+ 'age' => 42 }] }
221
+
222
+ parentcontroller = ParentController.new(data: data)
223
+ parentcontroller.invoke(:create)
224
+
225
+ assert_equal({ 'error' => {
226
+ ViewModel::TYPE_ATTRIBUTE => ViewModel::ErrorView.view_name,
227
+ ViewModel::VERSION_ATTRIBUTE => ViewModel::ErrorView.schema_version,
228
+ 'status' => 400,
229
+ 'detail' => 'Validation failed: \'age\' must be less than 42',
230
+ 'title' => nil,
231
+ 'code' => "DeserializationError.Validation",
232
+ 'meta' => { 'nodes' => [{ '_type' => "Child", 'id' => nil }],
233
+ 'attribute' => 'age',
234
+ 'message' => 'must be less than 42',
235
+ 'details' => { 'error' => 'less_than', 'value' => 42, 'count' => 42 }}}},
236
+ parentcontroller.hash_response)
237
+ end
238
+
239
+ def test_create_invalid_shallow_constraint
240
+ data = { '_type' => 'Parent',
241
+ 'children' => [{ '_type' => 'Child',
242
+ 'age' => 1 }] }
243
+ parentcontroller = ParentController.new(data: data)
244
+ parentcontroller.invoke(:create)
245
+
246
+ assert_equal(400, parentcontroller.status)
247
+ assert_match(%r{check constraint}i,
248
+ parentcontroller.hash_response["error"]["detail"],
249
+ "Database error propagated")
250
+ end
251
+
252
+ def test_destroy_missing
253
+ parentcontroller = ParentController.new(id: 9999)
254
+ parentcontroller.invoke(:destroy)
255
+
256
+ assert_equal({ 'error' => {
257
+ ViewModel::TYPE_ATTRIBUTE => ViewModel::ErrorView.view_name,
258
+ ViewModel::VERSION_ATTRIBUTE => ViewModel::ErrorView.schema_version,
259
+ 'status' => 404,
260
+ 'detail' => "Couldn't find Parent(s) with id(s)=[9999]",
261
+ 'title' => nil,
262
+ 'code' => "DeserializationError.NotFound",
263
+ 'meta' => { "nodes" => [{"_type" => "Parent", "id" => 9999}]}} },
264
+ parentcontroller.hash_response)
265
+ assert_equal(404, parentcontroller.status)
266
+ end
267
+
268
+ #### Controller for nested model
269
+
270
+ def test_nested_collection_index_associated
271
+ _distractor = Parent.create(name: 'p2', children: [Child.new(name: 'c3', position: 1)])
272
+
273
+ childcontroller = ChildController.new(parent_id: @parent.id)
274
+ childcontroller.invoke(:index_associated)
275
+
276
+ assert_equal(200, childcontroller.status)
277
+
278
+ expected_children = @parent.children
279
+ assert_equal({ 'data' => expected_children.map { |c| ChildView.new(c).to_hash } },
280
+ childcontroller.hash_response)
281
+
282
+ assert_all_hooks_nested_inside_parent_hook(childcontroller.hook_trace)
283
+ end
284
+
285
+ def test_nested_collection_index
286
+ distractor = Parent.create(name: 'p2', children: [Child.new(name: 'c3', position: 1)])
287
+ childcontroller = ChildController.new
288
+
289
+ childcontroller.invoke(:index)
290
+
291
+ assert_equal(200, childcontroller.status)
292
+
293
+ expected_children = @parent.children + distractor.children
294
+ assert_equal({ 'data' => expected_children.map { |c| ChildView.new(c).to_hash } },
295
+ childcontroller.hash_response)
296
+ end
297
+
298
+ def test_nested_collection_append_one
299
+ data = { '_type' => 'Child', 'name' => 'c3' }
300
+ childcontroller = ChildController.new(parent_id: @parent.id, data: data)
301
+
302
+ childcontroller.invoke(:append)
303
+
304
+ assert_equal(200, childcontroller.status, childcontroller.hash_response)
305
+
306
+ @parent.reload
307
+
308
+ assert_equal(%w{c1 c2 c3}, @parent.children.order(:position).pluck(:name))
309
+ assert_equal({ 'data' => ChildView.new(@parent.children.last).to_hash },
310
+ childcontroller.hash_response)
311
+
312
+ assert_all_hooks_nested_inside_parent_hook(childcontroller.hook_trace)
313
+ end
314
+
315
+ def test_nested_collection_append_many
316
+ data = [{ '_type' => 'Child', 'name' => 'c3' },
317
+ { '_type' => 'Child', 'name' => 'c4' }]
318
+
319
+ childcontroller = ChildController.new(parent_id: @parent.id, data: data)
320
+ childcontroller.invoke(:append)
321
+
322
+ assert_equal(200, childcontroller.status, childcontroller.hash_response)
323
+
324
+ @parent.reload
325
+
326
+ assert_equal(%w{c1 c2 c3 c4}, @parent.children.order(:position).pluck(:name))
327
+ new_children_hashes = @parent.children.last(2).map{ |c| ChildView.new(c).to_hash }
328
+ assert_equal({ 'data' => new_children_hashes },
329
+ childcontroller.hash_response)
330
+
331
+ assert_all_hooks_nested_inside_parent_hook(childcontroller.hook_trace)
332
+ end
333
+
334
+ def test_nested_collection_replace
335
+ # Parent.children
336
+ old_children = @parent.children
337
+
338
+ data = [{'_type' => 'Child', 'name' => 'newc1'},
339
+ {'_type' => 'Child', 'name' => 'newc2'}]
340
+
341
+ childcontroller = ChildController.new(parent_id: @parent.id, data: data)
342
+ childcontroller.invoke(:replace)
343
+
344
+ assert_equal(200, childcontroller.status, childcontroller.hash_response)
345
+
346
+ @parent.reload
347
+
348
+ assert_equal(%w{newc1 newc2}, @parent.children.order(:position).pluck(:name))
349
+ assert_predicate(Child.where(id: old_children.map(&:id)), :empty?)
350
+
351
+ assert_all_hooks_nested_inside_parent_hook(childcontroller.hook_trace)
352
+ end
353
+
354
+ def test_nested_collection_replace_bad_data
355
+ data = [{ "name" => "nc" }]
356
+ childcontroller = ChildController.new(parent_id: @parent.id, data: data)
357
+
358
+ childcontroller.invoke(:replace)
359
+
360
+ assert_equal(400, childcontroller.status)
361
+
362
+ assert_all_hooks_nested_inside_parent_hook(childcontroller.hook_trace)
363
+ end
364
+
365
+ def test_nested_collection_disassociate_one
366
+ old_child = @parent.children.first
367
+ childcontroller = ChildController.new(parent_id: @parent.id, id: old_child.id)
368
+ childcontroller.invoke(:disassociate)
369
+
370
+ assert_equal(200, childcontroller.status, childcontroller.hash_response)
371
+
372
+ @parent.reload
373
+
374
+ assert_equal(%w{c2}, @parent.children.order(:position).pluck(:name))
375
+ assert_predicate(Child.where(id: old_child.id), :empty?)
376
+
377
+ assert_all_hooks_nested_inside_parent_hook(childcontroller.hook_trace)
378
+ end
379
+
380
+ def test_nested_collection_disassociate_many
381
+ old_children = @parent.children
382
+
383
+ childcontroller = ChildController.new(parent_id: @parent.id)
384
+ childcontroller.invoke(:disassociate_all)
385
+
386
+ assert_equal(200, childcontroller.status, childcontroller.hash_response)
387
+
388
+ @parent.reload
389
+
390
+ assert_predicate(@parent.children, :empty?)
391
+ assert_predicate(Child.where(id: old_children.map(&:id)), :empty?)
392
+
393
+ assert_all_hooks_nested_inside_parent_hook(childcontroller.hook_trace)
394
+ end
395
+
396
+ # direct methods on nested controller
397
+ def test_nested_collection_destroy
398
+ old_child = @parent.children.first
399
+ childcontroller = ChildController.new(id: old_child.id)
400
+ childcontroller.invoke(:destroy)
401
+
402
+ assert_equal(200, childcontroller.status, childcontroller.hash_response)
403
+
404
+ @parent.reload
405
+
406
+ assert_equal(%w{c2}, @parent.children.order(:position).pluck(:name))
407
+ assert_predicate(Child.where(id: old_child.id), :empty?)
408
+ end
409
+
410
+ def test_nested_collection_update
411
+ old_child = @parent.children.first
412
+
413
+ data = { 'id' => old_child.id,
414
+ '_type' => 'Child',
415
+ 'name' => 'new_name' }
416
+
417
+ childcontroller = ChildController.new(data: data)
418
+ childcontroller.invoke(:create)
419
+
420
+ assert_equal(200, childcontroller.status, childcontroller.hash_response)
421
+
422
+ old_child.reload
423
+
424
+ assert_equal('new_name', old_child.name)
425
+ assert_equal({ 'data' => ChildView.new(old_child).to_hash },
426
+ childcontroller.hash_response)
427
+ end
428
+
429
+ def test_nested_collection_show
430
+ old_child = @parent.children.first
431
+
432
+ childcontroller = ChildController.new(id: old_child.id)
433
+ childcontroller.invoke(:show)
434
+
435
+ assert_equal({ 'data' => ChildView.new(old_child).to_hash },
436
+ childcontroller.hash_response)
437
+
438
+ assert_equal(200, childcontroller.status)
439
+ end
440
+
441
+
442
+ ## Single association
443
+
444
+ def test_nested_singular_replace_from_parent
445
+ old_label = @parent.label
446
+
447
+ data = {'_type' => 'Label', 'text' => 'new label'}
448
+ labelcontroller = LabelController.new(parent_id: @parent.id, data: data)
449
+ labelcontroller.invoke(:create_associated)
450
+
451
+ assert_equal(200, labelcontroller.status, labelcontroller.hash_response)
452
+
453
+ @parent.reload
454
+
455
+ assert_equal({ 'data' => { '_type' => 'Label',
456
+ '_version' => 1,
457
+ 'id' => @parent.label.id,
458
+ 'text' => 'new label' } },
459
+ labelcontroller.hash_response)
460
+
461
+ refute_equal(old_label, @parent.label)
462
+ assert_equal('new label', @parent.label.text)
463
+
464
+ assert_all_hooks_nested_inside_parent_hook(labelcontroller.hook_trace)
465
+ end
466
+
467
+ def test_nested_singular_show_from_parent
468
+ old_label = @parent.label
469
+
470
+ labelcontroller = LabelController.new(parent_id: @parent.id)
471
+ labelcontroller.invoke(:show_associated)
472
+
473
+ assert_equal(200, labelcontroller.status, labelcontroller.hash_response)
474
+
475
+ assert_equal({ 'data' => LabelView.new(old_label).to_hash },
476
+ labelcontroller.hash_response)
477
+
478
+ assert_all_hooks_nested_inside_parent_hook(labelcontroller.hook_trace)
479
+ end
480
+
481
+ def test_nested_singular_destroy_from_parent
482
+ old_label = @parent.label
483
+
484
+ labelcontroller = LabelController.new(parent_id: @parent.id)
485
+ labelcontroller.invoke(:destroy_associated)
486
+
487
+ @parent.reload
488
+
489
+ assert_equal(200, labelcontroller.status, labelcontroller.hash_response)
490
+ assert_equal({ 'data' => nil }, labelcontroller.hash_response)
491
+
492
+ assert_nil(@parent.label)
493
+ assert_predicate(Label.where(id: old_label.id), :empty?)
494
+
495
+ assert_all_hooks_nested_inside_parent_hook(labelcontroller.hook_trace)
496
+ end
497
+
498
+ def test_nested_singular_update_from_parent
499
+ old_label = @parent.label
500
+
501
+ data = {'_type' => 'Label', 'id' => old_label.id, 'text' => 'new label'}
502
+ labelcontroller = LabelController.new(parent_id: @parent.id, data: data)
503
+ labelcontroller.invoke(:create_associated)
504
+
505
+ assert_equal(200, labelcontroller.status, labelcontroller.hash_response)
506
+
507
+ old_label.reload
508
+
509
+ assert_equal('new label', old_label.text)
510
+ assert_equal({ 'data' => LabelView.new(old_label).to_hash },
511
+ labelcontroller.hash_response)
512
+
513
+ assert_all_hooks_nested_inside_parent_hook(labelcontroller.hook_trace)
514
+ end
515
+
516
+ def test_nested_singular_show_from_id
517
+ old_label = @parent.label
518
+
519
+ labelcontroller = LabelController.new(id: old_label.id)
520
+ labelcontroller.invoke(:show)
521
+
522
+ assert_equal(200, labelcontroller.status, labelcontroller.hash_response)
523
+
524
+ assert_equal({ 'data' => LabelView.new(old_label).to_hash },
525
+ labelcontroller.hash_response)
526
+ end
527
+
528
+ def test_nested_singular_destroy_from_id
529
+ # can't directly destroy pointed-to label that's referenced from parent:
530
+ # foreign key violation. Destroy target instead.
531
+ old_target = @parent.target
532
+
533
+ targetcontroller = TargetController.new(id: old_target.id)
534
+ targetcontroller.invoke(:destroy)
535
+
536
+ @parent.reload
537
+
538
+ assert_equal(200, targetcontroller.status, targetcontroller.hash_response)
539
+ assert_equal({ 'data' => nil }, targetcontroller.hash_response)
540
+
541
+ assert_nil(@parent.target)
542
+ assert_predicate(Target.where(id: old_target.id), :empty?)
543
+ end
544
+
545
+ def test_nested_singular_update
546
+ old_label = @parent.label
547
+
548
+ data = {'_type' => 'Label', 'id' => old_label.id, 'text' => 'new label'}
549
+ labelcontroller = LabelController.new(data: data)
550
+ labelcontroller.invoke(:create)
551
+
552
+ assert_equal(200, labelcontroller.status, labelcontroller.hash_response)
553
+
554
+ old_label.reload
555
+
556
+ assert_equal('new label', old_label.text)
557
+ assert_equal({ 'data' => LabelView.new(old_label).to_hash },
558
+ labelcontroller.hash_response)
559
+ end
560
+
561
+ end