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 = { 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 = { 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 = { 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'); }); }); });