activeshepherd 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
File without changes
@@ -0,0 +1,334 @@
1
+ require 'test_helper'
2
+
3
+ class IntegrationTest < MiniTest::Unit::TestCase
4
+ def setup
5
+ Project.destroy_all
6
+
7
+ @project = Project.new
8
+
9
+ @state = {
10
+ name: "Clean House",
11
+ owner_id: 1,
12
+ detail: {
13
+ description: "I need to clean the house"
14
+ },
15
+ todo_lists: [{
16
+ todos: [{
17
+ text: "Take out the trash",
18
+ todo_assignments: [{
19
+ assignee_id: 2,
20
+ },{
21
+ assignee_id: 3,
22
+ }],
23
+ comments: [{
24
+ author_id: 1,
25
+ text: "Have this done by Monday",
26
+ },{
27
+ author_id: 2,
28
+ text: "I'll do my best",
29
+ }],
30
+ },{
31
+ text: "Sweep the floor"
32
+ }],
33
+ }],
34
+ }
35
+
36
+ @changes = {
37
+ name: ["Clean House", "Clean My House"],
38
+ detail: {
39
+ description: ["I need to clean the house", "I need to clean my house"],
40
+ },
41
+ todo_lists: {
42
+ 0 => {
43
+ todos: {
44
+ 0 => { text: ["Take out the trash", "Take out my trash"] },
45
+ 2 => { text: [nil, "Another task!"], _create: '1' },
46
+ },
47
+ },
48
+ },
49
+ }
50
+ end
51
+
52
+ def test_state_setter_sets_attributes
53
+ @project.aggregate_state = { name: "Foo" }
54
+
55
+ assert_equal "Foo", @project.name
56
+ end
57
+
58
+ def test_state_getter_gets_attributes
59
+ @project.name = "Foo"
60
+
61
+ assert_equal "Foo", @project.aggregate_state[:name]
62
+ end
63
+
64
+ def test_state_setter_sets_attributes_on_has_one_associated_object
65
+ @project.aggregate_state = { detail: { description: "Foobar" }}
66
+
67
+ assert_equal "Foobar", @project.detail.try(:description)
68
+ end
69
+
70
+ def test_state_getter_gets_attributes_on_has_one_associated_object
71
+ @project.build_detail({ description: "Foobar" })
72
+
73
+ assert_equal "Foobar", @project.aggregate_state.fetch(:detail, {})[:description]
74
+ end
75
+
76
+ def test_state_getter_ignores_default_scope_attributes
77
+ @project.todo_lists.build({
78
+ todos_attributes: {
79
+ "0" => {
80
+ text: "Foo"
81
+ }
82
+ }
83
+ })
84
+
85
+ todo = @project.todo_lists.first.todos.first
86
+ c = todo.comments.build({ text: "Bar" })
87
+
88
+ # FIXME
89
+ comment_state = @project.aggregate_state[:todo_lists].first[:todos].first[:comments].first
90
+ # /fixme
91
+ assert_nil comment_state[:commentable_type]
92
+ end
93
+
94
+ def test_state_getter_ignores_has_many_through_associations
95
+ end
96
+
97
+ def test_state_setter_ignores_has_many_through_associations
98
+ end
99
+
100
+ def test_state_setter_sets_attributes_on_has_many_associated_object
101
+ @project.aggregate_state = { todo_lists: [{ todos: [{ text: "Hi" },{ text: "Bye" }] }] }
102
+
103
+ assert_equal 1, @project.todo_lists.size
104
+ assert_equal 2, @project.todo_lists.first.todos.size
105
+ assert_equal ["Hi", "Bye"], @project.todo_lists.first.todos.map(&:text)
106
+ end
107
+
108
+ def test_state_getter_rejects_id
109
+ refute @project.aggregate_state.keys.include?(:id)
110
+ end
111
+
112
+ def test_state_getter_rejects_unpopulated_associations
113
+ assert_equal 0, @project.todo_lists.size
114
+ assert_nil @project.detail
115
+
116
+ refute @project.aggregate_state.has_key?(:todo_lists)
117
+ refute @project.aggregate_state.has_key?(:detail)
118
+ end
119
+
120
+ def test_does_not_walk_associations_to_other_entities
121
+ @project.aggregate_state = { owner: { name: "Joe Schmoe" } }
122
+
123
+ refute_equal "Joe Schmoe", @project.owner.try(:name)
124
+ end
125
+
126
+ =begin
127
+ # FIXME: rails 4 is removing read only associations
128
+ def test_state_getter_does_not_walk_read_only_associations
129
+ @project.todo_lists.build.tap do |todo_list|
130
+ todo_list.todos.build({ text: "Hi" })
131
+ @project.recent_todo_list = todo_list
132
+ end
133
+
134
+ assert_nil @project.aggregate_state[:recent_todo_list]
135
+ end
136
+
137
+ def test_state_setter_does_not_walk_read_only_associations
138
+ @project.aggregate_state = { recent_todo_list: {} }
139
+
140
+ assert_nil @project.recent_todo_list
141
+ end
142
+ =end
143
+
144
+ def test_state_getter_ignores_foreign_key_relationship_to_parent_object
145
+ @project.save
146
+ @project.build_detail({ description: "Foo" })
147
+
148
+ assert_equal({ description: "Foo" }, @project.aggregate_state[:detail])
149
+ end
150
+
151
+ def test_changes_getter_ignores_foreign_key_relationship_to_parent_object
152
+ build_persisted_state
153
+
154
+ @project.todo_lists.build
155
+
156
+ assert_equal({todo_lists: { 1 => {_create: '1' }}}, @project.aggregate_changes)
157
+ end
158
+
159
+ def test_all_changes_to_associated_objects_show_up_in_aggregate_changes
160
+ build_persisted_state
161
+
162
+ @project.name = "Clean My House"
163
+ @project.detail.description = "I need to clean my house"
164
+ @project.todo_lists.first.todos.first.text = "Take out my trash"
165
+ @project.todo_lists.first.todos.build({ text: "Another task!" })
166
+
167
+ assert_equal @changes, @project.aggregate_changes
168
+ end
169
+
170
+ def test_applying_changes_shows_up_in_model_and_its_associations
171
+ build_persisted_state
172
+
173
+ @project.aggregate_state = @state
174
+ @project.save!
175
+
176
+ assert_equal "Clean House", @project.name
177
+ assert_equal "I need to clean the house", @project.detail.description
178
+ assert_equal "Take out the trash", @project.todo_lists.first.todos.first.text
179
+ assert_equal 2, @project.todo_lists.first.todos.size
180
+
181
+ @project.aggregate_changes = @changes
182
+
183
+ assert_equal "Clean My House", @project.name
184
+ assert_equal "I need to clean my house", @project.detail.description
185
+ assert_equal "Take out my trash", @project.todo_lists.first.todos.first.text
186
+ assert_equal 3, @project.todo_lists.first.todos.size
187
+ end
188
+
189
+ def test_applying_reverse_changes_invokes_apply_change_on_the_reverse_hash
190
+ build_persisted_state
191
+
192
+ @project.aggregate_changes = @changes
193
+ @project.save!
194
+
195
+ assert_equal "Clean My House", @project.name
196
+ assert_equal "I need to clean my house", @project.detail.description
197
+ assert_equal "Take out my trash", @project.todo_lists.first.todos.first.text
198
+ assert_equal 3, @project.todo_lists.first.todos.size
199
+
200
+ @project.reverse_aggregate_changes = @changes
201
+ @project.save!
202
+
203
+ assert_equal "Clean House", @project.name
204
+ assert_equal "I need to clean the house", @project.detail.description
205
+ assert_equal "Take out the trash", @project.todo_lists.first.todos.first.text
206
+ assert_equal 2, @project.todo_lists.first.todos.size
207
+ end
208
+
209
+ def test_state_getter_symbolizes_all_keys
210
+ @project.name = "Foo"
211
+
212
+ assert_equal({ name: "Foo" }, @project.aggregate_state)
213
+ end
214
+
215
+ def test_state_setter_populates_object_graph
216
+ @project.aggregate_state = @state
217
+ assert_equal @state, @project.aggregate_state
218
+ end
219
+
220
+ def test_state_setter_marks_existing_associations_for_deletion
221
+ @project.aggregate_state = @state
222
+ @project.save
223
+
224
+ assert_equal 2, @project.todo_lists.first.todos.size
225
+
226
+ new_state = Marshal.load(Marshal.dump(@state))
227
+
228
+ new_state[:todo_lists].first[:todos].first.tap do |todo|
229
+ todo[:todo_assignments].pop
230
+ todo[:comments].pop
231
+ todo[:comments].unshift({author_id: 2, text: "Brand new comment"})
232
+ end
233
+ new_state.delete(:detail)
234
+
235
+ @project.aggregate_state = new_state
236
+ @project.save
237
+ @project.reload
238
+
239
+ @project.todo_lists.first.todos.first.tap do |todo|
240
+ assert_equal 1, todo.todo_assignments.size
241
+ assert_equal 2, todo.comments.size
242
+ assert_equal ["Brand new comment", "Have this done by Monday"], todo.comments.map(&:text)
243
+ end
244
+
245
+ assert_nil @project.detail
246
+ end
247
+
248
+ def test_state_setter_resets_unsupplied_attributes_to_default
249
+ @project.aggregate_state = @state.merge(status: 5)
250
+ @project.save
251
+
252
+ new_state = Marshal.load(Marshal.dump(@state))
253
+ new_state.delete(:status)
254
+
255
+ @project.aggregate_state = new_state
256
+
257
+ assert_equal Project.new.status, @project.status
258
+ end
259
+
260
+ def state_setter_can_set_timestamps
261
+ timestamp = (1.year.ago - 15.seconds)
262
+
263
+ @project.state = { created_at: timestamp, updated_at: timestamp + 14.days }
264
+ assert_equal timestamp, @project.created_at
265
+ assert_equal timestamp + 14.days, @project.updated_at
266
+ end
267
+
268
+ def test_state_getter_respects_serialized_attributes
269
+ @project.fruit = :apple
270
+ assert_equal 'ELPPA', @project.aggregate_state[:fruit]
271
+ end
272
+
273
+ def test_state_setter_respects_serialized_attributes
274
+ @project.aggregate_state = @state
275
+ assert_equal nil, @project.fruit
276
+
277
+ @state[:fruit] = 'EGNARO'
278
+ @project.aggregate_state = @state
279
+ assert_equal :orange, @project.fruit
280
+ end
281
+
282
+ def test_state_changes_getter_and_setter_respect_serialized_attributes
283
+ build_persisted_state
284
+
285
+ @project.fruit = :banana
286
+ assert_equal [nil, 'ANANAB'], @project.aggregate_changes[:fruit]
287
+ @project.save!
288
+ assert_equal :banana, @project.reload.fruit
289
+
290
+ @project.fruit = :pear
291
+ assert_equal ['ANANAB', 'RAEP'], @project.aggregate_changes[:fruit]
292
+
293
+ @project.reload
294
+ assert_equal :banana, @project.fruit
295
+ @project.aggregate_changes = { fruit: [ 'ANANAB', 'OGNAM'] }
296
+ assert_equal :mango, @project.fruit
297
+ end
298
+
299
+ private
300
+
301
+ # Test 'changes' behavior with this common background
302
+ def build_persisted_state
303
+ @project.name = "Clean House"
304
+ @project.owner_id = 1
305
+ @project.todo_lists.build({
306
+ todos_attributes: {
307
+ "0" => { text: "Take out the trash" },
308
+ "1" => { text: "Make your bed" },
309
+ },
310
+ })
311
+ @project.build_detail({ description: "I need to clean the house" })
312
+
313
+ @project.save
314
+
315
+ assert_equal({}, @project.aggregate_changes)
316
+ end
317
+
318
+ def reverse_changes
319
+ @changes = {
320
+ name: ["Clean My House", "Clean House"],
321
+ detail: {
322
+ description: ["I need to clean my house", "I need to clean the house"],
323
+ },
324
+ todo_lists: {
325
+ 0 => {
326
+ todos: {
327
+ 0 => { text: ["Take out my trash", "Take out the trash"] },
328
+ 2 => { _destroy: '1' },
329
+ },
330
+ },
331
+ },
332
+ }
333
+ end
334
+ end
@@ -0,0 +1,115 @@
1
+ ActiveRecord::Migration.create_table :users, force: true do |t|
2
+ t.string :name
3
+ end
4
+
5
+ ActiveRecord::Migration.create_table :comments, force: true do |t|
6
+ t.string :text
7
+ t.string :type
8
+ t.belongs_to :commentable, polymorphic: true
9
+ t.belongs_to :author
10
+ end
11
+
12
+ ActiveRecord::Migration.create_table :projects, force: true do |t|
13
+ t.string :name
14
+ t.belongs_to :owner
15
+ t.integer :status, default: 1, null: false
16
+ t.string :fruit
17
+ t.timestamps
18
+ end
19
+
20
+ ActiveRecord::Migration.create_table :project_details, force: true do |t|
21
+ t.belongs_to :project
22
+ t.text :description
23
+ end
24
+
25
+ ActiveRecord::Migration.create_table :project_todo_lists, force: true do |t|
26
+ t.belongs_to :project
27
+ t.timestamps
28
+ end
29
+
30
+ ActiveRecord::Migration.create_table :project_todos, force: true do |t|
31
+ t.string :text
32
+ t.integer :comments_count, default: 0, null: false
33
+ t.belongs_to :todo_list
34
+ end
35
+
36
+ ActiveRecord::Migration.create_table :project_todo_assignments, force: true do |t|
37
+ t.belongs_to :todo
38
+ t.belongs_to :assignee
39
+ end
40
+
41
+
42
+ User = Class.new(ActiveRecord::Base)
43
+
44
+ class Comment < ActiveRecord::Base
45
+ belongs_to :commentable, polymorphic: true, counter_cache: true
46
+ belongs_to :author, class_name: "User"
47
+ end
48
+
49
+ class Project < ActiveRecord::Base
50
+ act_as_aggregate_root!
51
+
52
+ belongs_to :owner, class_name: "User"
53
+
54
+ has_one :detail, inverse_of: :project, dependent: :destroy, autosave: true
55
+
56
+ has_many :todo_lists, validate: true, dependent: :destroy, inverse_of: :project, autosave: true
57
+ has_many :todos, through: :todo_lists # not part of aggregate
58
+
59
+ has_one :recent_todo_list, ->{ order("updated_at DESC") },
60
+ class_name: "Project::TodoList"
61
+
62
+ FruitSerializer = Class.new do
63
+ def self.dump(fruit)
64
+ return nil if fruit.nil?
65
+ fruit.to_s.reverse.upcase
66
+ end
67
+
68
+ def self.load(blob)
69
+ return nil if blob.nil?
70
+ blob.reverse.downcase.to_sym
71
+ end
72
+ end
73
+ serialize :fruit, FruitSerializer
74
+ end
75
+
76
+ class Project::Detail < ActiveRecord::Base
77
+ belongs_to :project, inverse_of: :detail
78
+ end
79
+
80
+ class Project::TodoList < ActiveRecord::Base
81
+ belongs_to :project, inverse_of: :todo_lists
82
+ has_many :todos, validate: true, dependent: :destroy, inverse_of: :todo_list
83
+
84
+ accepts_nested_attributes_for :todos
85
+ end
86
+
87
+ class Project::Todo < ActiveRecord::Base
88
+ belongs_to :todo_list, inverse_of: :todos
89
+ has_many :todo_assignments, inverse_of: :todo, dependent: :destroy, autosave: true
90
+
91
+ has_many :assignees, through: :todo_assignments
92
+ has_many :comments, inverse_of: :todo, foreign_key: "commentable_id"
93
+
94
+ validates :text, uniqueness: { scope: :todo_list_id }
95
+ end
96
+
97
+ class Project::TodoAssignment < ActiveRecord::Base
98
+ belongs_to :todo, inverse_of: :todo_assignments
99
+ belongs_to :assignee, class_name: "User"
100
+ end
101
+
102
+ class Project::Comment < Comment
103
+ belongs_to :todo, foreign_key: "commentable_id", inverse_of: :comments
104
+
105
+ default_scope { where({ commentable_type: "Project::Todo" }) }
106
+ end
107
+
108
+
109
+ # Instantiate an instance of each model class; this causes mistakes in the above
110
+ # code to fail fast.
111
+ models = ObjectSpace.each_object(Class).select do |klass|
112
+ klass < ActiveRecord::Base
113
+ end
114
+
115
+ models.each(&:new)