ruby_cqrs 0.2.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.
@@ -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