import { describe, it, expect } from 'vitest'; import { AsyncUseCase } from './AsyncUseCase'; import { Result } from '../domain/Result'; import { ApplicationErrorCode } from '../errors/ApplicationErrorCode'; describe('AsyncUseCase', () => { describe('AsyncUseCase interface', () => { it('should have execute method returning Promise', async () => { // Concrete implementation for testing class TestAsyncUseCase implements AsyncUseCase<{ id: string }, { data: string }, 'NOT_FOUND'> { async execute(input: { id: string }): Promise>> { if (input.id === 'not-found') { return Result.err({ code: 'NOT_FOUND' }); } return Result.ok({ data: `Data for ${input.id}` }); } } const useCase = new TestAsyncUseCase(); const successResult = await useCase.execute({ id: 'test-123' }); expect(successResult.isOk()).toBe(true); expect(successResult.unwrap()).toEqual({ data: 'Data for test-123' }); const errorResult = await useCase.execute({ id: 'not-found' }); expect(errorResult.isErr()).toBe(true); expect(errorResult.unwrapErr()).toEqual({ code: 'NOT_FOUND' }); }); it('should support different input types', async () => { interface GetUserInput { userId: string; includeProfile?: boolean; } interface UserDTO { id: string; name: string; email: string; profile?: { avatar: string; bio: string; }; } type GetUserErrorCode = 'USER_NOT_FOUND' | 'PERMISSION_DENIED'; class GetUserUseCase implements AsyncUseCase { async execute(input: GetUserInput): Promise>> { if (input.userId === 'not-found') { return Result.err({ code: 'USER_NOT_FOUND' }); } if (input.userId === 'no-permission') { return Result.err({ code: 'PERMISSION_DENIED' }); } const user: UserDTO = { id: input.userId, name: 'John Doe', email: 'john@example.com' }; if (input.includeProfile) { user.profile = { avatar: 'avatar.jpg', bio: 'Software developer' }; } return Result.ok(user); } } const useCase = new GetUserUseCase(); // Success case with profile const successWithProfile = await useCase.execute({ userId: 'user-123', includeProfile: true }); expect(successWithProfile.isOk()).toBe(true); const userWithProfile = successWithProfile.unwrap(); expect(userWithProfile.id).toBe('user-123'); expect(userWithProfile.profile).toBeDefined(); expect(userWithProfile.profile?.avatar).toBe('avatar.jpg'); // Success case without profile const successWithoutProfile = await useCase.execute({ userId: 'user-456', includeProfile: false }); expect(successWithoutProfile.isOk()).toBe(true); const userWithoutProfile = successWithoutProfile.unwrap(); expect(userWithoutProfile.id).toBe('user-456'); expect(userWithoutProfile.profile).toBeUndefined(); // Error cases const notFoundResult = await useCase.execute({ userId: 'not-found' }); expect(notFoundResult.isErr()).toBe(true); expect(notFoundResult.unwrapErr()).toEqual({ code: 'USER_NOT_FOUND' }); const permissionResult = await useCase.execute({ userId: 'no-permission' }); expect(permissionResult.isErr()).toBe(true); expect(permissionResult.unwrapErr()).toEqual({ code: 'PERMISSION_DENIED' }); }); it('should support complex query patterns', async () => { interface SearchOrdersInput { customerId?: string; status?: 'pending' | 'completed' | 'cancelled'; dateRange?: { start: Date; end: Date }; page?: number; limit?: number; } interface OrderDTO { id: string; customerId: string; status: string; total: number; items: Array<{ productId: string; quantity: number; price: number }>; createdAt: Date; } interface OrdersResult { orders: OrderDTO[]; total: number; page: number; totalPages: number; filters: SearchOrdersInput; } type SearchOrdersErrorCode = 'INVALID_FILTERS' | 'NO_ORDERS_FOUND'; class SearchOrdersUseCase implements AsyncUseCase { async execute(input: SearchOrdersInput): Promise>> { // Validate at least one filter if (!input.customerId && !input.status && !input.dateRange) { return Result.err({ code: 'INVALID_FILTERS' }); } // Simulate database query const allOrders: OrderDTO[] = [ { id: 'order-1', customerId: 'cust-1', status: 'completed', total: 150, items: [{ productId: 'prod-1', quantity: 2, price: 75 }], createdAt: new Date('2024-01-01') }, { id: 'order-2', customerId: 'cust-1', status: 'pending', total: 200, items: [{ productId: 'prod-2', quantity: 1, price: 200 }], createdAt: new Date('2024-01-02') }, { id: 'order-3', customerId: 'cust-2', status: 'completed', total: 300, items: [{ productId: 'prod-3', quantity: 3, price: 100 }], createdAt: new Date('2024-01-03') } ]; // Apply filters let filteredOrders = allOrders; if (input.customerId) { filteredOrders = filteredOrders.filter(o => o.customerId === input.customerId); } if (input.status) { filteredOrders = filteredOrders.filter(o => o.status === input.status); } if (input.dateRange) { filteredOrders = filteredOrders.filter(o => { const orderDate = o.createdAt.getTime(); return orderDate >= input.dateRange!.start.getTime() && orderDate <= input.dateRange!.end.getTime(); }); } if (filteredOrders.length === 0) { return Result.err({ code: 'NO_ORDERS_FOUND' }); } // Apply pagination const page = input.page || 1; const limit = input.limit || 10; const start = (page - 1) * limit; const end = start + limit; const paginatedOrders = filteredOrders.slice(start, end); const result: OrdersResult = { orders: paginatedOrders, total: filteredOrders.length, page, totalPages: Math.ceil(filteredOrders.length / limit), filters: input }; return Result.ok(result); } } const useCase = new SearchOrdersUseCase(); // Success case - filter by customer const customerResult = await useCase.execute({ customerId: 'cust-1' }); expect(customerResult.isOk()).toBe(true); const customerOrders = customerResult.unwrap(); expect(customerOrders.orders).toHaveLength(2); expect(customerOrders.total).toBe(2); // Success case - filter by status const statusResult = await useCase.execute({ status: 'completed' }); expect(statusResult.isOk()).toBe(true); const completedOrders = statusResult.unwrap(); expect(completedOrders.orders).toHaveLength(2); expect(completedOrders.total).toBe(2); // Success case - filter by date range const dateResult = await useCase.execute({ dateRange: { start: new Date('2024-01-01'), end: new Date('2024-01-02') } }); expect(dateResult.isOk()).toBe(true); const dateOrders = dateResult.unwrap(); expect(dateOrders.orders).toHaveLength(2); expect(dateOrders.total).toBe(2); // Error case - no filters const noFiltersResult = await useCase.execute({}); expect(noFiltersResult.isErr()).toBe(true); expect(noFiltersResult.unwrapErr()).toEqual({ code: 'INVALID_FILTERS' }); // Error case - no matching orders const noOrdersResult = await useCase.execute({ customerId: 'nonexistent' }); expect(noOrdersResult.isErr()).toBe(true); expect(noOrdersResult.unwrapErr()).toEqual({ code: 'NO_ORDERS_FOUND' }); }); it('should support async operations with delays', async () => { interface ProcessBatchInput { items: Array<{ id: string; data: string }>; delayMs?: number; } interface ProcessBatchResult { processed: number; failed: number; results: Array<{ id: string; status: 'success' | 'failed'; message?: string }>; } type ProcessBatchErrorCode = 'EMPTY_BATCH' | 'PROCESSING_ERROR'; class ProcessBatchUseCase implements AsyncUseCase { async execute(input: ProcessBatchInput): Promise>> { if (input.items.length === 0) { return Result.err({ code: 'EMPTY_BATCH' }); } const delay = input.delayMs || 10; const results: Array<{ id: string; status: 'success' | 'failed'; message?: string }> = []; let processed = 0; let failed = 0; for (const item of input.items) { // Simulate async processing with delay await new Promise(resolve => setTimeout(resolve, delay)); // Simulate some failures if (item.id === 'fail-1' || item.id === 'fail-2') { results.push({ id: item.id, status: 'failed', message: 'Processing failed' }); failed++; } else { results.push({ id: item.id, status: 'success' }); processed++; } } return Result.ok({ processed, failed, results }); } } const useCase = new ProcessBatchUseCase(); // Success case const successResult = await useCase.execute({ items: [ { id: 'item-1', data: 'data1' }, { id: 'item-2', data: 'data2' }, { id: 'item-3', data: 'data3' } ], delayMs: 5 }); expect(successResult.isOk()).toBe(true); const batchResult = successResult.unwrap(); expect(batchResult.processed).toBe(3); expect(batchResult.failed).toBe(0); expect(batchResult.results).toHaveLength(3); // Mixed success/failure case const mixedResult = await useCase.execute({ items: [ { id: 'item-1', data: 'data1' }, { id: 'fail-1', data: 'data2' }, { id: 'item-3', data: 'data3' }, { id: 'fail-2', data: 'data4' } ], delayMs: 5 }); expect(mixedResult.isOk()).toBe(true); const mixedBatchResult = mixedResult.unwrap(); expect(mixedBatchResult.processed).toBe(2); expect(mixedBatchResult.failed).toBe(2); expect(mixedBatchResult.results).toHaveLength(4); // Error case - empty batch const emptyResult = await useCase.execute({ items: [] }); expect(emptyResult.isErr()).toBe(true); expect(emptyResult.unwrapErr()).toEqual({ code: 'EMPTY_BATCH' }); }); it('should support streaming-like operations', async () => { interface StreamInput { source: string; chunkSize?: number; } interface StreamResult { chunks: string[]; totalSize: number; source: string; } type StreamErrorCode = 'SOURCE_NOT_FOUND' | 'STREAM_ERROR'; class StreamUseCase implements AsyncUseCase { async execute(input: StreamInput): Promise>> { if (input.source === 'not-found') { return Result.err({ code: 'SOURCE_NOT_FOUND' }); } if (input.source === 'error') { return Result.err({ code: 'STREAM_ERROR' }); } const chunkSize = input.chunkSize || 10; const data = 'This is a test data stream that will be split into chunks'; const chunks: string[] = []; for (let i = 0; i < data.length; i += chunkSize) { // Simulate async chunk reading await new Promise(resolve => setTimeout(resolve, 1)); chunks.push(data.slice(i, i + chunkSize)); } return Result.ok({ chunks, totalSize: data.length, source: input.source }); } } const useCase = new StreamUseCase(); // Success case with default chunk size const defaultResult = await useCase.execute({ source: 'test-source' }); expect(defaultResult.isOk()).toBe(true); const defaultStream = defaultResult.unwrap(); expect(defaultStream.chunks).toHaveLength(6); expect(defaultStream.totalSize).toBe(57); expect(defaultStream.source).toBe('test-source'); // Success case with custom chunk size const customResult = await useCase.execute({ source: 'test-source', chunkSize: 15 }); expect(customResult.isOk()).toBe(true); const customStream = customResult.unwrap(); expect(customStream.chunks).toHaveLength(4); expect(customStream.totalSize).toBe(57); // Error case - source not found const notFoundResult = await useCase.execute({ source: 'not-found' }); expect(notFoundResult.isErr()).toBe(true); expect(notFoundResult.unwrapErr()).toEqual({ code: 'SOURCE_NOT_FOUND' }); // Error case - stream error const errorResult = await useCase.execute({ source: 'error' }); expect(errorResult.isErr()).toBe(true); expect(errorResult.unwrapErr()).toEqual({ code: 'STREAM_ERROR' }); }); }); });