core tests
This commit is contained in:
297
core/shared/domain/DomainEvent.test.ts
Normal file
297
core/shared/domain/DomainEvent.test.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user