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.
- 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,121 @@
|
|
|
1
|
+
require_relative "../../../helpers/arvm_test_utilities.rb"
|
|
2
|
+
require_relative "../../../helpers/arvm_test_models.rb"
|
|
3
|
+
|
|
4
|
+
require "minitest/autorun"
|
|
5
|
+
|
|
6
|
+
require "view_model/active_record"
|
|
7
|
+
|
|
8
|
+
class ViewModel::ActiveRecord::VersionTest < ActiveSupport::TestCase
|
|
9
|
+
include ARVMTestUtilities
|
|
10
|
+
|
|
11
|
+
def before_all
|
|
12
|
+
super
|
|
13
|
+
|
|
14
|
+
build_viewmodel(:ChildA) do
|
|
15
|
+
define_schema {}
|
|
16
|
+
define_model do
|
|
17
|
+
has_one :parent, inverse_of: :child
|
|
18
|
+
end
|
|
19
|
+
define_viewmodel do
|
|
20
|
+
self.schema_version = 10
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
build_viewmodel(:Target) do
|
|
26
|
+
define_schema {}
|
|
27
|
+
define_model do
|
|
28
|
+
has_one :parent, inverse_of: :target
|
|
29
|
+
end
|
|
30
|
+
define_viewmodel do
|
|
31
|
+
self.schema_version = 20
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
build_viewmodel(:Parent) do
|
|
36
|
+
define_schema do |t|
|
|
37
|
+
t.string :child_type
|
|
38
|
+
t.integer :child_id
|
|
39
|
+
t.integer :target_id
|
|
40
|
+
end
|
|
41
|
+
define_model do
|
|
42
|
+
belongs_to :child, polymorphic: true
|
|
43
|
+
belongs_to :target
|
|
44
|
+
end
|
|
45
|
+
define_viewmodel do
|
|
46
|
+
self.schema_version = 5
|
|
47
|
+
association :child, viewmodels: [:ChildA]
|
|
48
|
+
association :target, shared: true, optional: false
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def setup
|
|
54
|
+
super
|
|
55
|
+
@parent_with_a = Parent.create(child: ChildA.new, target: Target.new)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_schema_versions_reflected_in_output
|
|
59
|
+
data, refs = serialize_with_references(ParentView.new(@parent_with_a))
|
|
60
|
+
|
|
61
|
+
target_ref = refs.keys.first
|
|
62
|
+
|
|
63
|
+
assert_equal({ '_type' => 'Parent',
|
|
64
|
+
'id' => @parent_with_a.id,
|
|
65
|
+
'_version' => 5,
|
|
66
|
+
'child' => {
|
|
67
|
+
'_type' => 'ChildA',
|
|
68
|
+
'id' => @parent_with_a.child.id,
|
|
69
|
+
'_version' => 10,
|
|
70
|
+
},
|
|
71
|
+
'target' => { '_ref' => target_ref } },
|
|
72
|
+
data)
|
|
73
|
+
|
|
74
|
+
assert_equal({ target_ref =>
|
|
75
|
+
{
|
|
76
|
+
'_type' => 'Target',
|
|
77
|
+
'id' => @parent_with_a.target.id,
|
|
78
|
+
'_version' => 20
|
|
79
|
+
} },
|
|
80
|
+
refs)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_regular_version_verification
|
|
84
|
+
ex = assert_raise(ViewModel::DeserializationError::SchemaVersionMismatch) do
|
|
85
|
+
ParentView.deserialize_from_view(
|
|
86
|
+
{ '_type' => 'Parent',
|
|
87
|
+
'_new' => true,
|
|
88
|
+
'_version' => 99 },)
|
|
89
|
+
end
|
|
90
|
+
assert_match(/schema version/, ex.message)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_polymorphic_version_verification
|
|
94
|
+
ex = assert_raise(ViewModel::DeserializationError::SchemaVersionMismatch) do
|
|
95
|
+
ParentView.deserialize_from_view(
|
|
96
|
+
{ '_type' => 'Parent',
|
|
97
|
+
'_new' => true,
|
|
98
|
+
'child' => {
|
|
99
|
+
'_type' => 'ChildA',
|
|
100
|
+
'_version' => 99,
|
|
101
|
+
} })
|
|
102
|
+
end
|
|
103
|
+
assert_match(/schema version/, ex.message)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_shared_parse_version_verification
|
|
107
|
+
ex = assert_raise(ViewModel::DeserializationError::SchemaVersionMismatch) do
|
|
108
|
+
ParentView.deserialize_from_view(
|
|
109
|
+
{ '_type' => 'Parent',
|
|
110
|
+
'_new' => true,
|
|
111
|
+
'target' => { '_ref' => 't1' },
|
|
112
|
+
},
|
|
113
|
+
references: { 't1' => {
|
|
114
|
+
'_type' => 'Target',
|
|
115
|
+
'_new' => true,
|
|
116
|
+
'_version' => 99,
|
|
117
|
+
} })
|
|
118
|
+
end
|
|
119
|
+
assert_match(/schema version/, ex.message)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require_relative "../../helpers/arvm_test_utilities.rb"
|
|
4
|
+
require_relative "../../helpers/arvm_test_models.rb"
|
|
5
|
+
|
|
6
|
+
require "minitest/autorun"
|
|
7
|
+
require 'minitest/unit'
|
|
8
|
+
|
|
9
|
+
require "view_model/active_record"
|
|
10
|
+
|
|
11
|
+
class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
|
|
12
|
+
include ARVMTestUtilities
|
|
13
|
+
|
|
14
|
+
def before_all
|
|
15
|
+
super
|
|
16
|
+
|
|
17
|
+
build_viewmodel(:Trivial) do
|
|
18
|
+
define_schema
|
|
19
|
+
define_model {}
|
|
20
|
+
define_viewmodel {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
build_viewmodel(:Parent) do
|
|
24
|
+
define_schema do |t|
|
|
25
|
+
t.string :name, null: false
|
|
26
|
+
t.integer :one, null: false, default: 1
|
|
27
|
+
t.integer :lock_version, null: false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
define_model do
|
|
31
|
+
validates :name, exclusion: {
|
|
32
|
+
in: %w[invalid],
|
|
33
|
+
message: 'invalid due to matching test sentinel',
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
define_viewmodel do
|
|
38
|
+
attributes :name, :lock_version
|
|
39
|
+
attribute :one, read_only: true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def setup
|
|
45
|
+
@parent1 = Parent.create(name: "p1")
|
|
46
|
+
@parent2 = Parent.create(name: "p2")
|
|
47
|
+
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
## Tests
|
|
52
|
+
|
|
53
|
+
def test_find
|
|
54
|
+
parentview = ParentView.find(@parent1.id)
|
|
55
|
+
assert_equal(@parent1, parentview.model)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_find_multiple
|
|
59
|
+
pv1, pv2 = ParentView.find([@parent1.id, @parent2.id])
|
|
60
|
+
assert_equal(@parent1, pv1.model)
|
|
61
|
+
assert_equal(@parent2, pv2.model)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_find_errors
|
|
65
|
+
ex = assert_raises(ViewModel::DeserializationError::NotFound) do
|
|
66
|
+
ParentView.find([@parent1.id, 9999])
|
|
67
|
+
end
|
|
68
|
+
assert_equal([ViewModel::Reference.new(ParentView, 9999)], ex.nodes)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_load
|
|
72
|
+
parentviews = ParentView.load
|
|
73
|
+
assert_equal(2, parentviews.size)
|
|
74
|
+
|
|
75
|
+
h = parentviews.index_by(&:id)
|
|
76
|
+
assert_equal(@parent1, h[@parent1.id].model)
|
|
77
|
+
assert_equal(@parent2, h[@parent2.id].model)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_create_from_view
|
|
81
|
+
view = {
|
|
82
|
+
"_type" => "Parent",
|
|
83
|
+
"name" => "p",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pv = ParentView.deserialize_from_view(view)
|
|
87
|
+
p = pv.model
|
|
88
|
+
|
|
89
|
+
assert(!p.changed?)
|
|
90
|
+
assert(!p.new_record?)
|
|
91
|
+
|
|
92
|
+
assert_equal("p", p.name)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_create_from_view_with_explicit_id
|
|
96
|
+
view = {
|
|
97
|
+
"_type" => "Parent",
|
|
98
|
+
"id" => 9999,
|
|
99
|
+
"name" => "p",
|
|
100
|
+
"_new" => true
|
|
101
|
+
}
|
|
102
|
+
pv = ParentView.deserialize_from_view(view)
|
|
103
|
+
p = pv.model
|
|
104
|
+
|
|
105
|
+
assert(!p.changed?)
|
|
106
|
+
assert(!p.new_record?)
|
|
107
|
+
assert_equal(9999, p.id)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def test_create_explicit_id_raises_with_id
|
|
111
|
+
view = {
|
|
112
|
+
"_type" => "Parent",
|
|
113
|
+
"id" => 9999,
|
|
114
|
+
"_new" => true
|
|
115
|
+
}
|
|
116
|
+
ex = assert_raises(ViewModel::DeserializationError::DatabaseConstraint) do
|
|
117
|
+
ParentView.deserialize_from_view(view)
|
|
118
|
+
end
|
|
119
|
+
assert_match(/not-null constraint/, ex.message)
|
|
120
|
+
assert_equal([ViewModel::Reference.new(ParentView, 9999)], ex.nodes)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def test_read_only_raises_with_id
|
|
124
|
+
view = {
|
|
125
|
+
"_type" => "Parent",
|
|
126
|
+
"one" => 2,
|
|
127
|
+
"id" => 9999,
|
|
128
|
+
"_new" => true
|
|
129
|
+
}
|
|
130
|
+
ex = assert_raises(ViewModel::DeserializationError::ReadOnlyAttribute) do
|
|
131
|
+
ParentView.deserialize_from_view(view)
|
|
132
|
+
end
|
|
133
|
+
assert_match("one", ex.attribute)
|
|
134
|
+
assert_equal([ViewModel::Reference.new(ParentView, 9999)], ex.nodes)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def test_visibility_raises
|
|
138
|
+
parentview = ParentView.new(@parent1)
|
|
139
|
+
|
|
140
|
+
assert_raises(ViewModel::AccessControlError) do
|
|
141
|
+
no_view_context = ViewModelBase.new_serialize_context(can_view: false)
|
|
142
|
+
parentview.to_hash(serialize_context: no_view_context)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
assert_raises(ViewModel::AccessControlError) do
|
|
146
|
+
no_view_context = ViewModelBase.new_deserialize_context(can_view: false)
|
|
147
|
+
ParentView.deserialize_from_view({'_type' => 'Parent', 'name' => 'p'},
|
|
148
|
+
deserialize_context: no_view_context)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_editability_checks_create
|
|
153
|
+
context = ViewModelBase.new_deserialize_context
|
|
154
|
+
pv = ParentView.deserialize_from_view({ '_type' => 'Parent', 'name' => 'p' },
|
|
155
|
+
deserialize_context: context)
|
|
156
|
+
|
|
157
|
+
assert_equal([pv.to_reference],
|
|
158
|
+
context.valid_edit_refs)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def test_editability_checks_create_on_empty_record
|
|
162
|
+
context = ViewModelBase.new_deserialize_context
|
|
163
|
+
TrivialView.deserialize_from_view({'_type' => 'Trivial' },
|
|
164
|
+
deserialize_context: context)
|
|
165
|
+
|
|
166
|
+
ref = ViewModel::Reference.new(TrivialView, nil)
|
|
167
|
+
assert_equal([ref], context.valid_edit_refs)
|
|
168
|
+
|
|
169
|
+
changes = context.valid_edit_changes(ref)
|
|
170
|
+
assert_equal(true, changes.new?)
|
|
171
|
+
assert_empty(changes.changed_attributes)
|
|
172
|
+
assert_empty(changes.changed_associations)
|
|
173
|
+
assert_equal(false, changes.deleted?)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def test_editability_raises
|
|
177
|
+
no_edit_context = ViewModelBase.new_deserialize_context(can_edit: false)
|
|
178
|
+
|
|
179
|
+
ex = assert_raises(ViewModel::AccessControlError) do
|
|
180
|
+
# create
|
|
181
|
+
ParentView.deserialize_from_view({ "_type" => "Parent", "name" => "p" }, deserialize_context: no_edit_context)
|
|
182
|
+
end
|
|
183
|
+
assert_match(/Illegal edit/, ex.message)
|
|
184
|
+
|
|
185
|
+
ex = assert_raises(ViewModel::AccessControlError) do
|
|
186
|
+
# edit
|
|
187
|
+
v = ParentView.new(@parent1).to_hash.merge("name" => "p2")
|
|
188
|
+
ParentView.deserialize_from_view(v, deserialize_context: no_edit_context)
|
|
189
|
+
end
|
|
190
|
+
assert_match(/Illegal edit/, ex.message)
|
|
191
|
+
|
|
192
|
+
ex = assert_raises(ViewModel::AccessControlError) do
|
|
193
|
+
# destroy
|
|
194
|
+
ParentView.new(@parent1).destroy!(deserialize_context: no_edit_context)
|
|
195
|
+
end
|
|
196
|
+
assert_match(/Illegal edit/, ex.message)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def test_valid_edit_raises
|
|
200
|
+
no_edit_context = ViewModelBase.new_deserialize_context(can_change: false)
|
|
201
|
+
|
|
202
|
+
ex = assert_raises(ViewModel::AccessControlError) do
|
|
203
|
+
# create
|
|
204
|
+
ParentView.deserialize_from_view({ "_type" => "Parent", "name" => "p" }, deserialize_context: no_edit_context)
|
|
205
|
+
end
|
|
206
|
+
assert_match(/Illegal edit/, ex.message)
|
|
207
|
+
|
|
208
|
+
ex = assert_raises(ViewModel::AccessControlError) do
|
|
209
|
+
# edit
|
|
210
|
+
v = ParentView.new(@parent1).to_hash.merge("name" => "p2")
|
|
211
|
+
ParentView.deserialize_from_view(v, deserialize_context: no_edit_context)
|
|
212
|
+
end
|
|
213
|
+
assert_match(/Illegal edit/, ex.message)
|
|
214
|
+
|
|
215
|
+
ex = assert_raises(ViewModel::AccessControlError) do
|
|
216
|
+
# destroy
|
|
217
|
+
ParentView.new(@parent1).destroy!(deserialize_context: no_edit_context)
|
|
218
|
+
end
|
|
219
|
+
assert_match(/Illegal edit/, ex.message)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def test_create_multiple
|
|
223
|
+
view = [{'_type' => 'Parent', 'name' => 'newp1'},
|
|
224
|
+
{'_type' => 'Parent', 'name' => 'newp2'}]
|
|
225
|
+
|
|
226
|
+
result = ParentView.deserialize_from_view(view)
|
|
227
|
+
|
|
228
|
+
new_parents = Parent.where(id: result.map{|x| x.model.id})
|
|
229
|
+
|
|
230
|
+
assert_equal(%w{newp1 newp2}, new_parents.pluck(:name).sort)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def test_update_duplicate_specification
|
|
234
|
+
view = [
|
|
235
|
+
{'_type' => 'Parent', 'id' => @parent1.id},
|
|
236
|
+
{'_type' => 'Parent', 'id' => @parent1.id},
|
|
237
|
+
]
|
|
238
|
+
assert_raises(ViewModel::DeserializationError::DuplicateNodes) do
|
|
239
|
+
ParentView.deserialize_from_view(view)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def test_create_invalid_type
|
|
244
|
+
build_viewmodel(:Invalid) do
|
|
245
|
+
define_schema { |t| }
|
|
246
|
+
define_model {}
|
|
247
|
+
define_viewmodel {}
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
ex = assert_raises(ViewModel::DeserializationError::InvalidSyntax) do
|
|
251
|
+
ParentView.deserialize_from_view({ "target" => [] })
|
|
252
|
+
end
|
|
253
|
+
assert_match(/"_type" wasn't supplied/, ex.message)
|
|
254
|
+
|
|
255
|
+
ex = assert_raises(ViewModel::DeserializationError::InvalidViewType) do
|
|
256
|
+
ParentView.deserialize_from_view({ "_type" => "Invalid" })
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
ex = assert_raises(ViewModel::DeserializationError::UnknownView) do
|
|
260
|
+
ParentView.deserialize_from_view({ "_type" => "NotAViewmodelType" })
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def test_edit_attribute_from_view
|
|
265
|
+
alter_by_view!(ParentView, @parent1) do |view, refs|
|
|
266
|
+
view['name'] = 'renamed'
|
|
267
|
+
end
|
|
268
|
+
assert_equal('renamed', @parent1.name)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def test_edit_attribute_validation_failure
|
|
272
|
+
old_name = @parent1.name
|
|
273
|
+
ex = assert_raises(ViewModel::DeserializationError::Validation) do
|
|
274
|
+
alter_by_view!(ParentView, @parent1) do |view, refs|
|
|
275
|
+
view['name'] = 'invalid'
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
assert_equal(old_name, @parent1.name, 'validation failure causes rollback')
|
|
279
|
+
assert_equal(ex.attribute, "name")
|
|
280
|
+
assert_equal(ex.reason, "invalid due to matching test sentinel")
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def test_edit_readonly_attribute
|
|
284
|
+
assert_raises(ViewModel::DeserializationError::ReadOnlyAttribute) do
|
|
285
|
+
ex = alter_by_view!(ParentView, @parent1) do |view, refs|
|
|
286
|
+
view['one'] = 2
|
|
287
|
+
end
|
|
288
|
+
assert_equal("one", ex.attribute)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def test_edit_missing_root
|
|
293
|
+
view = {
|
|
294
|
+
"_type" => "Parent",
|
|
295
|
+
"id" => 9999
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
ex = assert_raises(ViewModel::DeserializationError::NotFound) do
|
|
299
|
+
ParentView.deserialize_from_view(view)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
assert_equal(ex.nodes, [ViewModel::Reference.new(ParentView, 9999)])
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def test_optimistic_locking
|
|
306
|
+
@parent1.name = "changed"
|
|
307
|
+
@parent1.save!
|
|
308
|
+
|
|
309
|
+
assert_raises(ViewModel::DeserializationError::LockFailure) do
|
|
310
|
+
alter_by_view!(ParentView, @parent1) do |view, _refs|
|
|
311
|
+
view['lock_version'] = 0
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# Tests for overriding the serialization of attributes using custom viewmodels
|
|
318
|
+
class CustomAttributeViewsTests < ActiveSupport::TestCase
|
|
319
|
+
include ARVMTestUtilities
|
|
320
|
+
|
|
321
|
+
class ComplexAttributeView < ViewModel
|
|
322
|
+
attribute :array
|
|
323
|
+
|
|
324
|
+
def serialize_view(json, serialize_context:)
|
|
325
|
+
json.a array[0]
|
|
326
|
+
json.b array[1]
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def self.deserialize_from_view(hash_data, references: {}, deserialize_context:)
|
|
330
|
+
array = [hash_data["a"], hash_data["b"]]
|
|
331
|
+
self.new(array)
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def before_all
|
|
336
|
+
super
|
|
337
|
+
build_viewmodel(:Pair) do
|
|
338
|
+
define_schema do |t|
|
|
339
|
+
t.column :pair, "integer[]"
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
define_model do
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
define_viewmodel do
|
|
346
|
+
attribute :pair, using: ComplexAttributeView
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def setup
|
|
352
|
+
super
|
|
353
|
+
@pair = Pair.create!(pair: [1,2])
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def test_serialize_view
|
|
357
|
+
view, _refs = serialize_with_references(PairView.new(@pair))
|
|
358
|
+
|
|
359
|
+
assert_equal({ "_type" => "Pair",
|
|
360
|
+
"_version" => 1,
|
|
361
|
+
"id" => @pair.id,
|
|
362
|
+
"pair" => { "a" => 1, "b" => 2 } },
|
|
363
|
+
view)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def test_create
|
|
367
|
+
view = { "_type" => "Pair", "pair" => { "a" => 3, "b" => 4 } }
|
|
368
|
+
pv = PairView.deserialize_from_view(view)
|
|
369
|
+
assert_equal([3,4], pv.model.pair)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Tests for functionality common to all ARVM instances, but require some kind
|
|
374
|
+
# of relationship.
|
|
375
|
+
class RelationshipTests < ActiveSupport::TestCase
|
|
376
|
+
include ARVMTestUtilities
|
|
377
|
+
|
|
378
|
+
def before_all
|
|
379
|
+
super
|
|
380
|
+
|
|
381
|
+
build_viewmodel(:Parent) do
|
|
382
|
+
define_schema do |t|
|
|
383
|
+
t.string :name
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
define_model do
|
|
387
|
+
has_many :children, dependent: :destroy, inverse_of: :parent
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
define_viewmodel do
|
|
391
|
+
attributes :name
|
|
392
|
+
associations :children
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
build_viewmodel(:Child) do
|
|
397
|
+
define_schema do |t|
|
|
398
|
+
t.references :parent, null: false, foreign_key: true
|
|
399
|
+
t.string :name
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
define_model do
|
|
403
|
+
belongs_to :parent, inverse_of: :children
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
define_viewmodel do
|
|
407
|
+
attributes :name
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def test_updated_associations_returned
|
|
413
|
+
# This test ensures the data is passed back through the context. The tests
|
|
414
|
+
# for the values are in the relationship-specific tests.
|
|
415
|
+
|
|
416
|
+
updated_by_view = ->(view) do
|
|
417
|
+
context = ViewModelBase.new_deserialize_context
|
|
418
|
+
ParentView.deserialize_from_view(view, deserialize_context: context)
|
|
419
|
+
context.updated_associations
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
assert_equal({},
|
|
423
|
+
updated_by_view.({ '_type' => 'Parent',
|
|
424
|
+
'name' => 'p' }))
|
|
425
|
+
|
|
426
|
+
assert_equal({ 'children' => {} },
|
|
427
|
+
updated_by_view.({ '_type' => 'Parent',
|
|
428
|
+
'name' => 'p',
|
|
429
|
+
'children' => [] }))
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Parent view should be correctly passed down the tree when deserializing
|
|
434
|
+
class DeserializationParentContextTest < ActiveSupport::TestCase
|
|
435
|
+
include ARVMTestUtilities
|
|
436
|
+
|
|
437
|
+
class RefError < RuntimeError
|
|
438
|
+
attr_reader :ref
|
|
439
|
+
def initialize(ref)
|
|
440
|
+
super("Boom")
|
|
441
|
+
@ref = ref
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def before_all
|
|
446
|
+
super
|
|
447
|
+
|
|
448
|
+
build_viewmodel(:List) do
|
|
449
|
+
define_schema do |t|
|
|
450
|
+
t.integer :child_id
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
define_model do
|
|
454
|
+
belongs_to :child, class_name: :List
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
define_viewmodel do
|
|
458
|
+
association :child
|
|
459
|
+
attribute :explode
|
|
460
|
+
# Escape deserialization with the parent context
|
|
461
|
+
define_method(:deserialize_explode) do |val, references:, deserialize_context: |
|
|
462
|
+
raise RefError.new(deserialize_context.parent_ref) if val
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def setup
|
|
469
|
+
@list = List.new(child: List.new(child: nil))
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def test_deserialize_context
|
|
473
|
+
view = {
|
|
474
|
+
"_type" => "List",
|
|
475
|
+
"id" => 1000,
|
|
476
|
+
"_new" => true,
|
|
477
|
+
"child" => {
|
|
478
|
+
"_type" => "List",
|
|
479
|
+
}}
|
|
480
|
+
|
|
481
|
+
ref_error = assert_raises(RefError) do
|
|
482
|
+
ListView.deserialize_from_view(view.deep_merge("child" => { "explode" => true }))
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
assert_equal(ListView, ref_error.ref.viewmodel_class)
|
|
486
|
+
assert_equal(1000, ref_error.ref.model_id)
|
|
487
|
+
|
|
488
|
+
ref_error = assert_raises(RefError) do
|
|
489
|
+
ListView.deserialize_from_view(view.deep_merge("explode" => true))
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
assert_nil(ref_error.ref)
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Parent view should be correctly passed down the tree when deserializing
|
|
497
|
+
class DeferredConstraintTest < ActiveSupport::TestCase
|
|
498
|
+
include ARVMTestUtilities
|
|
499
|
+
|
|
500
|
+
def before_all
|
|
501
|
+
super
|
|
502
|
+
|
|
503
|
+
build_viewmodel(:List) do
|
|
504
|
+
define_schema do |t|
|
|
505
|
+
t.integer :child_id
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
define_model do
|
|
509
|
+
belongs_to :child, class_name: :List
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
define_viewmodel do
|
|
513
|
+
association :child, shared: true
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
List.connection.execute("ALTER TABLE lists ADD CONSTRAINT unique_child UNIQUE (child_id) DEFERRABLE INITIALLY DEFERRED")
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def test_deferred_constraint_violation
|
|
520
|
+
l1 = List.create!(child: List.new)
|
|
521
|
+
l2 = List.create!
|
|
522
|
+
|
|
523
|
+
ex = assert_raises(ViewModel::DeserializationError::UniqueViolation) do
|
|
524
|
+
alter_by_view!(ListView, l2) do |view, refs|
|
|
525
|
+
view['child'] = { "_ref" => "r1" }
|
|
526
|
+
refs["r1"] = { "_type" => "List", "id" => l1.child.id }
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
constraint = 'unique_child'
|
|
531
|
+
columns = ['child_id']
|
|
532
|
+
values = l1.child.id.to_s
|
|
533
|
+
|
|
534
|
+
assert_match(/#{constraint}/, ex.message)
|
|
535
|
+
assert_equal(constraint, ex.constraint)
|
|
536
|
+
assert_equal(columns, ex.columns)
|
|
537
|
+
assert_equal(values, ex.values)
|
|
538
|
+
|
|
539
|
+
assert_equal({ constraint: constraint, columns: columns, values: values, nodes: [] }, ex.meta)
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
end
|