298 lines
9.2 KiB
TypeScript
298 lines
9.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { DomainEvent, DomainEventPublisher, DomainEventAlias } from './DomainEvent';
|
|
|
|
describe('DomainEvent', () => {
|
|
describe('DomainEvent interface', () => {
|
|
it('should have required properties', () => {
|
|
const event: DomainEvent<{ userId: string }> = {
|
|
eventType: 'USER_CREATED',
|
|
aggregateId: 'user-123',
|
|
eventData: { userId: 'user-123' },
|
|
occurredAt: new Date('2024-01-01T00:00:00Z')
|
|
};
|
|
|
|
expect(event.eventType).toBe('USER_CREATED');
|
|
expect(event.aggregateId).toBe('user-123');
|
|
expect(event.eventData).toEqual({ userId: 'user-123' });
|
|
expect(event.occurredAt).toEqual(new Date('2024-01-01T00:00:00Z'));
|
|
});
|
|
|
|
it('should support different event data types', () => {
|
|
const stringEvent: DomainEvent<string> = {
|
|
eventType: 'STRING_EVENT',
|
|
aggregateId: 'agg-1',
|
|
eventData: 'some data',
|
|
occurredAt: new Date()
|
|
};
|
|
|
|
const objectEvent: DomainEvent<{ id: number; name: string }> = {
|
|
eventType: 'OBJECT_EVENT',
|
|
aggregateId: 'agg-2',
|
|
eventData: { id: 1, name: 'test' },
|
|
occurredAt: new Date()
|
|
};
|
|
|
|
const arrayEvent: DomainEvent<string[]> = {
|
|
eventType: 'ARRAY_EVENT',
|
|
aggregateId: 'agg-3',
|
|
eventData: ['a', 'b', 'c'],
|
|
occurredAt: new Date()
|
|
};
|
|
|
|
expect(stringEvent.eventData).toBe('some data');
|
|
expect(objectEvent.eventData).toEqual({ id: 1, name: 'test' });
|
|
expect(arrayEvent.eventData).toEqual(['a', 'b', 'c']);
|
|
});
|
|
|
|
it('should support default unknown type', () => {
|
|
const event: DomainEvent = {
|
|
eventType: 'UNKNOWN_EVENT',
|
|
aggregateId: 'agg-1',
|
|
eventData: { any: 'data' },
|
|
occurredAt: new Date()
|
|
};
|
|
|
|
expect(event.eventType).toBe('UNKNOWN_EVENT');
|
|
expect(event.aggregateId).toBe('agg-1');
|
|
});
|
|
|
|
it('should support complex event data structures', () => {
|
|
interface ComplexEventData {
|
|
userId: string;
|
|
changes: {
|
|
field: string;
|
|
oldValue: unknown;
|
|
newValue: unknown;
|
|
}[];
|
|
metadata: {
|
|
source: string;
|
|
timestamp: string;
|
|
};
|
|
}
|
|
|
|
const event: DomainEvent<ComplexEventData> = {
|
|
eventType: 'USER_UPDATED',
|
|
aggregateId: 'user-456',
|
|
eventData: {
|
|
userId: 'user-456',
|
|
changes: [
|
|
{ field: 'email', oldValue: 'old@example.com', newValue: 'new@example.com' }
|
|
],
|
|
metadata: {
|
|
source: 'admin-panel',
|
|
timestamp: '2024-01-01T12:00:00Z'
|
|
}
|
|
},
|
|
occurredAt: new Date('2024-01-01T12:00:00Z')
|
|
};
|
|
|
|
expect(event.eventData.userId).toBe('user-456');
|
|
expect(event.eventData.changes).toHaveLength(1);
|
|
expect(event.eventData.metadata.source).toBe('admin-panel');
|
|
});
|
|
});
|
|
|
|
describe('DomainEventPublisher interface', () => {
|
|
it('should have publish method', async () => {
|
|
const mockPublisher: DomainEventPublisher = {
|
|
publish: async (event: DomainEvent) => {
|
|
// Mock implementation
|
|
return Promise.resolve();
|
|
}
|
|
};
|
|
|
|
const event: DomainEvent<{ message: string }> = {
|
|
eventType: 'TEST_EVENT',
|
|
aggregateId: 'test-1',
|
|
eventData: { message: 'test' },
|
|
occurredAt: new Date()
|
|
};
|
|
|
|
// Should not throw
|
|
await expect(mockPublisher.publish(event)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('should support async publish operations', async () => {
|
|
const publishedEvents: DomainEvent[] = [];
|
|
|
|
const mockPublisher: DomainEventPublisher = {
|
|
publish: async (event: DomainEvent) => {
|
|
publishedEvents.push(event);
|
|
// Simulate async operation
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
return Promise.resolve();
|
|
}
|
|
};
|
|
|
|
const event1: DomainEvent = {
|
|
eventType: 'EVENT_1',
|
|
aggregateId: 'agg-1',
|
|
eventData: { data: 'value1' },
|
|
occurredAt: new Date()
|
|
};
|
|
|
|
const event2: DomainEvent = {
|
|
eventType: 'EVENT_2',
|
|
aggregateId: 'agg-2',
|
|
eventData: { data: 'value2' },
|
|
occurredAt: new Date()
|
|
};
|
|
|
|
await mockPublisher.publish(event1);
|
|
await mockPublisher.publish(event2);
|
|
|
|
expect(publishedEvents).toHaveLength(2);
|
|
expect(publishedEvents[0].eventType).toBe('EVENT_1');
|
|
expect(publishedEvents[1].eventType).toBe('EVENT_2');
|
|
});
|
|
});
|
|
|
|
describe('DomainEvent behavior', () => {
|
|
it('should support event ordering by occurredAt', () => {
|
|
const events: DomainEvent[] = [
|
|
{
|
|
eventType: 'EVENT_3',
|
|
aggregateId: 'agg-3',
|
|
eventData: {},
|
|
occurredAt: new Date('2024-01-03T00:00:00Z')
|
|
},
|
|
{
|
|
eventType: 'EVENT_1',
|
|
aggregateId: 'agg-1',
|
|
eventData: {},
|
|
occurredAt: new Date('2024-01-01T00:00:00Z')
|
|
},
|
|
{
|
|
eventType: 'EVENT_2',
|
|
aggregateId: 'agg-2',
|
|
eventData: {},
|
|
occurredAt: new Date('2024-01-02T00:00:00Z')
|
|
}
|
|
];
|
|
|
|
const sorted = [...events].sort((a, b) =>
|
|
a.occurredAt.getTime() - b.occurredAt.getTime()
|
|
);
|
|
|
|
expect(sorted[0].eventType).toBe('EVENT_1');
|
|
expect(sorted[1].eventType).toBe('EVENT_2');
|
|
expect(sorted[2].eventType).toBe('EVENT_3');
|
|
});
|
|
|
|
it('should support filtering events by aggregateId', () => {
|
|
const events: DomainEvent[] = [
|
|
{ eventType: 'EVENT_1', aggregateId: 'user-1', eventData: {}, occurredAt: new Date() },
|
|
{ eventType: 'EVENT_2', aggregateId: 'user-2', eventData: {}, occurredAt: new Date() },
|
|
{ eventType: 'EVENT_3', aggregateId: 'user-1', eventData: {}, occurredAt: new Date() }
|
|
];
|
|
|
|
const user1Events = events.filter(e => e.aggregateId === 'user-1');
|
|
expect(user1Events).toHaveLength(2);
|
|
expect(user1Events[0].eventType).toBe('EVENT_1');
|
|
expect(user1Events[1].eventType).toBe('EVENT_3');
|
|
});
|
|
|
|
it('should support event replay from event store', () => {
|
|
// Simulating event replay pattern
|
|
const eventStore: DomainEvent[] = [
|
|
{
|
|
eventType: 'USER_CREATED',
|
|
aggregateId: 'user-123',
|
|
eventData: { userId: 'user-123', name: 'John' },
|
|
occurredAt: new Date('2024-01-01T00:00:00Z')
|
|
},
|
|
{
|
|
eventType: 'USER_UPDATED',
|
|
aggregateId: 'user-123',
|
|
eventData: { userId: 'user-123', email: 'john@example.com' },
|
|
occurredAt: new Date('2024-01-02T00:00:00Z')
|
|
}
|
|
];
|
|
|
|
// Replay events to build current state
|
|
let currentState: { userId: string; name?: string; email?: string } = { userId: 'user-123' };
|
|
|
|
for (const event of eventStore) {
|
|
if (event.eventType === 'USER_CREATED') {
|
|
const data = event.eventData as { userId: string; name: string };
|
|
currentState.name = data.name;
|
|
} else if (event.eventType === 'USER_UPDATED') {
|
|
const data = event.eventData as { userId: string; email: string };
|
|
currentState.email = data.email;
|
|
}
|
|
}
|
|
|
|
expect(currentState.name).toBe('John');
|
|
expect(currentState.email).toBe('john@example.com');
|
|
});
|
|
|
|
it('should support event sourcing pattern', () => {
|
|
// Event sourcing: state is derived from events
|
|
interface AccountState {
|
|
balance: number;
|
|
transactions: number;
|
|
}
|
|
|
|
const events: DomainEvent[] = [
|
|
{
|
|
eventType: 'ACCOUNT_CREATED',
|
|
aggregateId: 'account-1',
|
|
eventData: { initialBalance: 100 },
|
|
occurredAt: new Date('2024-01-01T00:00:00Z')
|
|
},
|
|
{
|
|
eventType: 'DEPOSIT',
|
|
aggregateId: 'account-1',
|
|
eventData: { amount: 50 },
|
|
occurredAt: new Date('2024-01-02T00:00:00Z')
|
|
},
|
|
{
|
|
eventType: 'WITHDRAWAL',
|
|
aggregateId: 'account-1',
|
|
eventData: { amount: 30 },
|
|
occurredAt: new Date('2024-01-03T00:00:00Z')
|
|
}
|
|
];
|
|
|
|
const state: AccountState = {
|
|
balance: 0,
|
|
transactions: 0
|
|
};
|
|
|
|
for (const event of events) {
|
|
switch (event.eventType) {
|
|
case 'ACCOUNT_CREATED':
|
|
state.balance = (event.eventData as { initialBalance: number }).initialBalance;
|
|
state.transactions = 1;
|
|
break;
|
|
case 'DEPOSIT':
|
|
state.balance += (event.eventData as { amount: number }).amount;
|
|
state.transactions += 1;
|
|
break;
|
|
case 'WITHDRAWAL':
|
|
state.balance -= (event.eventData as { amount: number }).amount;
|
|
state.transactions += 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
expect(state.balance).toBe(120); // 100 + 50 - 30
|
|
expect(state.transactions).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('DomainEventAlias type', () => {
|
|
it('should be assignable to DomainEvent', () => {
|
|
const alias: DomainEventAlias<{ id: string }> = {
|
|
eventType: 'TEST',
|
|
aggregateId: 'agg-1',
|
|
eventData: { id: 'test' },
|
|
occurredAt: new Date()
|
|
};
|
|
|
|
expect(alias.eventType).toBe('TEST');
|
|
expect(alias.aggregateId).toBe('agg-1');
|
|
});
|
|
});
|
|
});
|