iknow_view_models 2.8.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +115 -0
- data/.gitignore +36 -0
- data/.travis.yml +31 -0
- data/Appraisals +9 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +19 -0
- data/Rakefile +21 -0
- data/appveyor.yml +22 -0
- data/gemfiles/rails_5_2.gemfile +15 -0
- data/gemfiles/rails_6_0_beta.gemfile +15 -0
- data/iknow_view_models.gemspec +49 -0
- data/lib/iknow_view_models.rb +12 -0
- data/lib/iknow_view_models/railtie.rb +8 -0
- data/lib/iknow_view_models/version.rb +5 -0
- data/lib/view_model.rb +333 -0
- data/lib/view_model/access_control.rb +154 -0
- data/lib/view_model/access_control/composed.rb +216 -0
- data/lib/view_model/access_control/open.rb +13 -0
- data/lib/view_model/access_control/read_only.rb +13 -0
- data/lib/view_model/access_control/tree.rb +264 -0
- data/lib/view_model/access_control_error.rb +10 -0
- data/lib/view_model/active_record.rb +383 -0
- data/lib/view_model/active_record/association_data.rb +178 -0
- data/lib/view_model/active_record/association_manipulation.rb +389 -0
- data/lib/view_model/active_record/cache.rb +265 -0
- data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
- data/lib/view_model/active_record/cloner.rb +113 -0
- data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
- data/lib/view_model/active_record/controller.rb +77 -0
- data/lib/view_model/active_record/controller_base.rb +185 -0
- data/lib/view_model/active_record/nested_controller_base.rb +93 -0
- data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
- data/lib/view_model/active_record/update_context.rb +252 -0
- data/lib/view_model/active_record/update_data.rb +749 -0
- data/lib/view_model/active_record/update_operation.rb +810 -0
- data/lib/view_model/active_record/visitor.rb +77 -0
- data/lib/view_model/after_transaction_runner.rb +29 -0
- data/lib/view_model/callbacks.rb +219 -0
- data/lib/view_model/changes.rb +62 -0
- data/lib/view_model/config.rb +29 -0
- data/lib/view_model/controller.rb +142 -0
- data/lib/view_model/deserialization_error.rb +437 -0
- data/lib/view_model/deserialize_context.rb +16 -0
- data/lib/view_model/error.rb +191 -0
- data/lib/view_model/error_view.rb +35 -0
- data/lib/view_model/record.rb +367 -0
- data/lib/view_model/record/attribute_data.rb +48 -0
- data/lib/view_model/reference.rb +31 -0
- data/lib/view_model/references.rb +48 -0
- data/lib/view_model/registry.rb +73 -0
- data/lib/view_model/schemas.rb +45 -0
- data/lib/view_model/serialization_error.rb +10 -0
- data/lib/view_model/serialize_context.rb +118 -0
- data/lib/view_model/test_helpers.rb +103 -0
- data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
- data/lib/view_model/traversal_context.rb +126 -0
- data/lib/view_model/utils.rb +24 -0
- data/lib/view_model/utils/collections.rb +49 -0
- data/test/helpers/arvm_test_models.rb +59 -0
- data/test/helpers/arvm_test_utilities.rb +187 -0
- data/test/helpers/callback_tracer.rb +27 -0
- data/test/helpers/controller_test_helpers.rb +270 -0
- data/test/helpers/match_enumerator.rb +58 -0
- data/test/helpers/query_logging.rb +71 -0
- data/test/helpers/test_access_control.rb +56 -0
- data/test/helpers/viewmodel_spec_helpers.rb +326 -0
- data/test/unit/view_model/access_control_test.rb +769 -0
- data/test/unit/view_model/active_record/alias_test.rb +35 -0
- data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
- data/test/unit/view_model/active_record/cache_test.rb +351 -0
- data/test/unit/view_model/active_record/cloner_test.rb +313 -0
- data/test/unit/view_model/active_record/controller_test.rb +561 -0
- data/test/unit/view_model/active_record/counter_test.rb +80 -0
- data/test/unit/view_model/active_record/customization_test.rb +388 -0
- data/test/unit/view_model/active_record/has_many_test.rb +957 -0
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
- data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
- data/test/unit/view_model/active_record/has_one_test.rb +334 -0
- data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
- data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
- data/test/unit/view_model/active_record/poly_test.rb +320 -0
- data/test/unit/view_model/active_record/shared_test.rb +285 -0
- data/test/unit/view_model/active_record/version_test.rb +121 -0
- data/test/unit/view_model/active_record_test.rb +542 -0
- data/test/unit/view_model/callbacks_test.rb +582 -0
- data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
- data/test/unit/view_model/record_test.rb +524 -0
- data/test/unit/view_model/traversal_context_test.rb +371 -0
- data/test/unit/view_model_test.rb +62 -0
- metadata +490 -0
@@ -0,0 +1,326 @@
|
|
1
|
+
require 'view_model'
|
2
|
+
require 'view_model/test_helpers'
|
3
|
+
|
4
|
+
require 'minitest/unit'
|
5
|
+
require 'minitest/hooks'
|
6
|
+
|
7
|
+
module ViewModelSpecHelpers
|
8
|
+
module Base
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
include Minitest::Hooks # not a concern, can I do this?
|
11
|
+
|
12
|
+
included do
|
13
|
+
around do |&block|
|
14
|
+
@builders = []
|
15
|
+
super(&block)
|
16
|
+
@builders.each do |b|
|
17
|
+
b.teardown
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def namespace
|
23
|
+
Object
|
24
|
+
end
|
25
|
+
|
26
|
+
def viewmodel_base
|
27
|
+
ViewModelBase
|
28
|
+
end
|
29
|
+
|
30
|
+
def model_base
|
31
|
+
ApplicationRecord
|
32
|
+
end
|
33
|
+
|
34
|
+
def model_class
|
35
|
+
viewmodel_class.model_class
|
36
|
+
end
|
37
|
+
|
38
|
+
def child_model_class
|
39
|
+
child_viewmodel_class.model_class
|
40
|
+
end
|
41
|
+
|
42
|
+
def view_name
|
43
|
+
viewmodel_class.view_name
|
44
|
+
end
|
45
|
+
|
46
|
+
def child_view_name
|
47
|
+
child_viewmodel_class.view_name
|
48
|
+
end
|
49
|
+
|
50
|
+
def viewmodel_class
|
51
|
+
@viewmodel_class ||= define_viewmodel_class(
|
52
|
+
:Model,
|
53
|
+
spec: model_attributes,
|
54
|
+
namespace: namespace,
|
55
|
+
viewmodel_base: viewmodel_base,
|
56
|
+
model_base: model_base).tap { |klass| yield(klass) if block_given? }
|
57
|
+
end
|
58
|
+
|
59
|
+
def child_viewmodel_class
|
60
|
+
@child_viewmodel_class ||= define_viewmodel_class(
|
61
|
+
:Child,
|
62
|
+
spec: child_attributes,
|
63
|
+
namespace: namespace,
|
64
|
+
viewmodel_base: viewmodel_base,
|
65
|
+
model_base: model_base).tap { |klass| yield(klass) if block_given? }
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_viewmodel!
|
69
|
+
viewmodel_class.new(create_model!)
|
70
|
+
end
|
71
|
+
|
72
|
+
def create_model!
|
73
|
+
new_model.tap { |m| m.save! }
|
74
|
+
end
|
75
|
+
|
76
|
+
def model_attributes
|
77
|
+
ViewModel::TestHelpers::ARVMBuilder::Spec.new(
|
78
|
+
schema: ->(t) { t.string :name },
|
79
|
+
model: ->(m) {},
|
80
|
+
viewmodel: ->(v) { attribute :name }
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
def child_attributes
|
85
|
+
ViewModel::TestHelpers::ARVMBuilder::Spec.new(
|
86
|
+
schema: ->(t) { t.string :name },
|
87
|
+
model: ->(m) {},
|
88
|
+
viewmodel: ->(v) { attribute :name }
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
def define_viewmodel_class(name, **args, &block)
|
93
|
+
builder = ViewModel::TestHelpers::ARVMBuilder.new(name, **args, &block)
|
94
|
+
@builders << builder
|
95
|
+
builder.viewmodel
|
96
|
+
end
|
97
|
+
|
98
|
+
def subject_association
|
99
|
+
raise RuntimeError.new('Test model does not have a child association')
|
100
|
+
end
|
101
|
+
|
102
|
+
def subject_association_name
|
103
|
+
subject_association.association_name
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
module Single
|
108
|
+
extend ActiveSupport::Concern
|
109
|
+
include ViewModelSpecHelpers::Base
|
110
|
+
end
|
111
|
+
|
112
|
+
module ParentAndBelongsToChild
|
113
|
+
extend ActiveSupport::Concern
|
114
|
+
include ViewModelSpecHelpers::Base
|
115
|
+
|
116
|
+
def model_attributes
|
117
|
+
super.merge(schema: ->(t) { t.references :child, foreign_key: true },
|
118
|
+
model: ->(m) { belongs_to :child, inverse_of: :model, dependent: :destroy },
|
119
|
+
viewmodel: ->(v) { association :child })
|
120
|
+
end
|
121
|
+
|
122
|
+
def child_attributes
|
123
|
+
super.merge(model: ->(m) { has_one :model, inverse_of: :child })
|
124
|
+
end
|
125
|
+
|
126
|
+
# parent depends on child, ensure it's touched first
|
127
|
+
def viewmodel_class
|
128
|
+
child_viewmodel_class
|
129
|
+
super
|
130
|
+
end
|
131
|
+
|
132
|
+
def subject_association
|
133
|
+
viewmodel_class._association_data('child')
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
module List
|
138
|
+
extend ActiveSupport::Concern
|
139
|
+
include ViewModelSpecHelpers::Base
|
140
|
+
|
141
|
+
def model_attributes
|
142
|
+
super.merge(schema: ->(t) { t.integer :next_id },
|
143
|
+
model: ->(m) {
|
144
|
+
belongs_to :next, class_name: self.name, inverse_of: :previous, dependent: :destroy
|
145
|
+
has_one :previous, class_name: self.name, foreign_key: :next_id, inverse_of: :next
|
146
|
+
},
|
147
|
+
viewmodel: ->(v) { association :next })
|
148
|
+
end
|
149
|
+
|
150
|
+
def subject_association
|
151
|
+
viewmodel_class._association_data('next')
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
module ParentAndHasOneChild
|
156
|
+
extend ActiveSupport::Concern
|
157
|
+
include ViewModelSpecHelpers::Base
|
158
|
+
|
159
|
+
def model_attributes
|
160
|
+
super.merge(
|
161
|
+
model: ->(m) { has_one :child, inverse_of: :model, dependent: :destroy },
|
162
|
+
viewmodel: ->(v) { association :child }
|
163
|
+
)
|
164
|
+
end
|
165
|
+
|
166
|
+
def child_attributes
|
167
|
+
super.merge(
|
168
|
+
schema: ->(t) { t.references :model, foreign_key: true },
|
169
|
+
model: ->(m) { belongs_to :model, inverse_of: :child }
|
170
|
+
)
|
171
|
+
end
|
172
|
+
|
173
|
+
# child depends on parent, ensure it's touched first
|
174
|
+
def child_viewmodel_class
|
175
|
+
viewmodel_class
|
176
|
+
super
|
177
|
+
end
|
178
|
+
|
179
|
+
def subject_association
|
180
|
+
viewmodel_class._association_data('child')
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
module ParentAndHasManyChildren
|
185
|
+
extend ActiveSupport::Concern
|
186
|
+
include ViewModelSpecHelpers::Base
|
187
|
+
|
188
|
+
def model_attributes
|
189
|
+
super.merge(
|
190
|
+
model: ->(m) { has_many :children, inverse_of: :model, dependent: :destroy },
|
191
|
+
viewmodel: ->(v) { association :children }
|
192
|
+
)
|
193
|
+
end
|
194
|
+
|
195
|
+
def child_attributes
|
196
|
+
super.merge(
|
197
|
+
schema: ->(t) { t.references :model, foreign_key: true },
|
198
|
+
model: ->(m) { belongs_to :model, inverse_of: :children }
|
199
|
+
)
|
200
|
+
end
|
201
|
+
|
202
|
+
# child depends on parent, ensure it's touched first
|
203
|
+
def child_viewmodel_class
|
204
|
+
viewmodel_class
|
205
|
+
super
|
206
|
+
end
|
207
|
+
|
208
|
+
def subject_association
|
209
|
+
viewmodel_class._association_data('children')
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
module ParentAndOrderedChildren
|
214
|
+
extend ActiveSupport::Concern
|
215
|
+
include ViewModelSpecHelpers::Base
|
216
|
+
|
217
|
+
def model_attributes
|
218
|
+
super.merge(
|
219
|
+
model: ->(m) { has_many :children, inverse_of: :model, dependent: :destroy },
|
220
|
+
viewmodel: ->(v) { association :children },
|
221
|
+
)
|
222
|
+
end
|
223
|
+
|
224
|
+
def child_attributes
|
225
|
+
super.merge(
|
226
|
+
schema: ->(t) { t.references :model, foreign_key: true; t.float :position, null: false },
|
227
|
+
model: ->(m) { belongs_to :model, inverse_of: :children },
|
228
|
+
viewmodel: ->(v) { acts_as_list :position },
|
229
|
+
)
|
230
|
+
end
|
231
|
+
|
232
|
+
def child_viewmodel_class
|
233
|
+
# child depends on parent, ensure it's touched first
|
234
|
+
viewmodel_class
|
235
|
+
|
236
|
+
# Add a deferrable unique position constraiont
|
237
|
+
super do |klass|
|
238
|
+
model = klass.model_class
|
239
|
+
table = model.table_name
|
240
|
+
model.connection.execute <<-SQL
|
241
|
+
ALTER TABLE #{table} ADD CONSTRAINT #{table}_unique_on_model_and_position UNIQUE(model_id, position) DEFERRABLE INITIALLY DEFERRED
|
242
|
+
SQL
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def subject_association
|
247
|
+
viewmodel_class._association_data('children')
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
module ParentAndSharedChild
|
252
|
+
extend ActiveSupport::Concern
|
253
|
+
include ViewModelSpecHelpers::Base
|
254
|
+
|
255
|
+
def model_attributes
|
256
|
+
super.merge(
|
257
|
+
schema: ->(t) { t.references :child, foreign_key: true },
|
258
|
+
model: ->(m) { belongs_to :child, inverse_of: :model, dependent: :destroy },
|
259
|
+
viewmodel: ->(v) { association :child, shared: true }
|
260
|
+
)
|
261
|
+
end
|
262
|
+
|
263
|
+
def child_attributes
|
264
|
+
super.merge(
|
265
|
+
model: ->(m) { has_one :model, inverse_of: :child }
|
266
|
+
)
|
267
|
+
end
|
268
|
+
|
269
|
+
# parent depends on child, ensure it's touched first
|
270
|
+
def viewmodel_class
|
271
|
+
child_viewmodel_class
|
272
|
+
super
|
273
|
+
end
|
274
|
+
|
275
|
+
def subject_association
|
276
|
+
viewmodel_class._association_data('child')
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
module ParentAndHasManyThroughChildren
|
281
|
+
extend ActiveSupport::Concern
|
282
|
+
include ViewModelSpecHelpers::Base
|
283
|
+
|
284
|
+
def model_attributes
|
285
|
+
super.merge(
|
286
|
+
model: ->(m) { has_many :model_children, inverse_of: :model, dependent: :destroy },
|
287
|
+
viewmodel: ->(v) { association :children, shared: true, through: :model_children, through_order_attr: :position }
|
288
|
+
)
|
289
|
+
end
|
290
|
+
|
291
|
+
def child_attributes
|
292
|
+
super.merge(
|
293
|
+
model: ->(m) { has_many :model_children, inverse_of: :child, dependent: :destroy }
|
294
|
+
)
|
295
|
+
end
|
296
|
+
|
297
|
+
def join_model_class
|
298
|
+
# depends on parent and child
|
299
|
+
viewmodel_class
|
300
|
+
child_viewmodel_class
|
301
|
+
|
302
|
+
@join_model_class ||=
|
303
|
+
begin
|
304
|
+
define_viewmodel_class(:ModelChild) do
|
305
|
+
define_schema do |t|
|
306
|
+
t.references :model, foreign_key: true
|
307
|
+
t.references :child, foreign_key: true
|
308
|
+
t.float :position
|
309
|
+
end
|
310
|
+
|
311
|
+
define_model do
|
312
|
+
belongs_to :model
|
313
|
+
belongs_to :child
|
314
|
+
end
|
315
|
+
|
316
|
+
no_viewmodel
|
317
|
+
end
|
318
|
+
ModelChild
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def subject_association
|
323
|
+
viewmodel_class._association_data('children')
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
@@ -0,0 +1,769 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../helpers/arvm_test_utilities.rb"
|
4
|
+
require_relative "../../helpers/arvm_test_models.rb"
|
5
|
+
require_relative '../../helpers/viewmodel_spec_helpers.rb'
|
6
|
+
|
7
|
+
require "minitest/autorun"
|
8
|
+
require 'minitest/unit'
|
9
|
+
|
10
|
+
require 'rspec/expectations/minitest_integration'
|
11
|
+
|
12
|
+
require "view_model/active_record"
|
13
|
+
|
14
|
+
class ViewModel::AccessControlTest < ActiveSupport::TestCase
|
15
|
+
include ARVMTestUtilities
|
16
|
+
|
17
|
+
class ComposedTest < ActiveSupport::TestCase
|
18
|
+
include ARVMTestUtilities
|
19
|
+
|
20
|
+
def before_all
|
21
|
+
super
|
22
|
+
|
23
|
+
build_viewmodel(:List) do
|
24
|
+
define_schema do |t|
|
25
|
+
t.string :car
|
26
|
+
t.integer :cdr_id
|
27
|
+
end
|
28
|
+
|
29
|
+
define_model do
|
30
|
+
belongs_to :cdr, class_name: :List, dependent: :destroy
|
31
|
+
end
|
32
|
+
|
33
|
+
define_viewmodel do
|
34
|
+
attribute :car
|
35
|
+
association :cdr
|
36
|
+
|
37
|
+
def self.new_serialize_context(**args)
|
38
|
+
super(access_control: TestAccessControl.new, **args)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.new_deserialize_context(**args)
|
42
|
+
super(access_control: TestAccessControl.new, **args)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def setup
|
49
|
+
ComposedTest.const_set(:TestAccessControl, Class.new(ViewModel::AccessControl::Composed))
|
50
|
+
enable_logging!
|
51
|
+
end
|
52
|
+
|
53
|
+
def teardown
|
54
|
+
ComposedTest.send(:remove_const, :TestAccessControl)
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_visible_if
|
58
|
+
TestAccessControl.visible_if!("car is visible1") do
|
59
|
+
view.car == "visible1"
|
60
|
+
end
|
61
|
+
|
62
|
+
TestAccessControl.visible_if!("car is visible2") do
|
63
|
+
view.car == "visible2"
|
64
|
+
end
|
65
|
+
|
66
|
+
assert_serializes(ListView, List.create!(car: "visible1"))
|
67
|
+
assert_serializes(ListView, List.create!(car: "visible2"))
|
68
|
+
ex = refute_serializes(ListView, List.create!(car: "bad"), /none of the possible/)
|
69
|
+
assert_equal(2, ex.reasons.count)
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_visible_unless
|
73
|
+
TestAccessControl.visible_if!("always") { true }
|
74
|
+
|
75
|
+
TestAccessControl.visible_unless!("car is invisible") do
|
76
|
+
view.car == "invisible"
|
77
|
+
end
|
78
|
+
|
79
|
+
assert_serializes(ListView, List.create!(car: "ok"))
|
80
|
+
refute_serializes(ListView, List.create!(car: "invisible"), /not permitted.*car is invisible/)
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_editable_if
|
84
|
+
TestAccessControl.visible_if!("always") { true }
|
85
|
+
|
86
|
+
TestAccessControl.editable_if!("car is editable1") do
|
87
|
+
view.car == "editable1"
|
88
|
+
end
|
89
|
+
|
90
|
+
TestAccessControl.editable_if!("car is editable2") do
|
91
|
+
view.car == "editable2"
|
92
|
+
end
|
93
|
+
|
94
|
+
assert_deserializes(ListView, List.create!(car: "editable1")) { |v, _| v["car"] = "unchecked" }
|
95
|
+
assert_deserializes(ListView, List.create!(car: "editable2")) { |v, _| v["car"] = "unchecked" }
|
96
|
+
assert_deserializes(ListView, List.create!(car: "forbidden")) { |v, _| v["car"] = "forbidden" } # no change so permitted
|
97
|
+
refute_deserializes(ListView, List.create!(car: "forbidden"), /none of the possible/) { |v, _| v["car"] = "unchecked" }
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_editable_unless
|
101
|
+
TestAccessControl.visible_if!("always") { true }
|
102
|
+
TestAccessControl.editable_if!("always") { true }
|
103
|
+
|
104
|
+
TestAccessControl.editable_unless!("car is uneditable") do
|
105
|
+
view.car == "uneditable"
|
106
|
+
end
|
107
|
+
|
108
|
+
assert_deserializes(ListView, List.create!(car: "ok")) { |v, _| v["car"] = "unchecked" }
|
109
|
+
assert_deserializes(ListView, List.create!(car: "uneditable")) { |v, _| v["car"] = "uneditable" } # no change so permitted
|
110
|
+
refute_deserializes(ListView, List.create!(car: "uneditable"), /car is uneditable/) { |v, _| v["car"] = "unchecked" }
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_edit_valid_if
|
114
|
+
TestAccessControl.visible_if!("always") { true }
|
115
|
+
|
116
|
+
TestAccessControl.edit_valid_if!("car is validedit") do
|
117
|
+
view.car == "validedit"
|
118
|
+
end
|
119
|
+
|
120
|
+
assert_deserializes(ListView, List.create!(car: "unchecked")) { |v, _| v["car"] = "validedit" }
|
121
|
+
assert_deserializes(ListView, List.create!(car: "unmodified")) { |v, _| v["car"] = "unmodified" } # no change so permitted
|
122
|
+
refute_deserializes(ListView, List.create!(car: "unchecked"), /none of the possible/) { |v, _| v["car"] = "bad" }
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_edit_valid_unless
|
126
|
+
TestAccessControl.visible_if!("always") { true }
|
127
|
+
TestAccessControl.edit_valid_if!("always") { true }
|
128
|
+
TestAccessControl.edit_valid_unless!("car is invalidedit") do
|
129
|
+
view.car == "invalidedit"
|
130
|
+
end
|
131
|
+
|
132
|
+
assert_deserializes(ListView, List.create!(car: "unchecked")) { |v, _| v["car"] = "ok" }
|
133
|
+
assert_deserializes(ListView, List.create!(car: "invalidedit")) { |v, _| v["car"] = "invalidedit" }
|
134
|
+
refute_deserializes(ListView, List.create!(car: "unchecked"), /car is invalidedit/) { |v, _| v["car"] = "invalidedit" }
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_editable_and_edit_valid
|
138
|
+
TestAccessControl.visible_if!("always") { true }
|
139
|
+
|
140
|
+
TestAccessControl.editable_if!("original car permits") do
|
141
|
+
view.car == "permitoriginal"
|
142
|
+
end
|
143
|
+
|
144
|
+
TestAccessControl.edit_valid_if!("resulting car permits") do
|
145
|
+
view.car == "permitresult"
|
146
|
+
end
|
147
|
+
|
148
|
+
# at least one valid
|
149
|
+
assert_deserializes(ListView, List.create!(car: "permitoriginal")) { |v, _| v["car"] = "permitresult" }
|
150
|
+
assert_deserializes(ListView, List.create!(car: "badoriginal")) { |v, _| v["car"] = "permitresult" }
|
151
|
+
assert_deserializes(ListView, List.create!(car: "permitoriginal")) { |v, _| v["car"] = "badresult" }
|
152
|
+
|
153
|
+
# no valid
|
154
|
+
ex = refute_deserializes(ListView, List.create!(car: "badoriginal"), /none of the possible/) { |v, _| v["car"] = "badresult" }
|
155
|
+
|
156
|
+
assert_equal(2, ex.reasons.count)
|
157
|
+
end
|
158
|
+
|
159
|
+
def test_inheritance
|
160
|
+
child_access_control = Class.new(ViewModel::AccessControl::Composed)
|
161
|
+
child_access_control.include_from(TestAccessControl)
|
162
|
+
|
163
|
+
TestAccessControl.visible_if!("car is ancestor") { view.car == "ancestor" }
|
164
|
+
child_access_control.visible_if!("car is descendent") { view.car == "descendent" }
|
165
|
+
|
166
|
+
s_ctx = ListView.new_serialize_context(access_control: child_access_control.new)
|
167
|
+
|
168
|
+
assert_serializes(ListView, List.create!(car: "ancestor"), serialize_context: s_ctx)
|
169
|
+
assert_serializes(ListView, List.create!(car: "descendent"), serialize_context: s_ctx)
|
170
|
+
ex = refute_serializes(ListView, List.create!(car: "foreigner"), serialize_context: s_ctx)
|
171
|
+
assert_equal(2, ex.reasons.count)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
class TreeTest < ActiveSupport::TestCase
|
176
|
+
include ARVMTestUtilities
|
177
|
+
|
178
|
+
def before_all
|
179
|
+
super
|
180
|
+
|
181
|
+
# Tree1 is a root, which owns Tree2.
|
182
|
+
build_viewmodel(:Tree1) do
|
183
|
+
define_schema do |t|
|
184
|
+
t.string :val
|
185
|
+
t.integer :tree2_id
|
186
|
+
end
|
187
|
+
|
188
|
+
define_model do
|
189
|
+
belongs_to :tree2, class_name: :Tree2, dependent: :destroy
|
190
|
+
end
|
191
|
+
|
192
|
+
define_viewmodel do
|
193
|
+
attribute :val
|
194
|
+
association :tree2
|
195
|
+
|
196
|
+
def self.new_serialize_context(**args)
|
197
|
+
super(access_control: TestAccessControl.new, **args)
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.new_deserialize_context(**args)
|
201
|
+
super(access_control: TestAccessControl.new, **args)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
build_viewmodel(:Tree2) do
|
207
|
+
define_schema do |t|
|
208
|
+
t.string :val
|
209
|
+
t.integer :tree1_id
|
210
|
+
end
|
211
|
+
|
212
|
+
define_model do
|
213
|
+
belongs_to :tree1, class_name: :Tree1, dependent: :destroy
|
214
|
+
end
|
215
|
+
|
216
|
+
define_viewmodel do
|
217
|
+
attribute :val
|
218
|
+
association :tree1, shared: true, optional: false
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def setup
|
224
|
+
TreeTest.const_set(:TestAccessControl, Class.new(ViewModel::AccessControl::Tree))
|
225
|
+
enable_logging!
|
226
|
+
end
|
227
|
+
|
228
|
+
def teardown
|
229
|
+
TreeTest.send(:remove_const, :TestAccessControl)
|
230
|
+
end
|
231
|
+
|
232
|
+
def make_tree(*vals)
|
233
|
+
tree = vals.each_slice(2).reverse_each.inject(nil) do |rest, (t1, t2)|
|
234
|
+
Tree1.new(val: t1, tree2: Tree2.new(val: t2, tree1: rest))
|
235
|
+
end
|
236
|
+
tree.save!
|
237
|
+
tree
|
238
|
+
end
|
239
|
+
|
240
|
+
def dig_tree(root, refs, attr, *rest)
|
241
|
+
raise "Root missing attribute '#{attr}'" unless root.has_key?(attr)
|
242
|
+
|
243
|
+
child = root[attr]
|
244
|
+
|
245
|
+
if (child_ref = child["_ref"])
|
246
|
+
child = refs[child_ref]
|
247
|
+
end
|
248
|
+
|
249
|
+
if rest.empty?
|
250
|
+
child
|
251
|
+
else
|
252
|
+
dig_tree(child, refs, *rest)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def test_visibility_from_root
|
257
|
+
TestAccessControl.view "Tree1" do
|
258
|
+
visible_if!("true") { true }
|
259
|
+
|
260
|
+
root_children_visible_if!("root children visible") do
|
261
|
+
view.val == "rule:visible_children"
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
refute_serializes(Tree1View, make_tree("arbitrary parent", "invisible child"))
|
266
|
+
assert_serializes(Tree1View, make_tree("rule:visible_children", "visible child"))
|
267
|
+
|
268
|
+
# nested root
|
269
|
+
refute_serializes(Tree1View, make_tree("rule:visible_children", "visible child", "arbitrary parent", "invisible child"))
|
270
|
+
assert_serializes(Tree1View, make_tree("rule:visible_children", "visible child", "rule:visible_children", "visible child"))
|
271
|
+
end
|
272
|
+
|
273
|
+
def test_visibility_veto_from_root
|
274
|
+
TestAccessControl.view "Tree1" do
|
275
|
+
root_children_visible_unless!("root children invisible") do
|
276
|
+
view.val == "rule:invisible_children"
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
TestAccessControl.always do
|
281
|
+
visible_if!("true") { true }
|
282
|
+
end
|
283
|
+
|
284
|
+
assert_serializes(Tree1View, make_tree("arbitrary parent", "visible child"))
|
285
|
+
refute_serializes(Tree1View, make_tree("rule:invisible_children", "invisible child"))
|
286
|
+
|
287
|
+
# nested root
|
288
|
+
assert_serializes(Tree1View, make_tree("arbitrary parent", "visible child", "arbitrary nested parent", "visible child"))
|
289
|
+
refute_serializes(Tree1View, make_tree("arbitrary parent", "visible child", "rule:invisible_children", "invisible child"))
|
290
|
+
end
|
291
|
+
|
292
|
+
def test_editability_from_root
|
293
|
+
TestAccessControl.always do
|
294
|
+
visible_if!("always") { true }
|
295
|
+
end
|
296
|
+
|
297
|
+
TestAccessControl.view "Tree1" do
|
298
|
+
editable_if!("true") { true }
|
299
|
+
|
300
|
+
root_children_editable_if!("root children editable") do
|
301
|
+
view.val == "rule:editable_children"
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
refute_deserializes(Tree1View, make_tree("arbitrary parent", "uneditable child")) { |v, r|
|
306
|
+
dig_tree(v, r, "tree2")["val"] = "change"
|
307
|
+
}
|
308
|
+
|
309
|
+
assert_deserializes(Tree1View, make_tree("rule:editable_children", "editable child")) { |v, r|
|
310
|
+
dig_tree(v, r, "tree2")["val"] = "change"
|
311
|
+
}
|
312
|
+
|
313
|
+
# nested root
|
314
|
+
refute_deserializes(Tree1View, make_tree("rule:editable_children", "editable child", "arbitrary parent", "uneditable child")) { |v, r|
|
315
|
+
dig_tree(v, r, "tree2", "tree1", "tree2")["val"] = "change"
|
316
|
+
}
|
317
|
+
|
318
|
+
assert_deserializes(Tree1View, make_tree("arbitrary parent", "uneditable child", "rule:editable_children", "editable child")) { |v, r|
|
319
|
+
dig_tree(v, r, "tree2", "tree1", "tree2")["val"] = "change"
|
320
|
+
}
|
321
|
+
end
|
322
|
+
|
323
|
+
def test_editability_veto_from_root
|
324
|
+
TestAccessControl.always do
|
325
|
+
visible_if!("always") { true }
|
326
|
+
editable_if!("always") { true }
|
327
|
+
end
|
328
|
+
|
329
|
+
TestAccessControl.view "Tree1" do
|
330
|
+
root_children_editable_unless!("root children uneditable") do
|
331
|
+
view.val == "rule:uneditable_children"
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
refute_deserializes(Tree1View, make_tree("rule:uneditable_children", "uneditable child")) { |v, r|
|
336
|
+
dig_tree(v, r, "tree2")["val"] = "change"
|
337
|
+
}
|
338
|
+
|
339
|
+
assert_deserializes(Tree1View, make_tree("arbitrary parent", "editable child")) { |v, r|
|
340
|
+
dig_tree(v, r, "tree2")["val"] = "change"
|
341
|
+
}
|
342
|
+
|
343
|
+
# nested root
|
344
|
+
refute_deserializes(Tree1View, make_tree("arbitrary parent", "editable child", "rule:uneditable_children", "uneditable child")) { |v, r|
|
345
|
+
dig_tree(v, r, "tree2", "tree1", "tree2")["val"] = "change"
|
346
|
+
}
|
347
|
+
|
348
|
+
assert_deserializes(Tree1View, make_tree("rule:uneditable_children", "uneditable child", "arbitrary parent", "editable child")) { |v, r|
|
349
|
+
dig_tree(v, r, "tree2", "tree1", "tree2")["val"] = "change"
|
350
|
+
}
|
351
|
+
end
|
352
|
+
|
353
|
+
def test_type_independence
|
354
|
+
TestAccessControl.view "Tree1" do
|
355
|
+
visible_if!("tree1 visible") do
|
356
|
+
view.val == "tree1visible"
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
TestAccessControl.view "Tree2" do
|
361
|
+
visible_if!("tree2 visible") do
|
362
|
+
view.val == "tree2visible"
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
refute_serializes(Tree1View, make_tree("tree1invisible", "tree2visible"))
|
367
|
+
assert_serializes(Tree1View, make_tree("tree1visible", "tree2visible"))
|
368
|
+
refute_serializes(Tree1View, make_tree("tree1visible", "tree2invisible"))
|
369
|
+
end
|
370
|
+
|
371
|
+
def test_visibility_always_composition
|
372
|
+
TestAccessControl.view "Tree1" do
|
373
|
+
visible_if!("tree1 visible") do
|
374
|
+
view.val == "tree1visible"
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
TestAccessControl.always do
|
379
|
+
visible_if!("tree2 visible") do
|
380
|
+
view.val == "alwaysvisible"
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
refute_serializes(Tree1View, Tree1.create(val: "bad"))
|
385
|
+
assert_serializes(Tree1View, Tree1.create(val: "tree1visible"))
|
386
|
+
assert_serializes(Tree1View, Tree1.create(val: "alwaysvisible"))
|
387
|
+
end
|
388
|
+
|
389
|
+
def test_editability_always_composition
|
390
|
+
TestAccessControl.view "Tree1" do
|
391
|
+
editable_if!("editable1") { view.val == "editable1" }
|
392
|
+
edit_valid_if!("editvalid1") { view.val == "editvalid1" }
|
393
|
+
end
|
394
|
+
|
395
|
+
TestAccessControl.always do
|
396
|
+
editable_if!("editable2") { view.val == "editable2" }
|
397
|
+
edit_valid_if!("editvalid2") { view.val == "editvalid2" }
|
398
|
+
|
399
|
+
visible_if!("always") { true }
|
400
|
+
end
|
401
|
+
|
402
|
+
refute_deserializes(Tree1View, Tree1.create!(val: "bad")) { |v, _| v["val"] = "alsobad" }
|
403
|
+
|
404
|
+
assert_deserializes(Tree1View, Tree1.create!(val: "editable1")) { |v, _| v["val"] = "unchecked" }
|
405
|
+
assert_deserializes(Tree1View, Tree1.create!(val: "editable2")) { |v, _| v["val"] = "unchecked" }
|
406
|
+
|
407
|
+
assert_deserializes(Tree1View, Tree1.create!(val: "unchecked")) { |v, _| v["val"] = "editvalid1" }
|
408
|
+
assert_deserializes(Tree1View, Tree1.create!(val: "unchecked")) { |v, _| v["val"] = "editvalid2" }
|
409
|
+
end
|
410
|
+
|
411
|
+
def test_ancestry
|
412
|
+
TestAccessControl.view "Tree1" do
|
413
|
+
visible_if!("parent tree1") { view.val == "parenttree1" }
|
414
|
+
end
|
415
|
+
|
416
|
+
TestAccessControl.always do
|
417
|
+
visible_if!("parent always") { view.val == "parentalways" }
|
418
|
+
end
|
419
|
+
|
420
|
+
# Child must be set up after parent is fully defined
|
421
|
+
child_access_control = Class.new(ViewModel::AccessControl::Tree)
|
422
|
+
child_access_control.include_from(TestAccessControl)
|
423
|
+
|
424
|
+
child_access_control.view "Tree1" do
|
425
|
+
visible_if!("child tree1") { view.val == "childtree1" }
|
426
|
+
end
|
427
|
+
|
428
|
+
child_access_control.always do
|
429
|
+
visible_if!("child always") { view.val == "childalways" }
|
430
|
+
end
|
431
|
+
|
432
|
+
s_ctx = Tree1View.new_serialize_context(access_control: child_access_control.new)
|
433
|
+
|
434
|
+
refute_serializes(Tree1View, Tree1.create!(val: "bad"), serialize_context: s_ctx)
|
435
|
+
|
436
|
+
assert_serializes(Tree1View, Tree1.create!(val: "parenttree1"), serialize_context: s_ctx)
|
437
|
+
assert_serializes(Tree1View, Tree1.create!(val: "parentalways"), serialize_context: s_ctx)
|
438
|
+
assert_serializes(Tree1View, Tree1.create!(val: "childtree1"), serialize_context: s_ctx)
|
439
|
+
assert_serializes(Tree1View, Tree1.create!(val: "childalways"), serialize_context: s_ctx)
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
# Integration-test access control, callbacks and viewmodel change tracking: do
|
444
|
+
# the edit checks get called as expected with the correct changes?
|
445
|
+
class ChangeTrackingTest < ActiveSupport::TestCase
|
446
|
+
include ARVMTestUtilities
|
447
|
+
include ViewModelSpecHelpers::List
|
448
|
+
extend Minitest::Spec::DSL
|
449
|
+
|
450
|
+
def assert_changes_match(changes, new, deleted, children, attributes, associations)
|
451
|
+
assert_equal(
|
452
|
+
changes,
|
453
|
+
ViewModel::Changes.new(
|
454
|
+
new: new,
|
455
|
+
deleted: deleted,
|
456
|
+
changed_children: children,
|
457
|
+
changed_attributes: attributes,
|
458
|
+
changed_associations: associations))
|
459
|
+
end
|
460
|
+
|
461
|
+
describe 'with parent and points-to child test models' do
|
462
|
+
include ViewModelSpecHelpers::ParentAndBelongsToChild
|
463
|
+
|
464
|
+
def new_model
|
465
|
+
model_class.new(name: 'a')
|
466
|
+
end
|
467
|
+
|
468
|
+
def new_model_with_child
|
469
|
+
model_class.new(name: 'a', child: child_model_class.new(name: 'b'))
|
470
|
+
end
|
471
|
+
|
472
|
+
it 'records a created model' do
|
473
|
+
view = {
|
474
|
+
'_type' => view_name,
|
475
|
+
'name' => 'a',
|
476
|
+
}
|
477
|
+
|
478
|
+
ctx = viewmodel_class.new_deserialize_context
|
479
|
+
vm = viewmodel_class.deserialize_from_view(view, deserialize_context: ctx)
|
480
|
+
|
481
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
482
|
+
assert_changes_match(vm_changes, true, false, false, ['name'], [])
|
483
|
+
end
|
484
|
+
|
485
|
+
it 'records a destroyed model' do
|
486
|
+
vm = create_viewmodel!
|
487
|
+
|
488
|
+
ctx = viewmodel_class.new_deserialize_context
|
489
|
+
vm.destroy!(deserialize_context: ctx)
|
490
|
+
|
491
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
492
|
+
assert_changes_match(vm_changes, false, true, false, [], [])
|
493
|
+
end
|
494
|
+
|
495
|
+
it 'records a change to an attribute' do
|
496
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
|
497
|
+
view['name'] = nil
|
498
|
+
end
|
499
|
+
|
500
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
501
|
+
assert_changes_match(vm_changes, false, false, false, ['name'], [])
|
502
|
+
end
|
503
|
+
|
504
|
+
it 'records a new child' do
|
505
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
|
506
|
+
view['child'] = { '_type' => child_view_name, 'name' => 'b' }
|
507
|
+
end
|
508
|
+
|
509
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
510
|
+
assert_changes_match(vm_changes, false, false, true, [], ['child'])
|
511
|
+
|
512
|
+
c_changes = ctx.valid_edit_changes(vm.child.to_reference)
|
513
|
+
assert_changes_match(c_changes, true, false, false, ['name'], [])
|
514
|
+
end
|
515
|
+
|
516
|
+
it 'records a replaced child' do
|
517
|
+
m = new_model_with_child.tap(&:save!)
|
518
|
+
old_child = m.child
|
519
|
+
|
520
|
+
vm, ctx = alter_by_view!(viewmodel_class, m) do |view, _refs|
|
521
|
+
view['child'] = { '_type' => child_view_name, 'name' => 'c' }
|
522
|
+
end
|
523
|
+
|
524
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
525
|
+
assert_changes_match(vm_changes, false, false, true, [], ['child'])
|
526
|
+
|
527
|
+
c_changes = ctx.valid_edit_changes(vm.child.to_reference)
|
528
|
+
assert_changes_match(c_changes, true, false, false, ['name'], [])
|
529
|
+
|
530
|
+
oc_changes = ctx.valid_edit_changes(
|
531
|
+
ViewModel::Reference.new(child_viewmodel_class, old_child.id))
|
532
|
+
assert_changes_match(oc_changes, false, true, false, [], [])
|
533
|
+
end
|
534
|
+
|
535
|
+
it 'records an edited child' do
|
536
|
+
m = new_model_with_child.tap(&:save!)
|
537
|
+
|
538
|
+
vm, ctx = alter_by_view!(viewmodel_class, m) do |view, _refs|
|
539
|
+
view['child']['name'] = 'c'
|
540
|
+
end
|
541
|
+
|
542
|
+
# The parent node itself wasn't changed, so must not have been
|
543
|
+
# valid_edit checked
|
544
|
+
refute(ctx.was_edited?(vm.to_reference))
|
545
|
+
assert_changes_match(vm.previous_changes, false, false, true, [], [])
|
546
|
+
|
547
|
+
c_changes = ctx.valid_edit_changes(vm.child.to_reference)
|
548
|
+
assert_changes_match(c_changes, false, false, false, ['name'], [])
|
549
|
+
end
|
550
|
+
|
551
|
+
it 'records a deleted child' do
|
552
|
+
m = new_model_with_child.tap(&:save!)
|
553
|
+
old_child = m.child
|
554
|
+
|
555
|
+
vm, ctx = alter_by_view!(viewmodel_class, m) do |view, _refs|
|
556
|
+
view['child'] = nil
|
557
|
+
end
|
558
|
+
|
559
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
560
|
+
assert_changes_match(vm_changes, false, false, true, [], ['child'])
|
561
|
+
|
562
|
+
oc_changes = ctx.valid_edit_changes(
|
563
|
+
ViewModel::Reference.new(child_viewmodel_class, old_child.id))
|
564
|
+
assert_changes_match(oc_changes, false, true, false, [], [])
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
describe 'with parent and pointed-to child test models' do
|
569
|
+
include ViewModelSpecHelpers::ParentAndOrderedChildren
|
570
|
+
|
571
|
+
def new_model
|
572
|
+
model_class.new(
|
573
|
+
name: 'a',
|
574
|
+
children: [child_model_class.new(name: 'x', position: 1),
|
575
|
+
child_model_class.new(name: 'y', position: 2)])
|
576
|
+
end
|
577
|
+
|
578
|
+
it 'records new children' do
|
579
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
|
580
|
+
view['children'].concat(
|
581
|
+
[
|
582
|
+
{ '_type' => child_view_name, 'name' => 'b' },
|
583
|
+
{ '_type' => child_view_name, 'name' => 'c' },
|
584
|
+
])
|
585
|
+
end
|
586
|
+
|
587
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
588
|
+
assert_changes_match(vm_changes, false, false, true, [], ['children'])
|
589
|
+
|
590
|
+
new_children, existing_children = vm.children.partition do |c|
|
591
|
+
c.name < 'm'
|
592
|
+
end
|
593
|
+
|
594
|
+
new_children.each do |c|
|
595
|
+
c_changes = ctx.valid_edit_changes(c.to_reference)
|
596
|
+
assert_changes_match(c_changes, true, false, false, ['name'], [])
|
597
|
+
end
|
598
|
+
|
599
|
+
existing_children.each do |c|
|
600
|
+
refute(ctx.was_edited?(c.to_reference))
|
601
|
+
end
|
602
|
+
end
|
603
|
+
|
604
|
+
it 'records replaced children' do
|
605
|
+
m = create_model!
|
606
|
+
replaced_child = m.children.last
|
607
|
+
|
608
|
+
vm, ctx = alter_by_view!(viewmodel_class, m) do |view, _refs|
|
609
|
+
view['children'].pop
|
610
|
+
view['children'] << { '_type' => child_view_name, 'name' => 'b' }
|
611
|
+
end
|
612
|
+
|
613
|
+
refute(vm.children.include?(replaced_child))
|
614
|
+
|
615
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
616
|
+
assert_changes_match(vm_changes, false, false, true, [], ['children'])
|
617
|
+
|
618
|
+
new_child = vm.children.detect { |c| c.name == 'b' }
|
619
|
+
c_changes = ctx.valid_edit_changes(new_child.to_reference)
|
620
|
+
assert_changes_match(c_changes, true, false, false, ['name'], [])
|
621
|
+
|
622
|
+
oc_changes = ctx.valid_edit_changes(
|
623
|
+
ViewModel::Reference.new(child_viewmodel_class, replaced_child.id))
|
624
|
+
assert_changes_match(oc_changes, false, true, false, [], [])
|
625
|
+
end
|
626
|
+
|
627
|
+
it 'records reordered children' do
|
628
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
|
629
|
+
view['children'].reverse!
|
630
|
+
end
|
631
|
+
|
632
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
633
|
+
assert_changes_match(vm_changes, false, false, false, [], ['children'])
|
634
|
+
|
635
|
+
vm.children.each do |c|
|
636
|
+
refute(ctx.was_edited?(c.to_reference))
|
637
|
+
end
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
describe 'with parent and shared child test models' do
|
642
|
+
include ViewModelSpecHelpers::ParentAndSharedChild
|
643
|
+
|
644
|
+
def new_model
|
645
|
+
model_class.new(name: 'a', child: child_model_class.new(name: 'z'))
|
646
|
+
end
|
647
|
+
|
648
|
+
it 'records an change to child without a tree change' do
|
649
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, refs|
|
650
|
+
view['child'] = { '_ref' => 'cref' }
|
651
|
+
refs.clear['cref'] = { '_type' => child_view_name, 'name' => 'b' }
|
652
|
+
end
|
653
|
+
|
654
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
655
|
+
assert_changes_match(vm_changes, false, false, false, [], ['child'])
|
656
|
+
|
657
|
+
c_changes = ctx.valid_edit_changes(vm.child.to_reference)
|
658
|
+
assert_changes_match(c_changes, true, false, false, ['name'], [])
|
659
|
+
end
|
660
|
+
|
661
|
+
it 'records an edited child without a tree change' do
|
662
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |_view, refs|
|
663
|
+
refs.values.first.merge!('name' => 'b')
|
664
|
+
end
|
665
|
+
|
666
|
+
refute(ctx.was_edited?(vm.to_reference))
|
667
|
+
assert_changes_match(vm.previous_changes, false, false, false, [], [])
|
668
|
+
|
669
|
+
c_changes = ctx.valid_edit_changes(vm.child.to_reference)
|
670
|
+
assert_changes_match(c_changes, false, false, false, ['name'], [])
|
671
|
+
end
|
672
|
+
|
673
|
+
it 'records a deleted child' do
|
674
|
+
vm = create_viewmodel!
|
675
|
+
old_child = vm.child
|
676
|
+
|
677
|
+
vm, ctx = alter_by_view!(viewmodel_class, vm.model) do |view, refs|
|
678
|
+
view['child'] = nil
|
679
|
+
refs.clear
|
680
|
+
end
|
681
|
+
|
682
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
683
|
+
assert_changes_match(vm_changes, false, false, false, [], ['child'])
|
684
|
+
|
685
|
+
refute(ctx.was_edited?(old_child.to_reference))
|
686
|
+
end
|
687
|
+
end
|
688
|
+
|
689
|
+
describe 'with has_many_through children test models' do
|
690
|
+
include ViewModelSpecHelpers::ParentAndHasManyThroughChildren
|
691
|
+
|
692
|
+
def new_model
|
693
|
+
model_class.new(
|
694
|
+
name: 'a',
|
695
|
+
model_children: [
|
696
|
+
join_model_class.new(position: 1, child: child_model_class.new(name: 'x')),
|
697
|
+
join_model_class.new(position: 2, child: child_model_class.new(name: 'y')),
|
698
|
+
])
|
699
|
+
end
|
700
|
+
|
701
|
+
it 'records new children' do
|
702
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, refs|
|
703
|
+
view['children'].concat([{ '_ref' => 'new1' }, { '_ref' => 'new2' }])
|
704
|
+
refs['new1'] = { '_type' => child_view_name, 'name' => 'b' }
|
705
|
+
refs['new2'] = { '_type' => child_view_name, 'name' => 'c' }
|
706
|
+
end
|
707
|
+
|
708
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
709
|
+
assert_changes_match(vm_changes, false, false, false, [], ['children'])
|
710
|
+
|
711
|
+
new_children, existing_children = vm.children.partition do |c|
|
712
|
+
c.name < 'm'
|
713
|
+
end
|
714
|
+
|
715
|
+
new_children.each do |c|
|
716
|
+
c_changes = ctx.valid_edit_changes(c.to_reference)
|
717
|
+
assert_changes_match(c_changes, true, false, false, ['name'], [])
|
718
|
+
end
|
719
|
+
|
720
|
+
existing_children.each do |c|
|
721
|
+
refute(ctx.was_edited?(c.to_reference))
|
722
|
+
end
|
723
|
+
end
|
724
|
+
|
725
|
+
it 'records replaced children' do
|
726
|
+
vm = create_viewmodel!
|
727
|
+
old_child = vm.children.first
|
728
|
+
|
729
|
+
vm, ctx = alter_by_view!(viewmodel_class, vm.model) do |view, refs|
|
730
|
+
refs.delete(view['children'].pop['_ref'])
|
731
|
+
|
732
|
+
view['children'] << { '_ref' => 'new1' }
|
733
|
+
refs['new1'] = { '_type' => child_view_name, 'name' => 'b' }
|
734
|
+
end
|
735
|
+
|
736
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
737
|
+
assert_changes_match(vm_changes, false, false, false, [], ['children'])
|
738
|
+
|
739
|
+
new_children, existing_children = vm.children.partition do |c|
|
740
|
+
c.name < 'm'
|
741
|
+
end
|
742
|
+
|
743
|
+
new_children.each do |c|
|
744
|
+
c_changes = ctx.valid_edit_changes(c.to_reference)
|
745
|
+
assert_changes_match(c_changes, true, false, false, ['name'], [])
|
746
|
+
end
|
747
|
+
|
748
|
+
existing_children.each do |c|
|
749
|
+
refute(ctx.was_edited?(c.to_reference))
|
750
|
+
end
|
751
|
+
|
752
|
+
refute(ctx.was_edited?(old_child.to_reference))
|
753
|
+
end
|
754
|
+
|
755
|
+
it 'records reordered children' do
|
756
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
|
757
|
+
view['children'].reverse!
|
758
|
+
end
|
759
|
+
|
760
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
761
|
+
assert_changes_match(vm_changes, false, false, false, [], ['children'])
|
762
|
+
|
763
|
+
vm.children.each do |c|
|
764
|
+
refute(ctx.was_edited?(c.to_reference))
|
765
|
+
end
|
766
|
+
end
|
767
|
+
end
|
768
|
+
end
|
769
|
+
end
|