import { describe, it, expect } from 'vitest'; import { ApplicationService, AsyncApplicationService, AsyncResultApplicationService } from './Service'; import { Result } from '../domain/Result'; import { ApplicationError } from '../errors/ApplicationError'; describe('Service', () => { describe('ApplicationService interface', () => { it('should have optional serviceName property', () => { const service: ApplicationService = { serviceName: 'TestService' }; expect(service.serviceName).toBe('TestService'); }); it('should work without serviceName', () => { const service: ApplicationService = {}; expect(service.serviceName).toBeUndefined(); }); it('should support different service implementations', () => { const service1: ApplicationService = { serviceName: 'Service1' }; const service2: ApplicationService = { serviceName: 'Service2' }; const service3: ApplicationService = {}; expect(service1.serviceName).toBe('Service1'); expect(service2.serviceName).toBe('Service2'); expect(service3.serviceName).toBeUndefined(); }); }); describe('AsyncApplicationService interface', () => { it('should have execute method returning Promise', async () => { const service: AsyncApplicationService = { execute: async (input: string) => input.length }; const result = await service.execute('test'); expect(result).toBe(4); }); it('should support different input and output types', async () => { const stringService: AsyncApplicationService = { execute: async (input: string) => input.toUpperCase() }; const objectService: AsyncApplicationService<{ x: number; y: number }, number> = { execute: async (input) => input.x + input.y }; const arrayService: AsyncApplicationService = { execute: async (input) => input.map(x => x * 2) }; expect(await stringService.execute('hello')).toBe('HELLO'); expect(await objectService.execute({ x: 3, y: 4 })).toBe(7); expect(await arrayService.execute([1, 2, 3])).toEqual([2, 4, 6]); }); it('should support async operations with delays', async () => { const service: AsyncApplicationService = { execute: async (input: string) => { await new Promise(resolve => setTimeout(resolve, 10)); return `Processed: ${input}`; } }; const start = Date.now(); const result = await service.execute('test'); const elapsed = Date.now() - start; expect(result).toBe('Processed: test'); expect(elapsed).toBeGreaterThanOrEqual(10); }); it('should support complex async operations', async () => { interface FetchUserInput { userId: string; includePosts?: boolean; } interface UserWithPosts { id: string; name: string; email: string; posts: Array<{ id: string; title: string; content: string }>; } const userService: AsyncApplicationService = { execute: async (input: FetchUserInput) => { // Simulate async database fetch await new Promise(resolve => setTimeout(resolve, 5)); const user: UserWithPosts = { id: input.userId, name: 'John Doe', email: 'john@example.com', posts: [] }; if (input.includePosts) { user.posts = [ { id: 'post-1', title: 'First Post', content: 'Content 1' }, { id: 'post-2', title: 'Second Post', content: 'Content 2' } ]; } return user; } }; const userWithPosts = await userService.execute({ userId: 'user-123', includePosts: true }); expect(userWithPosts.id).toBe('user-123'); expect(userWithPosts.posts).toHaveLength(2); const userWithoutPosts = await userService.execute({ userId: 'user-456', includePosts: false }); expect(userWithoutPosts.id).toBe('user-456'); expect(userWithoutPosts.posts).toHaveLength(0); }); }); describe('AsyncResultApplicationService interface', () => { it('should have execute method returning Promise', async () => { const service: AsyncResultApplicationService = { execute: async (input: string) => { if (input.length === 0) { return Result.err('Input cannot be empty'); } return Result.ok(input.length); } }; const successResult = await service.execute('test'); expect(successResult.isOk()).toBe(true); expect(successResult.unwrap()).toBe(4); const errorResult = await service.execute(''); expect(errorResult.isErr()).toBe(true); expect(errorResult.unwrapErr()).toBe('Input cannot be empty'); }); it('should support validation logic', async () => { interface ValidationResult { isValid: boolean; errors: string[]; } const validator: AsyncResultApplicationService = { execute: async (input: string) => { const errors: string[] = []; if (input.length < 3) { errors.push('Must be at least 3 characters'); } if (!input.match(/^[a-zA-Z]+$/)) { errors.push('Must contain only letters'); } if (errors.length > 0) { return Result.err(errors.join(', ')); } return Result.ok({ isValid: true, errors: [] }); } }; const validResult = await validator.execute('Hello'); expect(validResult.isOk()).toBe(true); expect(validResult.unwrap()).toEqual({ isValid: true, errors: [] }); const invalidResult = await validator.execute('ab'); expect(invalidResult.isErr()).toBe(true); expect(invalidResult.unwrapErr()).toBe('Must be at least 3 characters'); }); it('should support complex business rules', async () => { interface ProcessOrderInput { items: Array<{ productId: string; quantity: number; price: number }>; customerType: 'regular' | 'premium' | 'vip'; hasCoupon: boolean; } interface OrderResult { orderId: string; subtotal: number; discount: number; total: number; status: 'processed' | 'pending' | 'failed'; } const orderProcessor: AsyncResultApplicationService = { execute: async (input: ProcessOrderInput) => { // Calculate subtotal const subtotal = input.items.reduce((sum, item) => sum + (item.quantity * item.price), 0); if (subtotal <= 0) { return Result.err('Order must have items with positive prices'); } // Calculate discount let discount = 0; // Customer type discount switch (input.customerType) { case 'premium': discount += subtotal * 0.1; break; case 'vip': discount += subtotal * 0.2; break; } // Coupon discount if (input.hasCoupon) { discount += subtotal * 0.05; } const total = subtotal - discount; const orderResult: OrderResult = { orderId: `order-${Date.now()}`, subtotal, discount, total, status: 'processed' }; return Result.ok(orderResult); } }; // Success case - VIP with coupon const vipWithCoupon = await orderProcessor.execute({ items: [ { productId: 'prod-1', quantity: 2, price: 100 }, { productId: 'prod-2', quantity: 1, price: 50 } ], customerType: 'vip', hasCoupon: true }); expect(vipWithCoupon.isOk()).toBe(true); const order1 = vipWithCoupon.unwrap(); expect(order1.subtotal).toBe(250); // 2*100 + 1*50 expect(order1.discount).toBe(62.5); // 250 * 0.2 + 250 * 0.05 expect(order1.total).toBe(187.5); // 250 - 62.5 // Success case - Regular without coupon const regularWithoutCoupon = await orderProcessor.execute({ items: [{ productId: 'prod-1', quantity: 1, price: 100 }], customerType: 'regular', hasCoupon: false }); expect(regularWithoutCoupon.isOk()).toBe(true); const order2 = regularWithoutCoupon.unwrap(); expect(order2.subtotal).toBe(100); expect(order2.discount).toBe(0); expect(order2.total).toBe(100); // Error case - Empty order const emptyOrder = await orderProcessor.execute({ items: [], customerType: 'regular', hasCoupon: false }); expect(emptyOrder.isErr()).toBe(true); expect(emptyOrder.unwrapErr()).toBe('Order must have items with positive prices'); }); it('should support async operations with delays', async () => { interface ProcessBatchInput { items: Array<{ id: string; data: string }>; delayMs?: number; } interface BatchResult { processed: number; failed: number; results: Array<{ id: string; status: 'success' | 'failed'; message?: string }>; } const batchProcessor: AsyncResultApplicationService = { execute: async (input: ProcessBatchInput) => { if (input.items.length === 0) { return Result.err('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 }); } }; // Success case const successResult = await batchProcessor.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 batchProcessor.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 batchProcessor.execute({ items: [] }); expect(emptyResult.isErr()).toBe(true); expect(emptyResult.unwrapErr()).toBe('Empty batch'); }); it('should support error handling with ApplicationError', async () => { interface ProcessPaymentInput { amount: number; currency: string; } interface PaymentReceipt { receiptId: string; amount: number; currency: string; status: 'completed' | 'failed'; } const paymentProcessor: AsyncResultApplicationService = { execute: async (input: ProcessPaymentInput) => { // Validate amount if (input.amount <= 0) { return Result.err({ type: 'application', context: 'payment', kind: 'validation', message: 'Amount must be positive' } as ApplicationError); } // Validate currency const supportedCurrencies = ['USD', 'EUR', 'GBP']; if (!supportedCurrencies.includes(input.currency)) { return Result.err({ type: 'application', context: 'payment', kind: 'validation', message: `Currency ${input.currency} is not supported` } as ApplicationError); } // Simulate payment processing const receipt: PaymentReceipt = { receiptId: `receipt-${Date.now()}`, amount: input.amount, currency: input.currency, status: 'completed' }; return Result.ok(receipt); } }; // Success case const successResult = await paymentProcessor.execute({ amount: 100, currency: 'USD' }); 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 amount const invalidAmountResult = await paymentProcessor.execute({ amount: -50, currency: 'USD' }); expect(invalidAmountResult.isErr()).toBe(true); const error1 = invalidAmountResult.unwrapErr(); expect(error1.type).toBe('application'); expect(error1.kind).toBe('validation'); expect(error1.message).toBe('Amount must be positive'); // Error case - invalid currency const invalidCurrencyResult = await paymentProcessor.execute({ amount: 100, currency: 'JPY' }); expect(invalidCurrencyResult.isErr()).toBe(true); const error2 = invalidCurrencyResult.unwrapErr(); expect(error2.type).toBe('application'); expect(error2.kind).toBe('validation'); expect(error2.message).toBe('Currency JPY is not supported'); }); }); });