432 lines
13 KiB
TypeScript
432 lines
13 KiB
TypeScript
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' });
|
|
});
|
|
});
|
|
});
|