import { describe, it, expect } from 'vitest'; import { UseCase } from './UseCase'; import { Result } from '../domain/Result'; import { ApplicationErrorCode } from '../errors/ApplicationErrorCode'; describe('UseCase', () => { describe('UseCase interface', () => { it('should have execute method returning Promise', async () => { // Concrete implementation for testing class TestUseCase implements UseCase<{ value: number }, string, 'INVALID_VALUE'> { async execute(input: { value: number }): Promise>> { if (input.value < 0) { return Result.err({ code: 'INVALID_VALUE' }); } return Result.ok(`Value: ${input.value}`); } } const useCase = new TestUseCase(); const successResult = await useCase.execute({ value: 42 }); expect(successResult.isOk()).toBe(true); expect(successResult.unwrap()).toBe('Value: 42'); const errorResult = await useCase.execute({ value: -1 }); expect(errorResult.isErr()).toBe(true); expect(errorResult.unwrapErr()).toEqual({ code: 'INVALID_VALUE' }); }); it('should support different input types', async () => { interface CreateUserInput { name: string; email: string; } interface UserDTO { id: string; name: string; email: string; } type CreateUserErrorCode = 'INVALID_EMAIL' | 'USER_ALREADY_EXISTS'; class CreateUserUseCase implements UseCase { async execute(input: CreateUserInput): Promise>> { if (!input.email.includes('@')) { return Result.err({ code: 'INVALID_EMAIL' }); } // Simulate user creation const user: UserDTO = { id: `user-${Date.now()}`, name: input.name, email: input.email }; return Result.ok(user); } } const useCase = new CreateUserUseCase(); const successResult = await useCase.execute({ name: 'John Doe', email: 'john@example.com' }); expect(successResult.isOk()).toBe(true); const user = successResult.unwrap(); expect(user.name).toBe('John Doe'); expect(user.email).toBe('john@example.com'); expect(user.id).toMatch(/^user-\d+$/); const errorResult = await useCase.execute({ name: 'John Doe', email: 'invalid-email' }); expect(errorResult.isErr()).toBe(true); expect(errorResult.unwrapErr()).toEqual({ code: 'INVALID_EMAIL' }); }); it('should support complex error codes', async () => { interface ProcessPaymentInput { amount: number; currency: string; paymentMethod: 'credit_card' | 'paypal' | 'bank_transfer'; } interface PaymentReceipt { receiptId: string; amount: number; currency: string; status: 'completed' | 'pending' | 'failed'; } type ProcessPaymentErrorCode = | 'INSUFFICIENT_FUNDS' | 'INVALID_CURRENCY' | 'PAYMENT_METHOD_NOT_SUPPORTED' | 'NETWORK_ERROR'; class ProcessPaymentUseCase implements UseCase { async execute(input: ProcessPaymentInput): Promise>> { // Validate currency const supportedCurrencies = ['USD', 'EUR', 'GBP']; if (!supportedCurrencies.includes(input.currency)) { return Result.err({ code: 'INVALID_CURRENCY' }); } // Validate payment method const supportedMethods = ['credit_card', 'paypal']; if (!supportedMethods.includes(input.paymentMethod)) { return Result.err({ code: 'PAYMENT_METHOD_NOT_SUPPORTED' }); } // Simulate payment processing if (input.amount > 10000) { return Result.err({ code: 'INSUFFICIENT_FUNDS' }); } const receipt: PaymentReceipt = { receiptId: `receipt-${Date.now()}`, amount: input.amount, currency: input.currency, status: 'completed' }; return Result.ok(receipt); } } const useCase = new ProcessPaymentUseCase(); // Success case const successResult = await useCase.execute({ amount: 100, currency: 'USD', paymentMethod: 'credit_card' }); expect(successResult.isOk()).toBe(true); const receipt = successResult.unwrap(); expect(receipt.amount).toBe(100); expect(receipt.currency).toBe('USD'); expect(receipt.status).toBe('completed'); // Error case - invalid currency const currencyError = await useCase.execute({ amount: 100, currency: 'JPY', paymentMethod: 'credit_card' }); expect(currencyError.isErr()).toBe(true); expect(currencyError.unwrapErr()).toEqual({ code: 'INVALID_CURRENCY' }); // Error case - unsupported payment method const methodError = await useCase.execute({ amount: 100, currency: 'USD', paymentMethod: 'bank_transfer' }); expect(methodError.isErr()).toBe(true); expect(methodError.unwrapErr()).toEqual({ code: 'PAYMENT_METHOD_NOT_SUPPORTED' }); // Error case - insufficient funds const fundsError = await useCase.execute({ amount: 15000, currency: 'USD', paymentMethod: 'credit_card' }); expect(fundsError.isErr()).toBe(true); expect(fundsError.unwrapErr()).toEqual({ code: 'INSUFFICIENT_FUNDS' }); }); it('should support void success type', async () => { interface DeleteUserInput { userId: string; } type DeleteUserErrorCode = 'USER_NOT_FOUND' | 'INSUFFICIENT_PERMISSIONS'; class DeleteUserUseCase implements UseCase { async execute(input: DeleteUserInput): Promise>> { if (input.userId === 'not-found') { return Result.err({ code: 'USER_NOT_FOUND' }); } if (input.userId === 'no-permission') { return Result.err({ code: 'INSUFFICIENT_PERMISSIONS' }); } // Simulate deletion return Result.ok(undefined); } } const useCase = new DeleteUserUseCase(); const successResult = await useCase.execute({ userId: 'user-123' }); expect(successResult.isOk()).toBe(true); expect(successResult.unwrap()).toBeUndefined(); 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: 'INSUFFICIENT_PERMISSIONS' }); }); it('should support complex success types', async () => { interface SearchInput { query: string; filters?: { category?: string; priceRange?: { min: number; max: number }; inStock?: boolean; }; page?: number; limit?: number; } interface SearchResult { items: Array<{ id: string; name: string; price: number; category: string; inStock: boolean; }>; total: number; page: number; totalPages: number; } type SearchErrorCode = 'INVALID_QUERY' | 'NO_RESULTS'; class SearchUseCase implements UseCase { async execute(input: SearchInput): Promise>> { if (!input.query || input.query.length < 2) { return Result.err({ code: 'INVALID_QUERY' }); } // Simulate search results const items = [ { id: '1', name: 'Product A', price: 100, category: 'electronics', inStock: true }, { id: '2', name: 'Product B', price: 200, category: 'electronics', inStock: false }, { id: '3', name: 'Product C', price: 150, category: 'clothing', inStock: true } ]; const filteredItems = items.filter(item => { if (input.filters?.category && item.category !== input.filters.category) { return false; } if (input.filters?.priceRange) { if (item.price < input.filters.priceRange.min || item.price > input.filters.priceRange.max) { return false; } } if (input.filters?.inStock !== undefined && item.inStock !== input.filters.inStock) { return false; } return true; }); if (filteredItems.length === 0) { return Result.err({ code: 'NO_RESULTS' }); } const page = input.page || 1; const limit = input.limit || 10; const start = (page - 1) * limit; const end = start + limit; const paginatedItems = filteredItems.slice(start, end); const result: SearchResult = { items: paginatedItems, total: filteredItems.length, page, totalPages: Math.ceil(filteredItems.length / limit) }; return Result.ok(result); } } const useCase = new SearchUseCase(); // Success case const successResult = await useCase.execute({ query: 'product', filters: { category: 'electronics' }, page: 1, limit: 10 }); expect(successResult.isOk()).toBe(true); const searchResult = successResult.unwrap(); expect(searchResult.items).toHaveLength(2); expect(searchResult.total).toBe(2); expect(searchResult.page).toBe(1); expect(searchResult.totalPages).toBe(1); // Error case - invalid query const invalidQueryResult = await useCase.execute({ query: 'a' }); expect(invalidQueryResult.isErr()).toBe(true); expect(invalidQueryResult.unwrapErr()).toEqual({ code: 'INVALID_QUERY' }); // Error case - no results const noResultsResult = await useCase.execute({ query: 'product', filters: { category: 'nonexistent' } }); expect(noResultsResult.isErr()).toBe(true); expect(noResultsResult.unwrapErr()).toEqual({ code: 'NO_RESULTS' }); }); }); });