core tests
This commit is contained in:
431
core/shared/application/UseCaseOutputPort.test.ts
Normal file
431
core/shared/application/UseCaseOutputPort.test.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { UseCaseOutputPort } from './UseCaseOutputPort';
|
||||
|
||||
describe('UseCaseOutputPort', () => {
|
||||
describe('UseCaseOutputPort interface', () => {
|
||||
it('should have present method', () => {
|
||||
const presentedData: unknown[] = [];
|
||||
|
||||
const outputPort: UseCaseOutputPort<string> = {
|
||||
present: (data: string) => {
|
||||
presentedData.push(data);
|
||||
}
|
||||
};
|
||||
|
||||
outputPort.present('test data');
|
||||
|
||||
expect(presentedData).toHaveLength(1);
|
||||
expect(presentedData[0]).toBe('test data');
|
||||
});
|
||||
|
||||
it('should support different data types', () => {
|
||||
const presentedData: Array<{ type: string; data: unknown }> = [];
|
||||
|
||||
const outputPort: UseCaseOutputPort<unknown> = {
|
||||
present: (data: unknown) => {
|
||||
presentedData.push({ type: typeof data, data });
|
||||
}
|
||||
};
|
||||
|
||||
outputPort.present('string data');
|
||||
outputPort.present(42);
|
||||
outputPort.present({ id: 1, name: 'test' });
|
||||
outputPort.present([1, 2, 3]);
|
||||
|
||||
expect(presentedData).toHaveLength(4);
|
||||
expect(presentedData[0]).toEqual({ type: 'string', data: 'string data' });
|
||||
expect(presentedData[1]).toEqual({ type: 'number', data: 42 });
|
||||
expect(presentedData[2]).toEqual({ type: 'object', data: { id: 1, name: 'test' } });
|
||||
expect(presentedData[3]).toEqual({ type: 'object', data: [1, 2, 3] });
|
||||
});
|
||||
|
||||
it('should support complex data structures', () => {
|
||||
interface UserDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
profile: {
|
||||
avatar: string;
|
||||
bio: string;
|
||||
preferences: {
|
||||
theme: 'light' | 'dark';
|
||||
notifications: boolean;
|
||||
};
|
||||
};
|
||||
metadata: {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
lastLogin?: Date;
|
||||
};
|
||||
}
|
||||
|
||||
const presentedUsers: UserDTO[] = [];
|
||||
|
||||
const outputPort: UseCaseOutputPort<UserDTO> = {
|
||||
present: (data: UserDTO) => {
|
||||
presentedUsers.push(data);
|
||||
}
|
||||
};
|
||||
|
||||
const user: UserDTO = {
|
||||
id: 'user-123',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
profile: {
|
||||
avatar: 'avatar.jpg',
|
||||
bio: 'Software developer',
|
||||
preferences: {
|
||||
theme: 'dark',
|
||||
notifications: true
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-02'),
|
||||
lastLogin: new Date('2024-01-03')
|
||||
}
|
||||
};
|
||||
|
||||
outputPort.present(user);
|
||||
|
||||
expect(presentedUsers).toHaveLength(1);
|
||||
expect(presentedUsers[0]).toEqual(user);
|
||||
});
|
||||
|
||||
it('should support array data', () => {
|
||||
const presentedArrays: number[][] = [];
|
||||
|
||||
const outputPort: UseCaseOutputPort<number[]> = {
|
||||
present: (data: number[]) => {
|
||||
presentedArrays.push(data);
|
||||
}
|
||||
};
|
||||
|
||||
outputPort.present([1, 2, 3]);
|
||||
outputPort.present([4, 5, 6]);
|
||||
|
||||
expect(presentedArrays).toHaveLength(2);
|
||||
expect(presentedArrays[0]).toEqual([1, 2, 3]);
|
||||
expect(presentedArrays[1]).toEqual([4, 5, 6]);
|
||||
});
|
||||
|
||||
it('should support null and undefined values', () => {
|
||||
const presentedValues: unknown[] = [];
|
||||
|
||||
const outputPort: UseCaseOutputPort<unknown> = {
|
||||
present: (data: unknown) => {
|
||||
presentedValues.push(data);
|
||||
}
|
||||
};
|
||||
|
||||
outputPort.present(null);
|
||||
outputPort.present(undefined);
|
||||
outputPort.present('value');
|
||||
|
||||
expect(presentedValues).toHaveLength(3);
|
||||
expect(presentedValues[0]).toBe(null);
|
||||
expect(presentedValues[1]).toBe(undefined);
|
||||
expect(presentedValues[2]).toBe('value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UseCaseOutputPort behavior', () => {
|
||||
it('should support transformation before presentation', () => {
|
||||
const presentedData: Array<{ transformed: string; original: string }> = [];
|
||||
|
||||
const outputPort: UseCaseOutputPort<string> = {
|
||||
present: (data: string) => {
|
||||
const transformed = data.toUpperCase();
|
||||
presentedData.push({ transformed, original: data });
|
||||
}
|
||||
};
|
||||
|
||||
outputPort.present('hello');
|
||||
outputPort.present('world');
|
||||
|
||||
expect(presentedData).toHaveLength(2);
|
||||
expect(presentedData[0]).toEqual({ transformed: 'HELLO', original: 'hello' });
|
||||
expect(presentedData[1]).toEqual({ transformed: 'WORLD', original: 'world' });
|
||||
});
|
||||
|
||||
it('should support validation before presentation', () => {
|
||||
const presentedData: string[] = [];
|
||||
const validationErrors: string[] = [];
|
||||
|
||||
const outputPort: UseCaseOutputPort<string> = {
|
||||
present: (data: string) => {
|
||||
if (data.length === 0) {
|
||||
validationErrors.push('Data cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.length > 100) {
|
||||
validationErrors.push('Data exceeds maximum length');
|
||||
return;
|
||||
}
|
||||
|
||||
presentedData.push(data);
|
||||
}
|
||||
};
|
||||
|
||||
outputPort.present('valid data');
|
||||
outputPort.present('');
|
||||
outputPort.present('a'.repeat(101));
|
||||
outputPort.present('another valid');
|
||||
|
||||
expect(presentedData).toHaveLength(2);
|
||||
expect(presentedData[0]).toBe('valid data');
|
||||
expect(presentedData[1]).toBe('another valid');
|
||||
expect(validationErrors).toHaveLength(2);
|
||||
expect(validationErrors[0]).toBe('Data cannot be empty');
|
||||
expect(validationErrors[1]).toBe('Data exceeds maximum length');
|
||||
});
|
||||
|
||||
it('should support pagination', () => {
|
||||
interface PaginatedResult<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
const presentedPages: PaginatedResult<string>[] = [];
|
||||
|
||||
const outputPort: UseCaseOutputPort<PaginatedResult<string>> = {
|
||||
present: (data: PaginatedResult<string>) => {
|
||||
presentedPages.push(data);
|
||||
}
|
||||
};
|
||||
|
||||
const page1: PaginatedResult<string> = {
|
||||
items: ['item-1', 'item-2', 'item-3'],
|
||||
total: 10,
|
||||
page: 1,
|
||||
totalPages: 4
|
||||
};
|
||||
|
||||
const page2: PaginatedResult<string> = {
|
||||
items: ['item-4', 'item-5', 'item-6'],
|
||||
total: 10,
|
||||
page: 2,
|
||||
totalPages: 4
|
||||
};
|
||||
|
||||
outputPort.present(page1);
|
||||
outputPort.present(page2);
|
||||
|
||||
expect(presentedPages).toHaveLength(2);
|
||||
expect(presentedPages[0]).toEqual(page1);
|
||||
expect(presentedPages[1]).toEqual(page2);
|
||||
});
|
||||
|
||||
it('should support streaming presentation', () => {
|
||||
const stream: string[] = [];
|
||||
|
||||
const outputPort: UseCaseOutputPort<string> = {
|
||||
present: (data: string) => {
|
||||
stream.push(data);
|
||||
}
|
||||
};
|
||||
|
||||
// Simulate streaming data
|
||||
const chunks = ['chunk-1', 'chunk-2', 'chunk-3', 'chunk-4'];
|
||||
chunks.forEach(chunk => outputPort.present(chunk));
|
||||
|
||||
expect(stream).toHaveLength(4);
|
||||
expect(stream).toEqual(chunks);
|
||||
});
|
||||
|
||||
it('should support error handling in presentation', () => {
|
||||
const presentedData: string[] = [];
|
||||
const presentationErrors: Error[] = [];
|
||||
|
||||
const outputPort: UseCaseOutputPort<string> = {
|
||||
present: (data: string) => {
|
||||
try {
|
||||
// Simulate complex presentation logic that might fail
|
||||
if (data === 'error') {
|
||||
throw new Error('Presentation failed');
|
||||
}
|
||||
presentedData.push(data);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
presentationErrors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
outputPort.present('valid-1');
|
||||
outputPort.present('error');
|
||||
outputPort.present('valid-2');
|
||||
|
||||
expect(presentedData).toHaveLength(2);
|
||||
expect(presentedData[0]).toBe('valid-1');
|
||||
expect(presentedData[1]).toBe('valid-2');
|
||||
expect(presentationErrors).toHaveLength(1);
|
||||
expect(presentationErrors[0].message).toBe('Presentation failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UseCaseOutputPort implementation patterns', () => {
|
||||
it('should support console presenter', () => {
|
||||
const consoleOutputs: string[] = [];
|
||||
const originalConsoleLog = console.log;
|
||||
|
||||
// Mock console.log
|
||||
console.log = (...args: unknown[]) => consoleOutputs.push(args.join(' '));
|
||||
|
||||
const consolePresenter: UseCaseOutputPort<string> = {
|
||||
present: (data: string) => {
|
||||
console.log('Presented:', data);
|
||||
}
|
||||
};
|
||||
|
||||
consolePresenter.present('test data');
|
||||
|
||||
// Restore console.log
|
||||
console.log = originalConsoleLog;
|
||||
|
||||
expect(consoleOutputs).toHaveLength(1);
|
||||
expect(consoleOutputs[0]).toContain('Presented:');
|
||||
expect(consoleOutputs[0]).toContain('test data');
|
||||
});
|
||||
|
||||
it('should support HTTP response presenter', () => {
|
||||
const responses: Array<{ status: number; body: unknown; headers?: Record<string, string> }> = [];
|
||||
|
||||
const httpResponsePresenter: UseCaseOutputPort<unknown> = {
|
||||
present: (data: unknown) => {
|
||||
responses.push({
|
||||
status: 200,
|
||||
body: data,
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
httpResponsePresenter.present({ id: 1, name: 'test' });
|
||||
|
||||
expect(responses).toHaveLength(1);
|
||||
expect(responses[0].status).toBe(200);
|
||||
expect(responses[0].body).toEqual({ id: 1, name: 'test' });
|
||||
expect(responses[0].headers).toEqual({ 'content-type': 'application/json' });
|
||||
});
|
||||
|
||||
it('should support WebSocket presenter', () => {
|
||||
const messages: Array<{ type: string; data: unknown; timestamp: string }> = [];
|
||||
|
||||
const webSocketPresenter: UseCaseOutputPort<unknown> = {
|
||||
present: (data: unknown) => {
|
||||
messages.push({
|
||||
type: 'data',
|
||||
data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
webSocketPresenter.present({ event: 'user-joined', userId: 'user-123' });
|
||||
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0].type).toBe('data');
|
||||
expect(messages[0].data).toEqual({ event: 'user-joined', userId: 'user-123' });
|
||||
expect(messages[0].timestamp).toBeDefined();
|
||||
});
|
||||
|
||||
it('should support event bus presenter', () => {
|
||||
const events: Array<{ topic: string; payload: unknown; metadata: unknown }> = [];
|
||||
|
||||
const eventBusPresenter: UseCaseOutputPort<unknown> = {
|
||||
present: (data: unknown) => {
|
||||
events.push({
|
||||
topic: 'user-events',
|
||||
payload: data,
|
||||
metadata: {
|
||||
source: 'use-case',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
eventBusPresenter.present({ userId: 'user-123', action: 'created' });
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].topic).toBe('user-events');
|
||||
expect(events[0].payload).toEqual({ userId: 'user-123', action: 'created' });
|
||||
expect(events[0].metadata).toMatchObject({ source: 'use-case' });
|
||||
});
|
||||
|
||||
it('should support batch presenter', () => {
|
||||
const batches: Array<{ items: unknown[]; batchSize: number; processedAt: string }> = [];
|
||||
let currentBatch: unknown[] = [];
|
||||
const batchSize = 3;
|
||||
|
||||
const batchPresenter: UseCaseOutputPort<unknown> = {
|
||||
present: (data: unknown) => {
|
||||
currentBatch.push(data);
|
||||
|
||||
if (currentBatch.length >= batchSize) {
|
||||
batches.push({
|
||||
items: [...currentBatch],
|
||||
batchSize: currentBatch.length,
|
||||
processedAt: new Date().toISOString()
|
||||
});
|
||||
currentBatch = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Present 7 items
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
batchPresenter.present({ item: i });
|
||||
}
|
||||
|
||||
// Add remaining items
|
||||
if (currentBatch.length > 0) {
|
||||
batches.push({
|
||||
items: [...currentBatch],
|
||||
batchSize: currentBatch.length,
|
||||
processedAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
expect(batches).toHaveLength(3);
|
||||
expect(batches[0].items).toHaveLength(3);
|
||||
expect(batches[1].items).toHaveLength(3);
|
||||
expect(batches[2].items).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should support caching presenter', () => {
|
||||
const cache = new Map<string, unknown>();
|
||||
const cacheHits: string[] = [];
|
||||
const cacheMisses: string[] = [];
|
||||
|
||||
const cachingPresenter: UseCaseOutputPort<{ key: string; data: unknown }> = {
|
||||
present: (data: { key: string; data: unknown }) => {
|
||||
if (cache.has(data.key)) {
|
||||
cacheHits.push(data.key);
|
||||
} else {
|
||||
cacheMisses.push(data.key);
|
||||
cache.set(data.key, data.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cachingPresenter.present({ key: 'user-1', data: { name: 'John' } });
|
||||
cachingPresenter.present({ key: 'user-2', data: { name: 'Jane' } });
|
||||
cachingPresenter.present({ key: 'user-1', data: { name: 'John Updated' } });
|
||||
|
||||
expect(cacheHits).toHaveLength(1);
|
||||
expect(cacheHits[0]).toBe('user-1');
|
||||
expect(cacheMisses).toHaveLength(2);
|
||||
expect(cacheMisses[0]).toBe('user-1');
|
||||
expect(cacheMisses[1]).toBe('user-2');
|
||||
expect(cache.get('user-1')).toEqual({ name: 'John Updated' });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user