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');
|
||||
});
|
||||
});
|
||||
});
|
||||
372
core/shared/domain/Logger.test.ts
Normal file
372
core/shared/domain/Logger.test.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Logger } from './Logger';
|
||||
|
||||
describe('Logger', () => {
|
||||
describe('Logger interface', () => {
|
||||
it('should have debug method', () => {
|
||||
const logs: Array<{ message: string; context?: unknown }> = [];
|
||||
|
||||
const logger: Logger = {
|
||||
debug: (message: string, context?: unknown) => {
|
||||
logs.push({ message, context });
|
||||
},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {}
|
||||
};
|
||||
|
||||
logger.debug('Debug message', { userId: 123 });
|
||||
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(logs[0].message).toBe('Debug message');
|
||||
expect(logs[0].context).toEqual({ userId: 123 });
|
||||
});
|
||||
|
||||
it('should have info method', () => {
|
||||
const logs: Array<{ message: string; context?: unknown }> = [];
|
||||
|
||||
const logger: Logger = {
|
||||
debug: () => {},
|
||||
info: (message: string, context?: unknown) => {
|
||||
logs.push({ message, context });
|
||||
},
|
||||
warn: () => {},
|
||||
error: () => {}
|
||||
};
|
||||
|
||||
logger.info('Info message', { action: 'login' });
|
||||
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(logs[0].message).toBe('Info message');
|
||||
expect(logs[0].context).toEqual({ action: 'login' });
|
||||
});
|
||||
|
||||
it('should have warn method', () => {
|
||||
const logs: Array<{ message: string; context?: unknown }> = [];
|
||||
|
||||
const logger: Logger = {
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: (message: string, context?: unknown) => {
|
||||
logs.push({ message, context });
|
||||
},
|
||||
error: () => {}
|
||||
};
|
||||
|
||||
logger.warn('Warning message', { threshold: 0.8 });
|
||||
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(logs[0].message).toBe('Warning message');
|
||||
expect(logs[0].context).toEqual({ threshold: 0.8 });
|
||||
});
|
||||
|
||||
it('should have error method', () => {
|
||||
const logs: Array<{ message: string; error?: Error; context?: unknown }> = [];
|
||||
|
||||
const logger: Logger = {
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: (message: string, error?: Error, context?: unknown) => {
|
||||
logs.push({ message, error, context });
|
||||
}
|
||||
};
|
||||
|
||||
const testError = new Error('Test error');
|
||||
logger.error('Error occurred', testError, { userId: 456 });
|
||||
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(logs[0].message).toBe('Error occurred');
|
||||
expect(logs[0].error).toBe(testError);
|
||||
expect(logs[0].context).toEqual({ userId: 456 });
|
||||
});
|
||||
|
||||
it('should support logging without context', () => {
|
||||
const logs: string[] = [];
|
||||
|
||||
const logger: Logger = {
|
||||
debug: (message) => logs.push(`DEBUG: ${message}`),
|
||||
info: (message) => logs.push(`INFO: ${message}`),
|
||||
warn: (message) => logs.push(`WARN: ${message}`),
|
||||
error: (message) => logs.push(`ERROR: ${message}`)
|
||||
};
|
||||
|
||||
logger.debug('Debug without context');
|
||||
logger.info('Info without context');
|
||||
logger.warn('Warn without context');
|
||||
logger.error('Error without context');
|
||||
|
||||
expect(logs).toHaveLength(4);
|
||||
expect(logs[0]).toBe('DEBUG: Debug without context');
|
||||
expect(logs[1]).toBe('INFO: Info without context');
|
||||
expect(logs[2]).toBe('WARN: Warn without context');
|
||||
expect(logs[3]).toBe('ERROR: Error without context');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Logger behavior', () => {
|
||||
it('should support structured logging', () => {
|
||||
const logs: Array<{ level: string; message: string; timestamp: string; data: unknown }> = [];
|
||||
|
||||
const logger: Logger = {
|
||||
debug: (message, context) => {
|
||||
logs.push({ level: 'debug', message, timestamp: new Date().toISOString(), data: context });
|
||||
},
|
||||
info: (message, context) => {
|
||||
logs.push({ level: 'info', message, timestamp: new Date().toISOString(), data: context });
|
||||
},
|
||||
warn: (message, context) => {
|
||||
logs.push({ level: 'warn', message, timestamp: new Date().toISOString(), data: context });
|
||||
},
|
||||
error: (message, error, context) => {
|
||||
const data: Record<string, unknown> = { error };
|
||||
if (context) {
|
||||
Object.assign(data, context);
|
||||
}
|
||||
logs.push({ level: 'error', message, timestamp: new Date().toISOString(), data });
|
||||
}
|
||||
};
|
||||
|
||||
logger.info('User logged in', { userId: 'user-123', ip: '192.168.1.1' });
|
||||
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(logs[0].level).toBe('info');
|
||||
expect(logs[0].message).toBe('User logged in');
|
||||
expect(logs[0].data).toEqual({ userId: 'user-123', ip: '192.168.1.1' });
|
||||
});
|
||||
|
||||
it('should support log level filtering', () => {
|
||||
const logs: string[] = [];
|
||||
|
||||
const logger: Logger = {
|
||||
debug: (message) => logs.push(`[DEBUG] ${message}`),
|
||||
info: (message) => logs.push(`[INFO] ${message}`),
|
||||
warn: (message) => logs.push(`[WARN] ${message}`),
|
||||
error: (message) => logs.push(`[ERROR] ${message}`)
|
||||
};
|
||||
|
||||
// Simulate different log levels
|
||||
logger.debug('This is a debug message');
|
||||
logger.info('This is an info message');
|
||||
logger.warn('This is a warning message');
|
||||
logger.error('This is an error message');
|
||||
|
||||
expect(logs).toHaveLength(4);
|
||||
expect(logs[0]).toContain('[DEBUG]');
|
||||
expect(logs[1]).toContain('[INFO]');
|
||||
expect(logs[2]).toContain('[WARN]');
|
||||
expect(logs[3]).toContain('[ERROR]');
|
||||
});
|
||||
|
||||
it('should support error logging with stack trace', () => {
|
||||
const logs: Array<{ message: string; error: Error; context?: unknown }> = [];
|
||||
|
||||
const logger: Logger = {
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: (message: string, error?: Error, context?: unknown) => {
|
||||
if (error) {
|
||||
logs.push({ message, error, context });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const error = new Error('Database connection failed');
|
||||
logger.error('Failed to connect to database', error, { retryCount: 3 });
|
||||
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(logs[0].message).toBe('Failed to connect to database');
|
||||
expect(logs[0].error.message).toBe('Database connection failed');
|
||||
expect(logs[0].error.stack).toBeDefined();
|
||||
expect(logs[0].context).toEqual({ retryCount: 3 });
|
||||
});
|
||||
|
||||
it('should support logging complex objects', () => {
|
||||
const logs: Array<{ message: string; context: unknown }> = [];
|
||||
|
||||
const logger: Logger = {
|
||||
debug: () => {},
|
||||
info: (message, context) => logs.push({ message, context }),
|
||||
warn: () => {},
|
||||
error: () => {}
|
||||
};
|
||||
|
||||
const complexObject = {
|
||||
user: {
|
||||
id: 'user-123',
|
||||
profile: {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
preferences: {
|
||||
theme: 'dark',
|
||||
notifications: true
|
||||
}
|
||||
}
|
||||
},
|
||||
session: {
|
||||
id: 'session-456',
|
||||
expiresAt: new Date('2024-12-31T23:59:59Z')
|
||||
}
|
||||
};
|
||||
|
||||
logger.info('User session data', complexObject);
|
||||
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(logs[0].message).toBe('User session data');
|
||||
expect(logs[0].context).toEqual(complexObject);
|
||||
});
|
||||
|
||||
it('should support logging arrays', () => {
|
||||
const logs: Array<{ message: string; context: unknown }> = [];
|
||||
|
||||
const logger: Logger = {
|
||||
debug: () => {},
|
||||
info: (message, context) => logs.push({ message, context }),
|
||||
warn: () => {},
|
||||
error: () => {}
|
||||
};
|
||||
|
||||
const items = [
|
||||
{ id: 1, name: 'Item 1' },
|
||||
{ id: 2, name: 'Item 2' },
|
||||
{ id: 3, name: 'Item 3' }
|
||||
];
|
||||
|
||||
logger.info('Processing items', { items, count: items.length });
|
||||
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(logs[0].message).toBe('Processing items');
|
||||
expect(logs[0].context).toEqual({ items, count: 3 });
|
||||
});
|
||||
|
||||
it('should support logging with null and undefined values', () => {
|
||||
const logs: Array<{ message: string; context?: unknown }> = [];
|
||||
|
||||
const logger: Logger = {
|
||||
debug: () => {},
|
||||
info: (message, context) => logs.push({ message, context }),
|
||||
warn: () => {},
|
||||
error: () => {}
|
||||
};
|
||||
|
||||
logger.info('Null value', null);
|
||||
logger.info('Undefined value', undefined);
|
||||
logger.info('Mixed values', { a: null, b: undefined, c: 'value' });
|
||||
|
||||
expect(logs).toHaveLength(3);
|
||||
expect(logs[0].context).toBe(null);
|
||||
expect(logs[1].context).toBe(undefined);
|
||||
expect(logs[2].context).toEqual({ a: null, b: undefined, c: 'value' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Logger implementation patterns', () => {
|
||||
it('should support console logger implementation', () => {
|
||||
const consoleLogs: string[] = [];
|
||||
const originalConsoleDebug = console.debug;
|
||||
const originalConsoleInfo = console.info;
|
||||
const originalConsoleWarn = console.warn;
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
// Mock console methods
|
||||
console.debug = (...args: unknown[]) => consoleLogs.push(`DEBUG: ${args.join(' ')}`);
|
||||
console.info = (...args: unknown[]) => consoleLogs.push(`INFO: ${args.join(' ')}`);
|
||||
console.warn = (...args: unknown[]) => consoleLogs.push(`WARN: ${args.join(' ')}`);
|
||||
console.error = (...args: unknown[]) => consoleLogs.push(`ERROR: ${args.join(' ')}`);
|
||||
|
||||
const consoleLogger: Logger = {
|
||||
debug: (message, context) => console.debug(message, context),
|
||||
info: (message, context) => console.info(message, context),
|
||||
warn: (message, context) => console.warn(message, context),
|
||||
error: (message, error, context) => console.error(message, error, context)
|
||||
};
|
||||
|
||||
consoleLogger.debug('Debug message', { data: 'test' });
|
||||
consoleLogger.info('Info message', { data: 'test' });
|
||||
consoleLogger.warn('Warn message', { data: 'test' });
|
||||
consoleLogger.error('Error message', new Error('Test'), { data: 'test' });
|
||||
|
||||
// Restore console methods
|
||||
console.debug = originalConsoleDebug;
|
||||
console.info = originalConsoleInfo;
|
||||
console.warn = originalConsoleWarn;
|
||||
console.error = originalConsoleError;
|
||||
|
||||
expect(consoleLogs).toHaveLength(4);
|
||||
expect(consoleLogs[0]).toContain('DEBUG:');
|
||||
expect(consoleLogs[1]).toContain('INFO:');
|
||||
expect(consoleLogs[2]).toContain('WARN:');
|
||||
expect(consoleLogs[3]).toContain('ERROR:');
|
||||
});
|
||||
|
||||
it('should support file logger implementation', () => {
|
||||
const fileLogs: Array<{ timestamp: string; level: string; message: string; data?: unknown }> = [];
|
||||
|
||||
const fileLogger: Logger = {
|
||||
debug: (message, context) => {
|
||||
fileLogs.push({ timestamp: new Date().toISOString(), level: 'DEBUG', message, data: context });
|
||||
},
|
||||
info: (message, context) => {
|
||||
fileLogs.push({ timestamp: new Date().toISOString(), level: 'INFO', message, data: context });
|
||||
},
|
||||
warn: (message, context) => {
|
||||
fileLogs.push({ timestamp: new Date().toISOString(), level: 'WARN', message, data: context });
|
||||
},
|
||||
error: (message, error, context) => {
|
||||
const data: Record<string, unknown> = { error };
|
||||
if (context) {
|
||||
Object.assign(data, context);
|
||||
}
|
||||
fileLogs.push({ timestamp: new Date().toISOString(), level: 'ERROR', message, data });
|
||||
}
|
||||
};
|
||||
|
||||
fileLogger.info('Application started', { version: '1.0.0' });
|
||||
fileLogger.warn('High memory usage', { usage: '85%' });
|
||||
fileLogger.error('Database error', new Error('Connection timeout'), { retry: 3 });
|
||||
|
||||
expect(fileLogs).toHaveLength(3);
|
||||
expect(fileLogs[0].level).toBe('INFO');
|
||||
expect(fileLogs[1].level).toBe('WARN');
|
||||
expect(fileLogs[2].level).toBe('ERROR');
|
||||
expect(fileLogs[0].data).toEqual({ version: '1.0.0' });
|
||||
});
|
||||
|
||||
it('should support remote logger implementation', async () => {
|
||||
const remoteLogs: Array<{ level: string; message: string; context?: unknown }> = [];
|
||||
|
||||
const remoteLogger: Logger = {
|
||||
debug: async (message, context) => {
|
||||
remoteLogs.push({ level: 'debug', message, context });
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
},
|
||||
info: async (message, context) => {
|
||||
remoteLogs.push({ level: 'info', message, context });
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
},
|
||||
warn: async (message, context) => {
|
||||
remoteLogs.push({ level: 'warn', message, context });
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
},
|
||||
error: async (message, error, context) => {
|
||||
const errorContext: Record<string, unknown> = { error };
|
||||
if (context) {
|
||||
Object.assign(errorContext, context);
|
||||
}
|
||||
remoteLogs.push({ level: 'error', message, context: errorContext });
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
}
|
||||
};
|
||||
|
||||
await remoteLogger.info('User action', { action: 'click', element: 'button' });
|
||||
await remoteLogger.warn('Performance warning', { duration: '2000ms' });
|
||||
await remoteLogger.error('API failure', new Error('404 Not Found'), { endpoint: '/api/users' });
|
||||
|
||||
expect(remoteLogs).toHaveLength(3);
|
||||
expect(remoteLogs[0].level).toBe('info');
|
||||
expect(remoteLogs[1].level).toBe('warn');
|
||||
expect(remoteLogs[2].level).toBe('error');
|
||||
});
|
||||
});
|
||||
});
|
||||
120
core/shared/domain/Option.test.ts
Normal file
120
core/shared/domain/Option.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { coalesce, present } from './Option';
|
||||
|
||||
describe('Option', () => {
|
||||
describe('coalesce()', () => {
|
||||
it('should return the value when it is defined', () => {
|
||||
expect(coalesce('defined', 'fallback')).toBe('defined');
|
||||
expect(coalesce(42, 0)).toBe(42);
|
||||
expect(coalesce(true, false)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return the fallback when value is undefined', () => {
|
||||
expect(coalesce(undefined, 'fallback')).toBe('fallback');
|
||||
expect(coalesce(undefined, 42)).toBe(42);
|
||||
expect(coalesce(undefined, true)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return the fallback when value is null', () => {
|
||||
expect(coalesce(null, 'fallback')).toBe('fallback');
|
||||
expect(coalesce(null, 42)).toBe(42);
|
||||
expect(coalesce(null, true)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle complex fallback values', () => {
|
||||
const fallback = { id: 0, name: 'default' };
|
||||
expect(coalesce(undefined, fallback)).toEqual(fallback);
|
||||
expect(coalesce(null, fallback)).toEqual(fallback);
|
||||
expect(coalesce({ id: 1, name: 'actual' }, fallback)).toEqual({ id: 1, name: 'actual' });
|
||||
});
|
||||
|
||||
it('should handle array values', () => {
|
||||
const fallback = [1, 2, 3];
|
||||
expect(coalesce(undefined, fallback)).toEqual([1, 2, 3]);
|
||||
expect(coalesce([4, 5], fallback)).toEqual([4, 5]);
|
||||
});
|
||||
|
||||
it('should handle zero and empty string as valid values', () => {
|
||||
expect(coalesce(0, 999)).toBe(0);
|
||||
expect(coalesce('', 'fallback')).toBe('');
|
||||
expect(coalesce(false, true)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('present()', () => {
|
||||
it('should return the value when it is defined and not null', () => {
|
||||
expect(present('value')).toBe('value');
|
||||
expect(present(42)).toBe(42);
|
||||
expect(present(true)).toBe(true);
|
||||
expect(present({})).toEqual({});
|
||||
expect(present([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return undefined when value is undefined', () => {
|
||||
expect(present(undefined)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined when value is null', () => {
|
||||
expect(present(null)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should handle zero and empty string as valid values', () => {
|
||||
expect(present(0)).toBe(0);
|
||||
expect(present('')).toBe('');
|
||||
expect(present(false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle complex objects', () => {
|
||||
const obj = { id: 1, name: 'test', nested: { value: 'data' } };
|
||||
expect(present(obj)).toEqual(obj);
|
||||
});
|
||||
|
||||
it('should handle arrays', () => {
|
||||
const arr = [1, 2, 3, 'test'];
|
||||
expect(present(arr)).toEqual(arr);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Option behavior', () => {
|
||||
it('should work together for optional value handling', () => {
|
||||
// Example: providing a default when value might be missing
|
||||
const maybeValue: string | undefined = undefined;
|
||||
const result = coalesce(maybeValue, 'default');
|
||||
expect(result).toBe('default');
|
||||
|
||||
// Example: filtering out null/undefined
|
||||
const values: (string | null | undefined)[] = ['a', null, 'b', undefined, 'c'];
|
||||
const filtered = values.map(present).filter((v): v is string => v !== undefined);
|
||||
expect(filtered).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should support conditional value assignment', () => {
|
||||
const config: { timeout?: number } = {};
|
||||
const timeout = coalesce(config.timeout, 5000);
|
||||
expect(timeout).toBe(5000);
|
||||
|
||||
config.timeout = 3000;
|
||||
const timeout2 = coalesce(config.timeout, 5000);
|
||||
expect(timeout2).toBe(3000);
|
||||
});
|
||||
|
||||
it('should handle nested optional properties', () => {
|
||||
interface User {
|
||||
profile?: {
|
||||
name?: string;
|
||||
email?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const user1: User = {};
|
||||
const user2: User = { profile: {} };
|
||||
const user3: User = { profile: { name: 'John' } };
|
||||
const user4: User = { profile: { name: 'John', email: 'john@example.com' } };
|
||||
|
||||
expect(coalesce(user1.profile?.name, 'Anonymous')).toBe('Anonymous');
|
||||
expect(coalesce(user2.profile?.name, 'Anonymous')).toBe('Anonymous');
|
||||
expect(coalesce(user3.profile?.name, 'Anonymous')).toBe('John');
|
||||
expect(coalesce(user4.profile?.name, 'Anonymous')).toBe('John');
|
||||
});
|
||||
});
|
||||
});
|
||||
370
core/shared/domain/Result.test.ts
Normal file
370
core/shared/domain/Result.test.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Result } from './Result';
|
||||
|
||||
describe('Result', () => {
|
||||
describe('Result.ok()', () => {
|
||||
it('should create a success result with a value', () => {
|
||||
const result = Result.ok('success-value');
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.isErr()).toBe(false);
|
||||
expect(result.unwrap()).toBe('success-value');
|
||||
});
|
||||
|
||||
it('should create a success result with undefined value', () => {
|
||||
const result = Result.ok(undefined);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.isErr()).toBe(false);
|
||||
expect(result.unwrap()).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should create a success result with null value', () => {
|
||||
const result = Result.ok(null);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.isErr()).toBe(false);
|
||||
expect(result.unwrap()).toBe(null);
|
||||
});
|
||||
|
||||
it('should create a success result with complex object', () => {
|
||||
const complexValue = { id: 123, name: 'test', nested: { data: 'value' } };
|
||||
const result = Result.ok(complexValue);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual(complexValue);
|
||||
});
|
||||
|
||||
it('should create a success result with array', () => {
|
||||
const arrayValue = [1, 2, 3, 'test'];
|
||||
const result = Result.ok(arrayValue);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual(arrayValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.err()', () => {
|
||||
it('should create an error result with an error', () => {
|
||||
const error = new Error('test error');
|
||||
const result = Result.err(error);
|
||||
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toBe(error);
|
||||
});
|
||||
|
||||
it('should create an error result with string error', () => {
|
||||
const result = Result.err('string error');
|
||||
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toBe('string error');
|
||||
});
|
||||
|
||||
it('should create an error result with object error', () => {
|
||||
const error = { code: 'VALIDATION_ERROR', message: 'Invalid input' };
|
||||
const result = Result.err(error);
|
||||
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual(error);
|
||||
});
|
||||
|
||||
it('should create an error result with custom error type', () => {
|
||||
interface CustomError {
|
||||
code: string;
|
||||
details: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const error: CustomError = {
|
||||
code: 'NOT_FOUND',
|
||||
details: { id: '123' }
|
||||
};
|
||||
|
||||
const result = Result.err<unknown, CustomError>(error);
|
||||
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.isOk()', () => {
|
||||
it('should return true for success results', () => {
|
||||
const result = Result.ok('value');
|
||||
expect(result.isOk()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for error results', () => {
|
||||
const result = Result.err(new Error('error'));
|
||||
expect(result.isOk()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.isErr()', () => {
|
||||
it('should return false for success results', () => {
|
||||
const result = Result.ok('value');
|
||||
expect(result.isErr()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for error results', () => {
|
||||
const result = Result.err(new Error('error'));
|
||||
expect(result.isErr()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.unwrap()', () => {
|
||||
it('should return the value for success results', () => {
|
||||
const result = Result.ok('test-value');
|
||||
expect(result.unwrap()).toBe('test-value');
|
||||
});
|
||||
|
||||
it('should throw error for error results', () => {
|
||||
const result = Result.err(new Error('test error'));
|
||||
expect(() => result.unwrap()).toThrow('Called unwrap on an error result');
|
||||
});
|
||||
|
||||
it('should return complex values for success results', () => {
|
||||
const complexValue = { id: 123, data: { nested: 'value' } };
|
||||
const result = Result.ok(complexValue);
|
||||
expect(result.unwrap()).toEqual(complexValue);
|
||||
});
|
||||
|
||||
it('should return arrays for success results', () => {
|
||||
const arrayValue = [1, 2, 3];
|
||||
const result = Result.ok(arrayValue);
|
||||
expect(result.unwrap()).toEqual(arrayValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.unwrapOr()', () => {
|
||||
it('should return the value for success results', () => {
|
||||
const result = Result.ok('actual-value');
|
||||
expect(result.unwrapOr('default-value')).toBe('actual-value');
|
||||
});
|
||||
|
||||
it('should return default value for error results', () => {
|
||||
const result = Result.err(new Error('error'));
|
||||
expect(result.unwrapOr('default-value')).toBe('default-value');
|
||||
});
|
||||
|
||||
it('should return default value when value is undefined', () => {
|
||||
const result = Result.ok(undefined);
|
||||
expect(result.unwrapOr('default-value')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return default value when value is null', () => {
|
||||
const result = Result.ok(null);
|
||||
expect(result.unwrapOr('default-value')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.unwrapErr()', () => {
|
||||
it('should return the error for error results', () => {
|
||||
const error = new Error('test error');
|
||||
const result = Result.err(error);
|
||||
expect(result.unwrapErr()).toBe(error);
|
||||
});
|
||||
|
||||
it('should throw error for success results', () => {
|
||||
const result = Result.ok('value');
|
||||
expect(() => result.unwrapErr()).toThrow('Called unwrapErr on a success result');
|
||||
});
|
||||
|
||||
it('should return string errors', () => {
|
||||
const result = Result.err('string error');
|
||||
expect(result.unwrapErr()).toBe('string error');
|
||||
});
|
||||
|
||||
it('should return object errors', () => {
|
||||
const error = { code: 'ERROR', message: 'Something went wrong' };
|
||||
const result = Result.err(error);
|
||||
expect(result.unwrapErr()).toEqual(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.map()', () => {
|
||||
it('should transform success values', () => {
|
||||
const result = Result.ok(5);
|
||||
const mapped = result.map((x) => x * 2);
|
||||
|
||||
expect(mapped.isOk()).toBe(true);
|
||||
expect(mapped.unwrap()).toBe(10);
|
||||
});
|
||||
|
||||
it('should not transform error results', () => {
|
||||
const error = new Error('test error');
|
||||
const result = Result.err<number, Error>(error);
|
||||
const mapped = result.map((x) => x * 2);
|
||||
|
||||
expect(mapped.isErr()).toBe(true);
|
||||
expect(mapped.unwrapErr()).toBe(error);
|
||||
});
|
||||
|
||||
it('should handle complex transformations', () => {
|
||||
const result = Result.ok({ id: 1, name: 'test' });
|
||||
const mapped = result.map((obj) => ({ ...obj, name: obj.name.toUpperCase() }));
|
||||
|
||||
expect(mapped.isOk()).toBe(true);
|
||||
expect(mapped.unwrap()).toEqual({ id: 1, name: 'TEST' });
|
||||
});
|
||||
|
||||
it('should handle array transformations', () => {
|
||||
const result = Result.ok([1, 2, 3]);
|
||||
const mapped = result.map((arr) => arr.map((x) => x * 2));
|
||||
|
||||
expect(mapped.isOk()).toBe(true);
|
||||
expect(mapped.unwrap()).toEqual([2, 4, 6]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.mapErr()', () => {
|
||||
it('should transform error values', () => {
|
||||
const error = new Error('original error');
|
||||
const result = Result.err<string, Error>(error);
|
||||
const mapped = result.mapErr((e) => new Error(`wrapped: ${e.message}`));
|
||||
|
||||
expect(mapped.isErr()).toBe(true);
|
||||
expect(mapped.unwrapErr().message).toBe('wrapped: original error');
|
||||
});
|
||||
|
||||
it('should not transform success results', () => {
|
||||
const result = Result.ok('value');
|
||||
const mapped = result.mapErr((e) => new Error(`wrapped: ${e.message}`));
|
||||
|
||||
expect(mapped.isOk()).toBe(true);
|
||||
expect(mapped.unwrap()).toBe('value');
|
||||
});
|
||||
|
||||
it('should handle string error transformations', () => {
|
||||
const result = Result.err('error message');
|
||||
const mapped = result.mapErr((e) => e.toUpperCase());
|
||||
|
||||
expect(mapped.isErr()).toBe(true);
|
||||
expect(mapped.unwrapErr()).toBe('ERROR MESSAGE');
|
||||
});
|
||||
|
||||
it('should handle object error transformations', () => {
|
||||
const error = { code: 'ERROR', message: 'Something went wrong' };
|
||||
const result = Result.err(error);
|
||||
const mapped = result.mapErr((e) => ({ ...e, code: `WRAPPED_${e.code}` }));
|
||||
|
||||
expect(mapped.isErr()).toBe(true);
|
||||
expect(mapped.unwrapErr()).toEqual({ code: 'WRAPPED_ERROR', message: 'Something went wrong' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.andThen()', () => {
|
||||
it('should chain success results', () => {
|
||||
const result1 = Result.ok(5);
|
||||
const result2 = result1.andThen((x) => Result.ok(x * 2));
|
||||
|
||||
expect(result2.isOk()).toBe(true);
|
||||
expect(result2.unwrap()).toBe(10);
|
||||
});
|
||||
|
||||
it('should propagate errors through chain', () => {
|
||||
const error = new Error('first error');
|
||||
const result1 = Result.err<number, Error>(error);
|
||||
const result2 = result1.andThen((x) => Result.ok(x * 2));
|
||||
|
||||
expect(result2.isErr()).toBe(true);
|
||||
expect(result2.unwrapErr()).toBe(error);
|
||||
});
|
||||
|
||||
it('should handle error in chained function', () => {
|
||||
const result1 = Result.ok(5);
|
||||
const result2 = result1.andThen((x) => Result.err(new Error(`error at ${x}`)));
|
||||
|
||||
expect(result2.isErr()).toBe(true);
|
||||
expect(result2.unwrapErr().message).toBe('error at 5');
|
||||
});
|
||||
|
||||
it('should support multiple chaining steps', () => {
|
||||
const result = Result.ok(2)
|
||||
.andThen((x) => Result.ok(x * 3))
|
||||
.andThen((x) => Result.ok(x + 1))
|
||||
.andThen((x) => Result.ok(x * 2));
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBe(14); // ((2 * 3) + 1) * 2 = 14
|
||||
});
|
||||
|
||||
it('should stop chaining on first error', () => {
|
||||
const result = Result.ok(2)
|
||||
.andThen((x) => Result.ok(x * 3))
|
||||
.andThen((x) => Result.err(new Error('stopped here')))
|
||||
.andThen((x) => Result.ok(x + 1)); // This should not execute
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().message).toBe('stopped here');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.value getter', () => {
|
||||
it('should return value for success results', () => {
|
||||
const result = Result.ok('test-value');
|
||||
expect(result.value).toBe('test-value');
|
||||
});
|
||||
|
||||
it('should return undefined for error results', () => {
|
||||
const result = Result.err(new Error('error'));
|
||||
expect(result.value).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined for success results with undefined value', () => {
|
||||
const result = Result.ok(undefined);
|
||||
expect(result.value).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result.error getter', () => {
|
||||
it('should return error for error results', () => {
|
||||
const error = new Error('test error');
|
||||
const result = Result.err(error);
|
||||
expect(result.error).toBe(error);
|
||||
});
|
||||
|
||||
it('should return undefined for success results', () => {
|
||||
const result = Result.ok('value');
|
||||
expect(result.error).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return string errors', () => {
|
||||
const result = Result.err('string error');
|
||||
expect(result.error).toBe('string error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result type safety', () => {
|
||||
it('should work with custom error codes', () => {
|
||||
type MyErrorCode = 'NOT_FOUND' | 'VALIDATION_ERROR' | 'PERMISSION_DENIED';
|
||||
|
||||
const successResult = Result.ok<string, MyErrorCode>('data');
|
||||
const errorResult = Result.err<string, MyErrorCode>('NOT_FOUND');
|
||||
|
||||
expect(successResult.isOk()).toBe(true);
|
||||
expect(errorResult.isErr()).toBe(true);
|
||||
});
|
||||
|
||||
it('should work with ApplicationErrorCode pattern', () => {
|
||||
interface ApplicationErrorCode<Code extends string, Details = unknown> {
|
||||
code: Code;
|
||||
details?: Details;
|
||||
}
|
||||
|
||||
type MyErrorCodes = 'USER_NOT_FOUND' | 'INVALID_EMAIL';
|
||||
|
||||
const successResult = Result.ok<string, ApplicationErrorCode<MyErrorCodes>>('user');
|
||||
const errorResult = Result.err<string, ApplicationErrorCode<MyErrorCodes>>({
|
||||
code: 'USER_NOT_FOUND',
|
||||
details: { userId: '123' }
|
||||
});
|
||||
|
||||
expect(successResult.isOk()).toBe(true);
|
||||
expect(errorResult.isErr()).toBe(true);
|
||||
expect(errorResult.unwrapErr().code).toBe('USER_NOT_FOUND');
|
||||
});
|
||||
});
|
||||
});
|
||||
374
core/shared/domain/Service.test.ts
Normal file
374
core/shared/domain/Service.test.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
DomainService,
|
||||
DomainCalculationService,
|
||||
ResultDomainCalculationService,
|
||||
DomainValidationService,
|
||||
DomainFactoryService,
|
||||
DomainServiceAlias,
|
||||
DomainCalculationServiceAlias,
|
||||
ResultDomainCalculationServiceAlias,
|
||||
DomainValidationServiceAlias,
|
||||
DomainFactoryServiceAlias
|
||||
} from './Service';
|
||||
import { Result } from './Result';
|
||||
|
||||
describe('Service', () => {
|
||||
describe('DomainService interface', () => {
|
||||
it('should have optional serviceName property', () => {
|
||||
const service: DomainService = {
|
||||
serviceName: 'TestService'
|
||||
};
|
||||
|
||||
expect(service.serviceName).toBe('TestService');
|
||||
});
|
||||
|
||||
it('should work without serviceName', () => {
|
||||
const service: DomainService = {};
|
||||
|
||||
expect(service.serviceName).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should support different service implementations', () => {
|
||||
const service1: DomainService = { serviceName: 'Service1' };
|
||||
const service2: DomainService = { serviceName: 'Service2' };
|
||||
const service3: DomainService = {};
|
||||
|
||||
expect(service1.serviceName).toBe('Service1');
|
||||
expect(service2.serviceName).toBe('Service2');
|
||||
expect(service3.serviceName).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DomainCalculationService interface', () => {
|
||||
it('should have calculate method', () => {
|
||||
const service: DomainCalculationService<number, number> = {
|
||||
calculate: (input: number) => input * 2
|
||||
};
|
||||
|
||||
expect(service.calculate(5)).toBe(10);
|
||||
});
|
||||
|
||||
it('should support different input and output types', () => {
|
||||
const stringService: DomainCalculationService<string, string> = {
|
||||
calculate: (input: string) => input.toUpperCase()
|
||||
};
|
||||
|
||||
const objectService: DomainCalculationService<{ x: number; y: number }, number> = {
|
||||
calculate: (input) => input.x + input.y
|
||||
};
|
||||
|
||||
expect(stringService.calculate('hello')).toBe('HELLO');
|
||||
expect(objectService.calculate({ x: 3, y: 4 })).toBe(7);
|
||||
});
|
||||
|
||||
it('should support complex calculations', () => {
|
||||
interface CalculationInput {
|
||||
values: number[];
|
||||
operation: 'sum' | 'average' | 'max';
|
||||
}
|
||||
|
||||
const calculator: DomainCalculationService<CalculationInput, number> = {
|
||||
calculate: (input) => {
|
||||
switch (input.operation) {
|
||||
case 'sum':
|
||||
return input.values.reduce((a, b) => a + b, 0);
|
||||
case 'average':
|
||||
return input.values.reduce((a, b) => a + b, 0) / input.values.length;
|
||||
case 'max':
|
||||
return Math.max(...input.values);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expect(calculator.calculate({ values: [1, 2, 3], operation: 'sum' })).toBe(6);
|
||||
expect(calculator.calculate({ values: [1, 2, 3], operation: 'average' })).toBe(2);
|
||||
expect(calculator.calculate({ values: [1, 2, 3], operation: 'max' })).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ResultDomainCalculationService interface', () => {
|
||||
it('should have calculate method returning Result', () => {
|
||||
const service: ResultDomainCalculationService<number, number, string> = {
|
||||
calculate: (input: number) => {
|
||||
if (input < 0) {
|
||||
return Result.err('Input must be non-negative');
|
||||
}
|
||||
return Result.ok(input * 2);
|
||||
}
|
||||
};
|
||||
|
||||
const successResult = service.calculate(5);
|
||||
expect(successResult.isOk()).toBe(true);
|
||||
expect(successResult.unwrap()).toBe(10);
|
||||
|
||||
const errorResult = service.calculate(-1);
|
||||
expect(errorResult.isErr()).toBe(true);
|
||||
expect(errorResult.unwrapErr()).toBe('Input must be non-negative');
|
||||
});
|
||||
|
||||
it('should support validation logic', () => {
|
||||
interface ValidationResult {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
const validator: ResultDomainCalculationService<string, ValidationResult, string> = {
|
||||
calculate: (input: string) => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (input.length < 3) {
|
||||
errors.push('Must be at least 3 characters');
|
||||
}
|
||||
|
||||
if (!input.match(/^[a-zA-Z]+$/)) {
|
||||
errors.push('Must contain only letters');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.err(errors.join(', '));
|
||||
}
|
||||
|
||||
return Result.ok({ isValid: true, errors: [] });
|
||||
}
|
||||
};
|
||||
|
||||
const validResult = validator.calculate('Hello');
|
||||
expect(validResult.isOk()).toBe(true);
|
||||
expect(validResult.unwrap()).toEqual({ isValid: true, errors: [] });
|
||||
|
||||
const invalidResult = validator.calculate('ab');
|
||||
expect(invalidResult.isErr()).toBe(true);
|
||||
expect(invalidResult.unwrapErr()).toBe('Must be at least 3 characters');
|
||||
});
|
||||
|
||||
it('should support complex business rules', () => {
|
||||
interface DiscountInput {
|
||||
basePrice: number;
|
||||
customerType: 'regular' | 'premium' | 'vip';
|
||||
hasCoupon: boolean;
|
||||
}
|
||||
|
||||
const discountCalculator: ResultDomainCalculationService<DiscountInput, number, string> = {
|
||||
calculate: (input) => {
|
||||
let discount = 0;
|
||||
|
||||
// Customer type discount
|
||||
switch (input.customerType) {
|
||||
case 'premium':
|
||||
discount += 0.1;
|
||||
break;
|
||||
case 'vip':
|
||||
discount += 0.2;
|
||||
break;
|
||||
}
|
||||
|
||||
// Coupon discount
|
||||
if (input.hasCoupon) {
|
||||
discount += 0.05;
|
||||
}
|
||||
|
||||
// Validate price
|
||||
if (input.basePrice <= 0) {
|
||||
return Result.err('Price must be positive');
|
||||
}
|
||||
|
||||
const finalPrice = input.basePrice * (1 - discount);
|
||||
return Result.ok(finalPrice);
|
||||
}
|
||||
};
|
||||
|
||||
const vipWithCoupon = discountCalculator.calculate({
|
||||
basePrice: 100,
|
||||
customerType: 'vip',
|
||||
hasCoupon: true
|
||||
});
|
||||
|
||||
expect(vipWithCoupon.isOk()).toBe(true);
|
||||
expect(vipWithCoupon.unwrap()).toBe(75); // 100 * (1 - 0.2 - 0.05) = 75
|
||||
|
||||
const invalidPrice = discountCalculator.calculate({
|
||||
basePrice: 0,
|
||||
customerType: 'regular',
|
||||
hasCoupon: false
|
||||
});
|
||||
|
||||
expect(invalidPrice.isErr()).toBe(true);
|
||||
expect(invalidPrice.unwrapErr()).toBe('Price must be positive');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DomainValidationService interface', () => {
|
||||
it('should have validate method returning Result', () => {
|
||||
const service: DomainValidationService<string, boolean, string> = {
|
||||
validate: (input: string) => {
|
||||
if (input.length === 0) {
|
||||
return Result.err('Input cannot be empty');
|
||||
}
|
||||
return Result.ok(true);
|
||||
}
|
||||
};
|
||||
|
||||
const validResult = service.validate('test');
|
||||
expect(validResult.isOk()).toBe(true);
|
||||
expect(validResult.unwrap()).toBe(true);
|
||||
|
||||
const invalidResult = service.validate('');
|
||||
expect(invalidResult.isErr()).toBe(true);
|
||||
expect(invalidResult.unwrapErr()).toBe('Input cannot be empty');
|
||||
});
|
||||
|
||||
it('should support validation of complex objects', () => {
|
||||
interface UserInput {
|
||||
email: string;
|
||||
password: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
const userValidator: DomainValidationService<UserInput, UserInput, string> = {
|
||||
validate: (input) => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!input.email.includes('@')) {
|
||||
errors.push('Invalid email format');
|
||||
}
|
||||
|
||||
if (input.password.length < 8) {
|
||||
errors.push('Password must be at least 8 characters');
|
||||
}
|
||||
|
||||
if (input.age < 18) {
|
||||
errors.push('Must be at least 18 years old');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.err(errors.join(', '));
|
||||
}
|
||||
|
||||
return Result.ok(input);
|
||||
}
|
||||
};
|
||||
|
||||
const validUser = userValidator.validate({
|
||||
email: 'john@example.com',
|
||||
password: 'securepassword',
|
||||
age: 25
|
||||
});
|
||||
|
||||
expect(validUser.isOk()).toBe(true);
|
||||
expect(validUser.unwrap()).toEqual({
|
||||
email: 'john@example.com',
|
||||
password: 'securepassword',
|
||||
age: 25
|
||||
});
|
||||
|
||||
const invalidUser = userValidator.validate({
|
||||
email: 'invalid-email',
|
||||
password: 'short',
|
||||
age: 15
|
||||
});
|
||||
|
||||
expect(invalidUser.isErr()).toBe(true);
|
||||
expect(invalidUser.unwrapErr()).toContain('Invalid email format');
|
||||
expect(invalidUser.unwrapErr()).toContain('Password must be at least 8 characters');
|
||||
expect(invalidUser.unwrapErr()).toContain('Must be at least 18 years old');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DomainFactoryService interface', () => {
|
||||
it('should have create method', () => {
|
||||
const service: DomainFactoryService<string, { id: number; value: string }> = {
|
||||
create: (input: string) => ({
|
||||
id: input.length,
|
||||
value: input.toUpperCase()
|
||||
})
|
||||
};
|
||||
|
||||
const result = service.create('test');
|
||||
expect(result).toEqual({ id: 4, value: 'TEST' });
|
||||
});
|
||||
|
||||
it('should support creating complex objects', () => {
|
||||
interface CreateUserInput {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
const userFactory: DomainFactoryService<CreateUserInput, User> = {
|
||||
create: (input) => ({
|
||||
id: `user-${Date.now()}`,
|
||||
name: input.name,
|
||||
email: input.email,
|
||||
createdAt: new Date()
|
||||
})
|
||||
};
|
||||
|
||||
const user = userFactory.create({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com'
|
||||
});
|
||||
|
||||
expect(user.id).toMatch(/^user-\d+$/);
|
||||
expect(user.name).toBe('John Doe');
|
||||
expect(user.email).toBe('john@example.com');
|
||||
expect(user.createdAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should support creating value objects', () => {
|
||||
interface AddressProps {
|
||||
street: string;
|
||||
city: string;
|
||||
zipCode: string;
|
||||
}
|
||||
|
||||
const addressFactory: DomainFactoryService<AddressProps, AddressProps> = {
|
||||
create: (input) => ({
|
||||
street: input.street.trim(),
|
||||
city: input.city.trim(),
|
||||
zipCode: input.zipCode.trim()
|
||||
})
|
||||
};
|
||||
|
||||
const address = addressFactory.create({
|
||||
street: ' 123 Main St ',
|
||||
city: ' New York ',
|
||||
zipCode: ' 10001 '
|
||||
});
|
||||
|
||||
expect(address.street).toBe('123 Main St');
|
||||
expect(address.city).toBe('New York');
|
||||
expect(address.zipCode).toBe('10001');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ServiceAlias types', () => {
|
||||
it('should be assignable to their base interfaces', () => {
|
||||
const service1: DomainServiceAlias = { serviceName: 'Test' };
|
||||
const service2: DomainCalculationServiceAlias<number, number> = {
|
||||
calculate: (x) => x * 2
|
||||
};
|
||||
const service3: ResultDomainCalculationServiceAlias<number, number, string> = {
|
||||
calculate: (x) => Result.ok(x * 2)
|
||||
};
|
||||
const service4: DomainValidationServiceAlias<string, boolean, string> = {
|
||||
validate: (x) => Result.ok(x.length > 0)
|
||||
};
|
||||
const service5: DomainFactoryServiceAlias<string, string> = {
|
||||
create: (x) => x.toUpperCase()
|
||||
};
|
||||
|
||||
expect(service1.serviceName).toBe('Test');
|
||||
expect(service2.calculate(5)).toBe(10);
|
||||
expect(service3.calculate(5).isOk()).toBe(true);
|
||||
expect(service4.validate('test').isOk()).toBe(true);
|
||||
expect(service5.create('test')).toBe('TEST');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user