activeshepherd 0.8.0

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.
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)