@signaltree/events 7.3.1
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.
- package/angular.d.ts +1 -0
- package/angular.esm.js +547 -0
- package/factory.esm.js +178 -0
- package/idempotency.esm.js +701 -0
- package/index.d.ts +1 -0
- package/index.esm.js +167 -0
- package/nestjs.d.ts +1 -0
- package/nestjs.esm.js +944 -0
- package/package.json +110 -0
- package/src/angular/handlers.d.ts +132 -0
- package/src/angular/index.d.ts +12 -0
- package/src/angular/optimistic-updates.d.ts +117 -0
- package/src/angular/websocket.service.d.ts +158 -0
- package/src/angular.d.ts +7 -0
- package/src/core/error-classification.d.ts +100 -0
- package/src/core/factory.d.ts +114 -0
- package/src/core/idempotency.d.ts +209 -0
- package/src/core/registry.d.ts +147 -0
- package/src/core/types.d.ts +127 -0
- package/src/core/validation.d.ts +619 -0
- package/src/index.d.ts +56 -0
- package/src/nestjs/base.subscriber.d.ts +169 -0
- package/src/nestjs/decorators.d.ts +37 -0
- package/src/nestjs/dlq.service.d.ts +117 -0
- package/src/nestjs/event-bus.module.d.ts +117 -0
- package/src/nestjs/event-bus.service.d.ts +114 -0
- package/src/nestjs/index.d.ts +16 -0
- package/src/nestjs/tokens.d.ts +8 -0
- package/src/nestjs.d.ts +7 -0
- package/src/testing/assertions.d.ts +113 -0
- package/src/testing/factories.d.ts +106 -0
- package/src/testing/helpers.d.ts +104 -0
- package/src/testing/index.d.ts +13 -0
- package/src/testing/mock-event-bus.d.ts +144 -0
- package/src/testing.d.ts +7 -0
- package/testing.d.ts +1 -0
- package/testing.esm.js +743 -0
package/testing.esm.js
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
import { b as generateCorrelationId, g as generateEventId, D as DEFAULT_EVENT_VERSION } from './factory.esm.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mock Event Bus for testing
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* const eventBus = createMockEventBus();
|
|
9
|
+
*
|
|
10
|
+
* // Subscribe to events
|
|
11
|
+
* eventBus.subscribe('TradeProposalCreated', (event) => {
|
|
12
|
+
* expect(event.data.tradeId).toBe('123');
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* // Publish an event
|
|
16
|
+
* await eventBus.publish({
|
|
17
|
+
* type: 'TradeProposalCreated',
|
|
18
|
+
* data: { tradeId: '123' },
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Assert published events
|
|
22
|
+
* expect(eventBus.getPublishedEvents()).toHaveLength(1);
|
|
23
|
+
* expect(eventBus.wasPublished('TradeProposalCreated')).toBe(true);
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
class MockEventBus {
|
|
27
|
+
options;
|
|
28
|
+
publishedEvents = [];
|
|
29
|
+
subscriptions = new Map();
|
|
30
|
+
allSubscriptions = new Set();
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.options = options;
|
|
33
|
+
this.options = {
|
|
34
|
+
simulateAsync: false,
|
|
35
|
+
asyncDelayMs: 10,
|
|
36
|
+
autoGenerateIds: true,
|
|
37
|
+
throwOnError: false,
|
|
38
|
+
...options
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Publish an event
|
|
43
|
+
*/
|
|
44
|
+
async publish(event, options) {
|
|
45
|
+
// Complete the event
|
|
46
|
+
const fullEvent = {
|
|
47
|
+
...event,
|
|
48
|
+
id: event.id ?? (this.options.autoGenerateIds ? generateEventId() : 'test-event-id'),
|
|
49
|
+
timestamp: event.timestamp ?? new Date().toISOString(),
|
|
50
|
+
correlationId: event.correlationId ?? generateCorrelationId()
|
|
51
|
+
};
|
|
52
|
+
const queue = options?.queue ?? this.getQueueForPriority(fullEvent.priority);
|
|
53
|
+
// Record the published event
|
|
54
|
+
this.publishedEvents.push({
|
|
55
|
+
event: fullEvent,
|
|
56
|
+
queue,
|
|
57
|
+
publishedAt: new Date(),
|
|
58
|
+
delay: options?.delay
|
|
59
|
+
});
|
|
60
|
+
// Simulate async if configured
|
|
61
|
+
if (this.options.simulateAsync) {
|
|
62
|
+
await this.delay(this.options.asyncDelayMs ?? 10);
|
|
63
|
+
}
|
|
64
|
+
// Notify subscribers
|
|
65
|
+
await this.notifySubscribers(fullEvent);
|
|
66
|
+
return {
|
|
67
|
+
eventId: fullEvent.id,
|
|
68
|
+
queue
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Publish multiple events
|
|
73
|
+
*/
|
|
74
|
+
async publishBatch(events, options) {
|
|
75
|
+
const correlationId = generateCorrelationId();
|
|
76
|
+
return Promise.all(events.map(event => this.publish({
|
|
77
|
+
...event,
|
|
78
|
+
correlationId
|
|
79
|
+
}, {
|
|
80
|
+
...options
|
|
81
|
+
})));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Subscribe to a specific event type
|
|
85
|
+
*/
|
|
86
|
+
subscribe(eventType, handler) {
|
|
87
|
+
let handlers = this.subscriptions.get(eventType);
|
|
88
|
+
if (!handlers) {
|
|
89
|
+
handlers = new Set();
|
|
90
|
+
this.subscriptions.set(eventType, handlers);
|
|
91
|
+
}
|
|
92
|
+
handlers.add(handler);
|
|
93
|
+
// Return unsubscribe function
|
|
94
|
+
// handlers is guaranteed to exist at this point since we just set it above
|
|
95
|
+
return () => {
|
|
96
|
+
const h = this.subscriptions.get(eventType);
|
|
97
|
+
if (h) {
|
|
98
|
+
h.delete(handler);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Subscribe to all events
|
|
104
|
+
*/
|
|
105
|
+
subscribeAll(handler) {
|
|
106
|
+
this.allSubscriptions.add(handler);
|
|
107
|
+
return () => {
|
|
108
|
+
this.allSubscriptions.delete(handler);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get all published events
|
|
113
|
+
*/
|
|
114
|
+
getPublishedEvents() {
|
|
115
|
+
return [...this.publishedEvents];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get published events by type
|
|
119
|
+
*/
|
|
120
|
+
getPublishedEventsByType(type) {
|
|
121
|
+
return this.publishedEvents.filter(p => p.event.type === type);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get the last published event
|
|
125
|
+
*/
|
|
126
|
+
getLastPublishedEvent() {
|
|
127
|
+
return this.publishedEvents[this.publishedEvents.length - 1];
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get the last published event of a specific type
|
|
131
|
+
*/
|
|
132
|
+
getLastPublishedEventByType(type) {
|
|
133
|
+
const events = this.getPublishedEventsByType(type);
|
|
134
|
+
return events[events.length - 1];
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Check if an event type was published
|
|
138
|
+
*/
|
|
139
|
+
wasPublished(eventType) {
|
|
140
|
+
return this.publishedEvents.some(p => p.event.type === eventType);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Check if an event with specific data was published
|
|
144
|
+
*/
|
|
145
|
+
wasPublishedWith(eventType, predicate) {
|
|
146
|
+
return this.publishedEvents.some(p => p.event.type === eventType && predicate(p.event));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get count of published events
|
|
150
|
+
*/
|
|
151
|
+
getPublishedCount(eventType) {
|
|
152
|
+
if (eventType) {
|
|
153
|
+
return this.publishedEvents.filter(p => p.event.type === eventType).length;
|
|
154
|
+
}
|
|
155
|
+
return this.publishedEvents.length;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Clear all published events (for test cleanup)
|
|
159
|
+
*/
|
|
160
|
+
clearHistory() {
|
|
161
|
+
this.publishedEvents = [];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Clear all subscriptions
|
|
165
|
+
*/
|
|
166
|
+
clearSubscriptions() {
|
|
167
|
+
this.subscriptions.clear();
|
|
168
|
+
this.allSubscriptions.clear();
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Reset the mock (clear history and subscriptions)
|
|
172
|
+
*/
|
|
173
|
+
reset() {
|
|
174
|
+
this.clearHistory();
|
|
175
|
+
this.clearSubscriptions();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Simulate an incoming event (as if received from server)
|
|
179
|
+
*/
|
|
180
|
+
async simulateIncomingEvent(event) {
|
|
181
|
+
await this.notifySubscribers(event);
|
|
182
|
+
}
|
|
183
|
+
// Private methods
|
|
184
|
+
async notifySubscribers(event) {
|
|
185
|
+
const errors = [];
|
|
186
|
+
// Notify type-specific subscribers
|
|
187
|
+
const handlers = this.subscriptions.get(event.type);
|
|
188
|
+
if (handlers) {
|
|
189
|
+
for (const handler of handlers) {
|
|
190
|
+
try {
|
|
191
|
+
await handler(event);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Notify all-event subscribers
|
|
198
|
+
for (const handler of this.allSubscriptions) {
|
|
199
|
+
try {
|
|
200
|
+
await handler(event);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (errors.length > 0 && this.options.throwOnError) {
|
|
206
|
+
throw new AggregateError(errors, 'Subscriber errors');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
getQueueForPriority(priority) {
|
|
210
|
+
switch (priority) {
|
|
211
|
+
case 'critical':
|
|
212
|
+
return 'events-critical';
|
|
213
|
+
case 'high':
|
|
214
|
+
return 'events-high';
|
|
215
|
+
case 'low':
|
|
216
|
+
return 'events-low';
|
|
217
|
+
case 'bulk':
|
|
218
|
+
return 'events-bulk';
|
|
219
|
+
default:
|
|
220
|
+
return 'events-normal';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
delay(ms) {
|
|
224
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Create a mock event bus for testing
|
|
229
|
+
*/
|
|
230
|
+
function createMockEventBus(options) {
|
|
231
|
+
return new MockEventBus(options);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Default test actor
|
|
236
|
+
*/
|
|
237
|
+
const DEFAULT_TEST_ACTOR = {
|
|
238
|
+
id: 'test-user-1',
|
|
239
|
+
type: 'user',
|
|
240
|
+
name: 'Test User'
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* Default test metadata
|
|
244
|
+
*/
|
|
245
|
+
const DEFAULT_TEST_METADATA = {
|
|
246
|
+
source: 'test',
|
|
247
|
+
environment: 'test'
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Create a test event
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```typescript
|
|
254
|
+
* const event = createTestEvent('TradeProposalCreated', {
|
|
255
|
+
* tradeId: '123',
|
|
256
|
+
* initiatorId: 'user-1',
|
|
257
|
+
* recipientId: 'user-2',
|
|
258
|
+
* });
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
function createTestEvent(type, data, options = {}) {
|
|
262
|
+
return {
|
|
263
|
+
id: options.id ?? generateEventId(),
|
|
264
|
+
type,
|
|
265
|
+
version: options.version ?? DEFAULT_EVENT_VERSION,
|
|
266
|
+
timestamp: options.timestamp ?? new Date().toISOString(),
|
|
267
|
+
correlationId: options.correlationId ?? generateCorrelationId(),
|
|
268
|
+
causationId: options.causationId,
|
|
269
|
+
actor: {
|
|
270
|
+
...DEFAULT_TEST_ACTOR,
|
|
271
|
+
...options.actor
|
|
272
|
+
},
|
|
273
|
+
metadata: {
|
|
274
|
+
...DEFAULT_TEST_METADATA,
|
|
275
|
+
...options.metadata
|
|
276
|
+
},
|
|
277
|
+
data,
|
|
278
|
+
priority: options.priority,
|
|
279
|
+
aggregate: options.aggregate
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Create a test event factory
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ```typescript
|
|
287
|
+
* const factory = createTestEventFactory({
|
|
288
|
+
* defaultActor: { id: 'test-user', type: 'user' },
|
|
289
|
+
* defaultMetadata: { source: 'test-service' },
|
|
290
|
+
* });
|
|
291
|
+
*
|
|
292
|
+
* const event = factory.create('TradeProposalCreated', {
|
|
293
|
+
* tradeId: '123',
|
|
294
|
+
* });
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
function createTestEventFactory(config) {
|
|
298
|
+
const defaultActor = {
|
|
299
|
+
...DEFAULT_TEST_ACTOR,
|
|
300
|
+
...config?.defaultActor
|
|
301
|
+
};
|
|
302
|
+
const defaultMetadata = {
|
|
303
|
+
...DEFAULT_TEST_METADATA,
|
|
304
|
+
...config?.defaultMetadata
|
|
305
|
+
};
|
|
306
|
+
return {
|
|
307
|
+
create(type, data, options = {}) {
|
|
308
|
+
return {
|
|
309
|
+
id: options.id ?? generateEventId(),
|
|
310
|
+
type,
|
|
311
|
+
version: options.version ?? DEFAULT_EVENT_VERSION,
|
|
312
|
+
timestamp: options.timestamp ?? new Date().toISOString(),
|
|
313
|
+
correlationId: options.correlationId ?? generateCorrelationId(),
|
|
314
|
+
causationId: options.causationId,
|
|
315
|
+
actor: {
|
|
316
|
+
...defaultActor,
|
|
317
|
+
...options.actor
|
|
318
|
+
},
|
|
319
|
+
metadata: {
|
|
320
|
+
...defaultMetadata,
|
|
321
|
+
...options.metadata
|
|
322
|
+
},
|
|
323
|
+
data,
|
|
324
|
+
priority: options.priority,
|
|
325
|
+
aggregate: options.aggregate
|
|
326
|
+
};
|
|
327
|
+
},
|
|
328
|
+
createMany(type, dataArray, options = {}) {
|
|
329
|
+
const correlationId = options.correlationId ?? generateCorrelationId();
|
|
330
|
+
return dataArray.map((data, index) => this.create(type, data, {
|
|
331
|
+
...options,
|
|
332
|
+
correlationId,
|
|
333
|
+
causationId: index > 0 ? undefined : options.causationId
|
|
334
|
+
}));
|
|
335
|
+
},
|
|
336
|
+
createRandom(type, overrides = {}, options = {}) {
|
|
337
|
+
// Get random generator for this type if available
|
|
338
|
+
const generator = config?.randomGenerators?.[type];
|
|
339
|
+
const randomData = generator ? generator() : {};
|
|
340
|
+
const overrideData = overrides ? overrides : {};
|
|
341
|
+
return this.create(type, {
|
|
342
|
+
...randomData,
|
|
343
|
+
...overrideData
|
|
344
|
+
}, options);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Event assertions class
|
|
351
|
+
*/
|
|
352
|
+
class EventAssertions {
|
|
353
|
+
publishedEvents;
|
|
354
|
+
constructor(events) {
|
|
355
|
+
this.publishedEvents = events;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Assert that an event was published
|
|
359
|
+
*/
|
|
360
|
+
toHavePublished(eventType) {
|
|
361
|
+
const found = this.publishedEvents.some(p => p.event.type === eventType);
|
|
362
|
+
return {
|
|
363
|
+
passed: found,
|
|
364
|
+
message: found ? `Event "${eventType}" was published` : `Expected event "${eventType}" to be published, but it was not`,
|
|
365
|
+
expected: eventType,
|
|
366
|
+
actual: this.publishedEvents.map(p => p.event.type)
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Assert that an event was NOT published
|
|
371
|
+
*/
|
|
372
|
+
toNotHavePublished(eventType) {
|
|
373
|
+
const found = this.publishedEvents.some(p => p.event.type === eventType);
|
|
374
|
+
return {
|
|
375
|
+
passed: !found,
|
|
376
|
+
message: !found ? `Event "${eventType}" was not published` : `Expected event "${eventType}" to NOT be published, but it was`,
|
|
377
|
+
expected: `NOT ${eventType}`,
|
|
378
|
+
actual: this.publishedEvents.map(p => p.event.type)
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Assert the number of published events
|
|
383
|
+
*/
|
|
384
|
+
toHavePublishedCount(count, eventType) {
|
|
385
|
+
const events = eventType ? this.publishedEvents.filter(p => p.event.type === eventType) : this.publishedEvents;
|
|
386
|
+
const actual = events.length;
|
|
387
|
+
const passed = actual === count;
|
|
388
|
+
return {
|
|
389
|
+
passed,
|
|
390
|
+
message: passed ? `Published ${count} events${eventType ? ` of type "${eventType}"` : ''}` : `Expected ${count} events${eventType ? ` of type "${eventType}"` : ''}, got ${actual}`,
|
|
391
|
+
expected: count,
|
|
392
|
+
actual
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Assert event was published with specific data
|
|
397
|
+
*/
|
|
398
|
+
toHavePublishedWith(eventType, predicate) {
|
|
399
|
+
const matching = this.publishedEvents.find(p => p.event.type === eventType && predicate(p.event));
|
|
400
|
+
return {
|
|
401
|
+
passed: !!matching,
|
|
402
|
+
message: matching ? `Found event "${eventType}" matching predicate` : `Expected to find event "${eventType}" matching predicate`,
|
|
403
|
+
expected: eventType,
|
|
404
|
+
actual: matching?.event
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Assert events were published in order
|
|
409
|
+
*/
|
|
410
|
+
toHavePublishedInOrder(eventTypes) {
|
|
411
|
+
const publishedTypes = this.publishedEvents.map(p => p.event.type);
|
|
412
|
+
let typeIndex = 0;
|
|
413
|
+
for (const publishedType of publishedTypes) {
|
|
414
|
+
if (publishedType === eventTypes[typeIndex]) {
|
|
415
|
+
typeIndex++;
|
|
416
|
+
if (typeIndex === eventTypes.length) break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const passed = typeIndex === eventTypes.length;
|
|
420
|
+
return {
|
|
421
|
+
passed,
|
|
422
|
+
message: passed ? `Events published in expected order` : `Expected events in order: [${eventTypes.join(', ')}], got: [${publishedTypes.join(', ')}]`,
|
|
423
|
+
expected: eventTypes,
|
|
424
|
+
actual: publishedTypes
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Assert event has valid structure
|
|
429
|
+
*/
|
|
430
|
+
toBeValidEvent(event) {
|
|
431
|
+
const issues = [];
|
|
432
|
+
if (!event.id) issues.push('Missing id');
|
|
433
|
+
if (!event.type) issues.push('Missing type');
|
|
434
|
+
if (!event.timestamp) issues.push('Missing timestamp');
|
|
435
|
+
if (!event.correlationId) issues.push('Missing correlationId');
|
|
436
|
+
if (!event.version) issues.push('Missing version');
|
|
437
|
+
if (!event.actor) issues.push('Missing actor');
|
|
438
|
+
if (!event.metadata) issues.push('Missing metadata');
|
|
439
|
+
if (event.actor) {
|
|
440
|
+
if (!event.actor.id) issues.push('Missing actor.id');
|
|
441
|
+
if (!event.actor.type) issues.push('Missing actor.type');
|
|
442
|
+
}
|
|
443
|
+
if (event.metadata) {
|
|
444
|
+
if (!event.metadata.source) issues.push('Missing metadata.source');
|
|
445
|
+
}
|
|
446
|
+
const passed = issues.length === 0;
|
|
447
|
+
return {
|
|
448
|
+
passed,
|
|
449
|
+
message: passed ? 'Event has valid structure' : `Event has invalid structure: ${issues.join(', ')}`,
|
|
450
|
+
expected: 'Valid event structure',
|
|
451
|
+
actual: issues
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Assert all events have same correlation ID
|
|
456
|
+
*/
|
|
457
|
+
toHaveSameCorrelationId() {
|
|
458
|
+
if (this.publishedEvents.length === 0) {
|
|
459
|
+
return {
|
|
460
|
+
passed: true,
|
|
461
|
+
message: 'No events to check'
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const correlationId = this.publishedEvents[0].event.correlationId;
|
|
465
|
+
const allMatch = this.publishedEvents.every(p => p.event.correlationId === correlationId);
|
|
466
|
+
return {
|
|
467
|
+
passed: allMatch,
|
|
468
|
+
message: allMatch ? `All events have correlation ID: ${correlationId}` : 'Events have different correlation IDs',
|
|
469
|
+
expected: correlationId,
|
|
470
|
+
actual: this.publishedEvents.map(p => p.event.correlationId)
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Assert event was published to specific queue
|
|
475
|
+
*/
|
|
476
|
+
toHavePublishedToQueue(eventType, queue) {
|
|
477
|
+
const matching = this.publishedEvents.find(p => p.event.type === eventType && p.queue === queue);
|
|
478
|
+
return {
|
|
479
|
+
passed: !!matching,
|
|
480
|
+
message: matching ? `Event "${eventType}" was published to queue "${queue}"` : `Expected event "${eventType}" to be published to queue "${queue}"`,
|
|
481
|
+
expected: {
|
|
482
|
+
eventType,
|
|
483
|
+
queue
|
|
484
|
+
},
|
|
485
|
+
actual: this.publishedEvents.filter(p => p.event.type === eventType).map(p => ({
|
|
486
|
+
type: p.event.type,
|
|
487
|
+
queue: p.queue
|
|
488
|
+
}))
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Get all assertions as an array
|
|
493
|
+
*/
|
|
494
|
+
getAllResults() {
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Create event assertions helper
|
|
500
|
+
*
|
|
501
|
+
* @example
|
|
502
|
+
* ```typescript
|
|
503
|
+
* const eventBus = createMockEventBus();
|
|
504
|
+
* // ... publish events ...
|
|
505
|
+
*
|
|
506
|
+
* const assertions = createEventAssertions(eventBus.getPublishedEvents());
|
|
507
|
+
*
|
|
508
|
+
* expect(assertions.toHavePublished('TradeCreated').passed).toBe(true);
|
|
509
|
+
* expect(assertions.toHavePublishedCount(3).passed).toBe(true);
|
|
510
|
+
* expect(assertions.toHavePublishedInOrder(['TradeCreated', 'TradeAccepted']).passed).toBe(true);
|
|
511
|
+
* ```
|
|
512
|
+
*/
|
|
513
|
+
function createEventAssertions(events) {
|
|
514
|
+
return new EventAssertions(events);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Test Helpers - Utility functions for testing
|
|
519
|
+
*
|
|
520
|
+
* Provides:
|
|
521
|
+
* - Event waiting utilities
|
|
522
|
+
* - Mock implementations
|
|
523
|
+
* - Test fixtures
|
|
524
|
+
*/
|
|
525
|
+
/**
|
|
526
|
+
* Wait for an event to be published
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* ```typescript
|
|
530
|
+
* const eventPromise = waitForEvent(eventBus, 'TradeCreated');
|
|
531
|
+
* await performAction();
|
|
532
|
+
* const event = await eventPromise;
|
|
533
|
+
* expect(event.data.tradeId).toBe('123');
|
|
534
|
+
* ```
|
|
535
|
+
*/
|
|
536
|
+
function waitForEvent(eventBus, eventType, timeoutMs = 5000) {
|
|
537
|
+
return new Promise((resolve, reject) => {
|
|
538
|
+
const timeout = setTimeout(() => {
|
|
539
|
+
unsubscribe();
|
|
540
|
+
reject(new Error(`Timeout waiting for event "${eventType}" after ${timeoutMs}ms`));
|
|
541
|
+
}, timeoutMs);
|
|
542
|
+
const unsubscribe = eventBus.subscribe(eventType, event => {
|
|
543
|
+
clearTimeout(timeout);
|
|
544
|
+
unsubscribe();
|
|
545
|
+
resolve(event);
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Wait for multiple events to be published
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* ```typescript
|
|
554
|
+
* const eventsPromise = waitForEvents(eventBus, ['TradeCreated', 'NotificationSent']);
|
|
555
|
+
* await performAction();
|
|
556
|
+
* const events = await eventsPromise;
|
|
557
|
+
* ```
|
|
558
|
+
*/
|
|
559
|
+
function waitForEvents(eventBus, eventTypes, timeoutMs = 5000) {
|
|
560
|
+
return new Promise((resolve, reject) => {
|
|
561
|
+
const remaining = new Set(eventTypes);
|
|
562
|
+
const collected = [];
|
|
563
|
+
const unsubscribes = [];
|
|
564
|
+
const timeout = setTimeout(() => {
|
|
565
|
+
unsubscribes.forEach(unsub => unsub());
|
|
566
|
+
reject(new Error(`Timeout waiting for events: [${Array.from(remaining).join(', ')}] after ${timeoutMs}ms`));
|
|
567
|
+
}, timeoutMs);
|
|
568
|
+
const checkComplete = () => {
|
|
569
|
+
if (remaining.size === 0) {
|
|
570
|
+
clearTimeout(timeout);
|
|
571
|
+
unsubscribes.forEach(unsub => unsub());
|
|
572
|
+
resolve(collected);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
for (const eventType of eventTypes) {
|
|
576
|
+
const unsub = eventBus.subscribe(eventType, event => {
|
|
577
|
+
if (remaining.has(eventType)) {
|
|
578
|
+
remaining.delete(eventType);
|
|
579
|
+
collected.push(event);
|
|
580
|
+
checkComplete();
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
unsubscribes.push(unsub);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Collect events during an action
|
|
589
|
+
*
|
|
590
|
+
* @example
|
|
591
|
+
* ```typescript
|
|
592
|
+
* const events = await collectEvents(eventBus, async () => {
|
|
593
|
+
* await createTrade();
|
|
594
|
+
* await acceptTrade();
|
|
595
|
+
* });
|
|
596
|
+
* expect(events).toHaveLength(2);
|
|
597
|
+
* ```
|
|
598
|
+
*/
|
|
599
|
+
async function collectEvents(eventBus, action, eventTypes) {
|
|
600
|
+
const events = [];
|
|
601
|
+
const unsubscribe = eventBus.subscribeAll(event => {
|
|
602
|
+
if (!eventTypes || eventTypes.includes(event.type)) {
|
|
603
|
+
events.push(event);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
try {
|
|
607
|
+
await action();
|
|
608
|
+
// Give async handlers time to complete
|
|
609
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
610
|
+
} finally {
|
|
611
|
+
unsubscribe();
|
|
612
|
+
}
|
|
613
|
+
return events;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Create a mock idempotency store
|
|
617
|
+
*
|
|
618
|
+
* @example
|
|
619
|
+
* ```typescript
|
|
620
|
+
* const store = mockIdempotencyStore({
|
|
621
|
+
* duplicateIds: ['event-1', 'event-2'],
|
|
622
|
+
* });
|
|
623
|
+
*
|
|
624
|
+
* const result = await store.check({ id: 'event-1' }, 'consumer');
|
|
625
|
+
* expect(result.isDuplicate).toBe(true);
|
|
626
|
+
* ```
|
|
627
|
+
*/
|
|
628
|
+
function mockIdempotencyStore(options) {
|
|
629
|
+
const duplicateIds = new Set(options?.duplicateIds ?? []);
|
|
630
|
+
const processedRecords = options?.processedRecords ?? new Map();
|
|
631
|
+
const processingLocks = new Map();
|
|
632
|
+
return {
|
|
633
|
+
async check(event, consumer, checkOptions) {
|
|
634
|
+
const key = `${consumer}:${event.id}`;
|
|
635
|
+
if (duplicateIds.has(event.id) || processedRecords.has(key)) {
|
|
636
|
+
const record = processedRecords.get(key);
|
|
637
|
+
return {
|
|
638
|
+
isDuplicate: true,
|
|
639
|
+
processedAt: record?.completedAt ?? record?.startedAt,
|
|
640
|
+
result: record?.result
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const shouldAcquire = options?.shouldAcquireLock ?? checkOptions?.acquireLock ?? true;
|
|
644
|
+
if (shouldAcquire) {
|
|
645
|
+
processingLocks.set(key, true);
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
isDuplicate: false,
|
|
649
|
+
lockAcquired: shouldAcquire
|
|
650
|
+
};
|
|
651
|
+
},
|
|
652
|
+
async markProcessing(event, consumer) {
|
|
653
|
+
const key = `${consumer}:${event.id}`;
|
|
654
|
+
if (processingLocks.has(key)) {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
processingLocks.set(key, true);
|
|
658
|
+
return true;
|
|
659
|
+
},
|
|
660
|
+
async markCompleted(event, consumer, result) {
|
|
661
|
+
const key = `${consumer}:${event.id}`;
|
|
662
|
+
processedRecords.set(key, {
|
|
663
|
+
eventId: event.id,
|
|
664
|
+
eventType: event.type,
|
|
665
|
+
startedAt: new Date(),
|
|
666
|
+
completedAt: new Date(),
|
|
667
|
+
status: 'completed',
|
|
668
|
+
result,
|
|
669
|
+
consumer,
|
|
670
|
+
attempts: 1
|
|
671
|
+
});
|
|
672
|
+
processingLocks.delete(key);
|
|
673
|
+
},
|
|
674
|
+
async markFailed(event, consumer, error) {
|
|
675
|
+
const key = `${consumer}:${event.id}`;
|
|
676
|
+
processedRecords.set(key, {
|
|
677
|
+
eventId: event.id,
|
|
678
|
+
eventType: event.type,
|
|
679
|
+
startedAt: new Date(),
|
|
680
|
+
completedAt: new Date(),
|
|
681
|
+
status: 'failed',
|
|
682
|
+
error: error instanceof Error ? error.message : String(error),
|
|
683
|
+
consumer,
|
|
684
|
+
attempts: 1
|
|
685
|
+
});
|
|
686
|
+
processingLocks.delete(key);
|
|
687
|
+
},
|
|
688
|
+
async releaseLock(event, consumer) {
|
|
689
|
+
const key = `${consumer}:${event.id}`;
|
|
690
|
+
processingLocks.delete(key);
|
|
691
|
+
},
|
|
692
|
+
async getRecord(eventId, consumer) {
|
|
693
|
+
return processedRecords.get(`${consumer}:${eventId}`) ?? null;
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Create a mock error classifier
|
|
699
|
+
*
|
|
700
|
+
* @example
|
|
701
|
+
* ```typescript
|
|
702
|
+
* const classifier = mockErrorClassifier({
|
|
703
|
+
* defaultClassification: 'transient',
|
|
704
|
+
* customClassifications: {
|
|
705
|
+
* 'ValidationError': 'permanent',
|
|
706
|
+
* 'NetworkError': 'transient',
|
|
707
|
+
* },
|
|
708
|
+
* });
|
|
709
|
+
* ```
|
|
710
|
+
*/
|
|
711
|
+
function mockErrorClassifier(options) {
|
|
712
|
+
const defaultClassification = options?.defaultClassification ?? 'unknown';
|
|
713
|
+
const customClassifications = options?.customClassifications ?? {};
|
|
714
|
+
const defaultRetryConfig = {
|
|
715
|
+
maxAttempts: 3,
|
|
716
|
+
initialDelayMs: 1000,
|
|
717
|
+
maxDelayMs: 30000,
|
|
718
|
+
backoffMultiplier: 2,
|
|
719
|
+
jitter: 0.1,
|
|
720
|
+
...options?.retryConfig
|
|
721
|
+
};
|
|
722
|
+
return {
|
|
723
|
+
classify(error) {
|
|
724
|
+
const errorName = error instanceof Error ? error.constructor.name : 'Error';
|
|
725
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
726
|
+
// Check custom classifications
|
|
727
|
+
const customClass = customClassifications[errorName] ?? customClassifications[errorMessage];
|
|
728
|
+
const classification = customClass ?? defaultClassification;
|
|
729
|
+
return {
|
|
730
|
+
classification,
|
|
731
|
+
retryConfig: defaultRetryConfig,
|
|
732
|
+
sendToDlq: classification === 'permanent' || classification === 'poison',
|
|
733
|
+
reason: `Mock classification: ${classification}`
|
|
734
|
+
};
|
|
735
|
+
},
|
|
736
|
+
isRetryable(error) {
|
|
737
|
+
const result = this.classify(error);
|
|
738
|
+
return result.classification === 'transient' || result.classification === 'unknown';
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export { EventAssertions, MockEventBus, collectEvents, createEventAssertions, createMockEventBus, createTestEvent, createTestEventFactory, mockErrorClassifier, mockIdempotencyStore, waitForEvent, waitForEvents };
|