Files
gridpilot.gg/core/shared/application/UseCaseOutputPort.test.ts
Marc Mintel 280d6fc199
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m51s
Contract Testing / contract-snapshot (pull_request) Has been skipped
core tests
2026-01-22 18:44:01 +01:00

434 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);
// Update cache with new data even if key exists
cache.set(data.key, data.data);
} 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' });
});
});
});