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