ruby_cqrs 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,332 @@
1
+ require_relative('../spec_helper')
2
+
3
+ describe RubyCqrs::Domain::AggregateRepository do
4
+ let(:unsorted_event_records) { [
5
+ { :aggregate_id => SomeDomain::AGGREGATE_ID,
6
+ :event_type => SomeDomain::SecondEvent.name,
7
+ :version => 2,
8
+ :data => '' },
9
+ { :aggregate_id => SomeDomain::AGGREGATE_ID,
10
+ :event_type => SomeDomain::FirstEvent.name,
11
+ :version => 1,
12
+ :data => '' } ] }
13
+ let(:aggregate_id) { SomeDomain::AGGREGATE_ID }
14
+ let(:event_store_load_result) { { :aggregate_id => aggregate_id,
15
+ :aggregate_type => 'SomeDomain::AggregateRoot',
16
+ :events => unsorted_event_records } }
17
+ let(:event_store) do
18
+ _event_store = (Class.new { include RubyCqrs::Data::EventStore}).new
19
+ allow(_event_store).to receive(:load_by).and_return(event_store_load_result)
20
+ _event_store
21
+ end
22
+ let(:command_context) { Object.new }
23
+ let(:repository) { RubyCqrs::Domain::AggregateRepository.new event_store, command_context }
24
+ let(:aggregate_type) { SomeDomain::AggregateRoot }
25
+
26
+ describe '#new' do
27
+ context 'when expecting arguments' do
28
+ it 'raises ArgumentError when the first argument is not an descendant from EventStore' do
29
+ expect { RubyCqrs::Domain::AggregateRepository.new Object.new, command_context }.to raise_error ArgumentError
30
+ end
31
+
32
+ it 'is initialized with an EventStore instance and an CommandContext instance' do
33
+ RubyCqrs::Domain::AggregateRepository.new event_store, command_context
34
+ end
35
+ end
36
+ end
37
+
38
+ describe '#find_by' do
39
+ context 'when expecting arguments' do
40
+ it 'raises ArgumentError when aggregate_id is nil' do
41
+ expect { repository.find_by nil }.to raise_error ArgumentError
42
+ end
43
+
44
+ it 'raises ArgumentError when aggregate_id is not a valid guid' do
45
+ expect { repository.find_by 'some_invalid_guid' }.to raise_error ArgumentError
46
+ end
47
+ end
48
+
49
+ it "delegates the actual data loading to the EventStore instance's #load_by" do
50
+ expect(event_store).to receive(:load_by) do |some_guid, some_command_context|
51
+ expect(some_guid).to be_a_valid_uuid
52
+ expect(some_command_context).to be(command_context)
53
+ end.and_return event_store_load_result
54
+
55
+ repository.find_by(aggregate_id)
56
+ end
57
+
58
+ context 'when the specified aggregate could not be found' do
59
+ let(:empty_event_store) do
60
+ _event_store = (Class.new { include RubyCqrs::Data::EventStore}).new
61
+ allow(_event_store).to receive(:load_by).and_return(nil)
62
+ _event_store
63
+ end
64
+ let(:matches_nothing_repository) { RubyCqrs::Domain::AggregateRepository.new\
65
+ empty_event_store, command_context }
66
+
67
+ it 'raises error of type AggregateNotFound' do
68
+ expect { matches_nothing_repository.find_by(aggregate_id) }.to \
69
+ raise_error(RubyCqrs::AggregateNotFound)
70
+ end
71
+ end
72
+
73
+ context 'when the specified aggregate is found' do
74
+ let(:aggregate) { repository.find_by(aggregate_id) }
75
+ let(:expeced_version) { unsorted_event_records.size }
76
+ let(:expeced_source_version) { unsorted_event_records.size }
77
+
78
+ it 'returns an instance of expected type' do
79
+ expect(aggregate).to be_an_instance_of(aggregate_type)
80
+ end
81
+
82
+ it 'returns an instance of expected aggregate_id' do
83
+ expect(aggregate.aggregate_id).to eq(aggregate_id)
84
+ end
85
+
86
+ it 'returns an instance of expected version' do
87
+ expect(aggregate.version).to eq(expeced_version)
88
+ end
89
+
90
+ it 'returns an instance of expected source_version' do
91
+ expect(aggregate.instance_variable_get(:@source_version)).to eq(expeced_source_version)
92
+ end
93
+ end
94
+ end
95
+
96
+ describe '#save' do
97
+ context 'when expecting arguments' do
98
+ it 'raises ArgumentError when given 0 or nil argument or an 0 length enumerable' do
99
+ expect { repository.save }.to raise_error ArgumentError
100
+ expect { repository.save nil }.to raise_error ArgumentError
101
+ expect { repository.save [] }.to raise_error ArgumentError
102
+ end
103
+ it 'raises ArgumentError when the first argument is not an descendant from AggregateBase' do
104
+ expect { repository.save Object.new }.to raise_error ArgumentError
105
+ end
106
+ it 'raises ArgumentError when the first argument is not an enumerable of AggregateBase' do
107
+ expect { repository.save [ Object.new ] }.to raise_error ArgumentError
108
+ end
109
+ end
110
+
111
+ describe 'during the saving process' do
112
+ context 'when saving a single aggregate' do
113
+ context 'when the aggregate has not been changed' do
114
+ let(:unchanged_aggregate) do
115
+ aggregate_type.new
116
+ end
117
+
118
+ it "short-circuit without calling the EventStore instance's #save" do
119
+ expect(event_store).to_not receive(:save)
120
+ repository.save unchanged_aggregate
121
+ end
122
+ end
123
+
124
+ context 'when the aggregate has been changed' do
125
+ let(:changed_aggregate) do
126
+ _aggregate = aggregate_type.new
127
+ _aggregate.test_fire
128
+ _aggregate.test_fire_ag
129
+ _aggregate
130
+ end
131
+
132
+ it "delegates event persistence to the EventStore instance's #save" do
133
+ expect(event_store).to receive(:save) do |aggregate_changes, some_command_context|
134
+ expect(aggregate_changes.size).to eq(1)
135
+ expect(aggregate_changes[0][:aggregate_id]).to eq(changed_aggregate.aggregate_id)
136
+ expect(aggregate_changes[0][:aggregate_type]).to eq(changed_aggregate.class.name)
137
+ expect(aggregate_changes[0][:events].size).to eq(2)
138
+ expect(aggregate_changes[0][:events][0][:aggregate_id]).to eq(changed_aggregate.aggregate_id)
139
+ expect(aggregate_changes[0][:events][0][:version]).to eq(1)
140
+ expect(aggregate_changes[0][:events][0][:event_type]).to eq(SomeDomain::ThirdEvent.name)
141
+ expect(aggregate_changes[0][:events][0][:data]).to_not be_nil
142
+ expect(aggregate_changes[0][:events][1][:aggregate_id]).to eq(changed_aggregate.aggregate_id)
143
+ expect(aggregate_changes[0][:events][1][:version]).to eq(2)
144
+ expect(aggregate_changes[0][:events][1][:event_type]).to eq(SomeDomain::ForthEvent.name)
145
+ expect(aggregate_changes[0][:events][1][:data]).to_not be_nil
146
+ expect(some_command_context).to be(command_context)
147
+ end
148
+
149
+ repository.save(changed_aggregate)
150
+ end
151
+
152
+ describe 'after the saving process finished successfully' do
153
+ it 'has both version and source_version set to the same value' do
154
+ expect(event_store).to receive(:save)
155
+
156
+ repository.save(changed_aggregate)
157
+
158
+ expect(changed_aggregate.version).to eq(2)
159
+ expect(changed_aggregate.instance_variable_get(:@source_version)).to eq(2)
160
+ end
161
+ end
162
+
163
+ describe 'if some error happened during the saving process' do
164
+ before(:each) do
165
+ expect(event_store).to receive(:save) { raise RubyCqrs::AggregateConcurrencyError }
166
+ end
167
+
168
+ it 'bubbles up that error directly' do
169
+ expect { repository.save(changed_aggregate) }.to\
170
+ raise_error(RubyCqrs::AggregateConcurrencyError)
171
+ end
172
+
173
+ it 'has source_version unchanged' do
174
+ original_source_version = changed_aggregate.instance_variable_get(:@source_version)
175
+
176
+ expect { repository.save(changed_aggregate) }.to\
177
+ raise_error(RubyCqrs::AggregateConcurrencyError)
178
+
179
+ expect(changed_aggregate.instance_variable_get(:@source_version)).to eq(original_source_version)
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ context 'when saving 2 instances of the same aggregate' do
186
+ it 'raises AggregateInstanceDuplicatedError and nothing should be changed' do
187
+ aggregate_1 = aggregate_type.new
188
+ aggregate_2 = aggregate_1.dup
189
+ aggregate_1.test_fire
190
+ aggregate_2.test_fire
191
+ aggregates = [ aggregate_1, aggregate_2 ]
192
+ expect{ repository.save aggregates }.to raise_error(RubyCqrs::AggregateInstanceDuplicatedError)
193
+ expect(aggregate_1.version).to_not eq(aggregate_1.instance_variable_get(:@source_verrsion))
194
+ expect(aggregate_2.version).to_not eq(aggregate_2.instance_variable_get(:@source_verrsion))
195
+ end
196
+ end
197
+
198
+ context 'when saving 2 aggregates' do
199
+ context 'when none of the aggregates have been changed' do
200
+ let(:unchanged_aggregates) do
201
+ [ aggregate_type.new, aggregate_type.new ]
202
+ end
203
+
204
+ it "short-circuit without calling the EventStore instance's #save" do
205
+ expect(event_store).to_not receive(:save)
206
+ repository.save unchanged_aggregates
207
+ end
208
+ end
209
+
210
+ context 'when one of the aggregates has been changed' do
211
+ let(:two_aggregates) do
212
+ _aggregate = aggregate_type.new
213
+ _aggregate.test_fire
214
+ _aggregate.test_fire_ag
215
+ [ _aggregate, aggregate_type.new ]
216
+ end
217
+
218
+ it "delegates event persistence to the EventStore instance's #save" do
219
+ expect(event_store).to receive(:save) do |aggregate_changes, some_command_context|
220
+ expect(aggregate_changes.size).to eq(1)
221
+ expect(some_command_context).to be(command_context)
222
+ end
223
+
224
+ repository.save(two_aggregates)
225
+ end
226
+
227
+ describe 'the aggregate that changed, after the saving process finished successfully' do
228
+ it 'has both version and source_version set to the same value' do
229
+ expect(event_store).to receive(:save)
230
+
231
+ repository.save(two_aggregates)
232
+
233
+ expect(two_aggregates[0].version).to eq(2)
234
+ expect(two_aggregates[0].instance_variable_get(:@source_version)).to eq(2)
235
+ end
236
+ end
237
+
238
+ describe 'the aggregate that did not change, after the saving process finished successfully' do
239
+ it 'has both version and source_version unchanged' do
240
+ expect(event_store).to receive(:save)
241
+ original_version = two_aggregates[1].version
242
+ original_source_version = two_aggregates[1].instance_variable_get(:@source_version)
243
+
244
+ repository.save(two_aggregates)
245
+
246
+ expect(two_aggregates[1].version).to eq(original_version)
247
+ expect(two_aggregates[1].instance_variable_get(:@source_version)).to eq(original_source_version)
248
+ end
249
+ end
250
+
251
+ describe 'if some error happened during the saving process' do
252
+ before(:each) do
253
+ expect(event_store).to receive(:save) { raise RubyCqrs::AggregateConcurrencyError }
254
+ end
255
+
256
+ it 'bubbles up that error directly' do
257
+ expect { repository.save(two_aggregates) }.to\
258
+ raise_error(RubyCqrs::AggregateConcurrencyError)
259
+ end
260
+
261
+ it 'has source_version unchanged' do
262
+ original_source_version_0 = two_aggregates[0].instance_variable_get(:@source_version)
263
+ original_source_version_1 = two_aggregates[1].instance_variable_get(:@source_version)
264
+
265
+ expect { repository.save(two_aggregates) }.to\
266
+ raise_error(RubyCqrs::AggregateConcurrencyError)
267
+
268
+ expect(two_aggregates[0].instance_variable_get(:@source_version)).to eq(original_source_version_0)
269
+ expect(two_aggregates[1].instance_variable_get(:@source_version)).to eq(original_source_version_1)
270
+ end
271
+ end
272
+ end
273
+
274
+ context 'when both aggregates have been changed' do
275
+ let(:two_aggregates) do
276
+ _aggregate_1 = aggregate_type.new
277
+ _aggregate_2 = aggregate_type.new
278
+ _aggregate_1.test_fire
279
+ _aggregate_1.test_fire_ag
280
+ _aggregate_2.test_fire
281
+ _aggregate_2.test_fire_ag
282
+ [ _aggregate_2, _aggregate_1 ]
283
+ end
284
+
285
+ it "delegates event persistence to the EventStore instance's #save" do
286
+ expect(event_store).to receive(:save) do |aggregate_changes, some_command_context|
287
+ expect(aggregate_changes.size).to eq(2)
288
+ expect(some_command_context).to be(command_context)
289
+ end
290
+
291
+ repository.save(two_aggregates)
292
+ end
293
+
294
+ describe 'after the saving process finished successfully' do
295
+ it "has both aggregates' version and source_version set to the same value" do
296
+ expect(event_store).to receive(:save)
297
+
298
+ repository.save(two_aggregates)
299
+
300
+ expect(two_aggregates[0].version).to eq(2)
301
+ expect(two_aggregates[0].instance_variable_get(:@source_version)).to eq(2)
302
+ expect(two_aggregates[1].version).to eq(2)
303
+ expect(two_aggregates[1].instance_variable_get(:@source_version)).to eq(2)
304
+ end
305
+ end
306
+
307
+ describe 'if something wrong happened during the saving process' do
308
+ before(:each) do
309
+ expect(event_store).to receive(:save) { raise RubyCqrs::AggregateConcurrencyError }
310
+ end
311
+
312
+ it 'bubbles up that error directly' do
313
+ expect { repository.save(two_aggregates) }.to\
314
+ raise_error(RubyCqrs::AggregateConcurrencyError)
315
+ end
316
+
317
+ it 'has source_version unchanged' do
318
+ original_source_version_0 = two_aggregates[0].instance_variable_get(:@source_version)
319
+ original_source_version_1 = two_aggregates[1].instance_variable_get(:@source_version)
320
+
321
+ expect { repository.save(two_aggregates) }.to\
322
+ raise_error(RubyCqrs::AggregateConcurrencyError)
323
+
324
+ expect(two_aggregates[0].instance_variable_get(:@source_version)).to eq(original_source_version_0)
325
+ expect(two_aggregates[1].instance_variable_get(:@source_version)).to eq(original_source_version_1)
326
+ end
327
+ end
328
+ end
329
+ end
330
+ end
331
+ end
332
+ end
@@ -0,0 +1,123 @@
1
+ require_relative('../spec_helper')
2
+
3
+ describe RubyCqrs::Domain::Aggregate do
4
+ let(:aggregate_id) { SomeDomain::AGGREGATE_ID }
5
+ let(:aggregate) { SomeDomain::AggregateRoot.new }
6
+ let(:unsorted_events) { [ SomeDomain::SecondEvent.new, SomeDomain::FirstEvent.new ] }
7
+
8
+ describe '#new' do
9
+ it 'has aggregate_id initilized as a valid uuid' do
10
+ expect(aggregate.aggregate_id).to be_a_valid_uuid
11
+ end
12
+
13
+ it 'has version initilized as 0' do
14
+ expect(aggregate.version).to be_zero
15
+ end
16
+
17
+ it 'has source_version initilized as 0' do
18
+ expect(aggregate.instance_variable_get(:@source_version)).to be_zero
19
+ end
20
+ end
21
+
22
+ describe '#raise_event' do
23
+ it 'raise NotADomainEventError when raising an object that is not a proper event' do
24
+ expect { aggregate.fire_weird_stuff }.to raise_error(RubyCqrs::NotADomainEventError)
25
+ end
26
+
27
+ context 'after raising an event' do
28
+ it 'has version increased by 1' do
29
+ original_version = aggregate.version
30
+ aggregate.test_fire
31
+
32
+ expect(aggregate.version).to eq(original_version + 1)
33
+ end
34
+
35
+ it 'leaves source_version unchanged' do
36
+ original_source_version = aggregate.instance_variable_get(:@source_version)
37
+ aggregate.test_fire
38
+
39
+ expect(aggregate.instance_variable_get(:@source_version)).to eq original_source_version
40
+ end
41
+
42
+ it 'calls #on_third_event' do
43
+ expect(aggregate).to receive(:on_third_event)
44
+ aggregate.test_fire
45
+ end
46
+ end
47
+ end
48
+
49
+ describe '#is_version_conflicted?' do
50
+ let(:state) { { :aggregate_id => aggregate_id, :events => unsorted_events } }
51
+ let(:loaded_aggregate) { aggregate.send(:load_from, state); aggregate; }
52
+
53
+ it 'returns true when supplied client side version does not match the server side persisted source_version' do
54
+ client_side_version = unsorted_events.size - 1
55
+ expect(loaded_aggregate.is_version_conflicted? client_side_version).to be_truthy
56
+ end
57
+
58
+ it 'returns false when supplied client side version matches the server side persisted source_version' do
59
+ client_side_version = unsorted_events.size
60
+ expect(loaded_aggregate.is_version_conflicted? client_side_version).to be_falsy
61
+ end
62
+ end
63
+
64
+ describe '#get_changes' do
65
+ context 'after raising no event' do
66
+ it 'returns nil' do
67
+ expect(aggregate.send(:get_changes)).to be_nil
68
+ end
69
+ end
70
+
71
+ context 'after raising 2 events' do
72
+ it 'returns proper change summary' do
73
+ aggregate.test_fire
74
+ aggregate.test_fire_ag
75
+ pending_changes = aggregate.send(:get_changes)
76
+
77
+ expect(pending_changes[:events].size).to eq(2)
78
+ expect(pending_changes[:events][0].version).to eq(1)
79
+ expect(pending_changes[:events][1].version).to eq(2)
80
+ expect(pending_changes[:aggregate_id]).to eq(aggregate.aggregate_id)
81
+ expect(pending_changes[:aggregate_type]).to eq(aggregate.class.name)
82
+ expect(pending_changes[:expecting_source_version]).to eq(0)
83
+ expect(pending_changes[:expecting_version]).to eq(2)
84
+ end
85
+ end
86
+ end
87
+
88
+ describe '#load_from' do
89
+ let(:state) { { :aggregate_id => aggregate_id, :events => unsorted_events } }
90
+ let(:loaded_aggregate) { aggregate.send(:load_from, state); aggregate; }
91
+
92
+ context 'when loading events' do
93
+ after(:each) { aggregate.send(:load_from, state) }
94
+
95
+ it 'calls #on_first_event' do
96
+ expect(aggregate).to receive(:on_first_event)
97
+ end
98
+
99
+ it 'calls #on_second_event' do
100
+ expect(aggregate).to receive(:on_second_event)
101
+ end
102
+
103
+ it 'calls #on_first_event, #on_second_event in order' do
104
+ expect(aggregate).to receive(:on_first_event).ordered
105
+ expect(aggregate).to receive(:on_second_event).ordered
106
+ end
107
+ end
108
+
109
+ context 'after events are loaded' do
110
+ it "has aggregate_id set to the events' aggregate_id" do
111
+ expect(loaded_aggregate.aggregate_id).to eq(aggregate_id)
112
+ end
113
+
114
+ it 'has version set to the number of loaded events' do
115
+ expect(loaded_aggregate.version).to eq(unsorted_events.size)
116
+ end
117
+
118
+ it 'has source_version set to the number of loaded events' do
119
+ expect(loaded_aggregate.instance_variable_get(:@source_version)).to eq(unsorted_events.size)
120
+ end
121
+ end
122
+ end
123
+ end