import { describe, it, expect } from 'vitest'; import { ApplicationError, CommonApplicationErrorKind } from './ApplicationError'; describe('ApplicationError', () => { describe('ApplicationError interface', () => { it('should have required properties', () => { const error: ApplicationError = { name: 'ApplicationError', type: 'application', context: 'user-service', kind: 'not_found', message: 'User not found' }; expect(error.type).toBe('application'); expect(error.context).toBe('user-service'); expect(error.kind).toBe('not_found'); expect(error.message).toBe('User not found'); }); it('should support optional details', () => { const error: ApplicationError = { name: 'ApplicationError', type: 'application', context: 'payment-service', kind: 'validation', message: 'Invalid payment amount', details: { amount: -100, minAmount: 0 } }; expect(error.details).toEqual({ amount: -100, minAmount: 0 }); }); it('should support different error kinds', () => { const notFoundError: ApplicationError = { name: 'ApplicationError', type: 'application', context: 'user-service', kind: 'not_found', message: 'User not found' }; const forbiddenError: ApplicationError = { name: 'ApplicationError', type: 'application', context: 'admin-service', kind: 'forbidden', message: 'Access denied' }; const conflictError: ApplicationError = { name: 'ApplicationError', type: 'application', context: 'order-service', kind: 'conflict', message: 'Order already exists' }; const validationError: ApplicationError = { name: 'ApplicationError', type: 'application', context: 'form-service', kind: 'validation', message: 'Invalid input' }; const unknownError: ApplicationError = { name: 'ApplicationError', type: 'application', context: 'unknown-service', kind: 'unknown', message: 'Unknown error' }; expect(notFoundError.kind).toBe('not_found'); expect(forbiddenError.kind).toBe('forbidden'); expect(conflictError.kind).toBe('conflict'); expect(validationError.kind).toBe('validation'); expect(unknownError.kind).toBe('unknown'); }); it('should support custom error kinds', () => { const customError: ApplicationError<'RATE_LIMIT_EXCEEDED'> = { name: 'ApplicationError', type: 'application', context: 'api-gateway', kind: 'RATE_LIMIT_EXCEEDED', message: 'Rate limit exceeded', details: { retryAfter: 60 } }; expect(customError.kind).toBe('RATE_LIMIT_EXCEEDED'); expect(customError.details).toEqual({ retryAfter: 60 }); }); }); describe('CommonApplicationErrorKind type', () => { it('should include standard error kinds', () => { const kinds: CommonApplicationErrorKind[] = [ 'not_found', 'forbidden', 'conflict', 'validation', 'unknown' ]; kinds.forEach(kind => { const error: ApplicationError = { name: 'ApplicationError', type: 'application', context: 'test', kind, message: `Test ${kind} error` }; expect(error.kind).toBe(kind); }); }); it('should support string extension for custom kinds', () => { const customKinds: CommonApplicationErrorKind[] = [ 'CUSTOM_ERROR_1', 'CUSTOM_ERROR_2', 'BUSINESS_RULE_VIOLATION' ]; customKinds.forEach(kind => { const error: ApplicationError = { name: 'ApplicationError', type: 'application', context: 'test', kind, message: `Test ${kind} error` }; expect(error.kind).toBe(kind); }); }); }); describe('ApplicationError behavior', () => { it('should be assignable to Error interface', () => { const error: ApplicationError = { name: 'ApplicationError', type: 'application', context: 'test-service', kind: 'validation', message: 'Validation failed' }; // ApplicationError extends Error expect(error.type).toBe('application'); expect(error.message).toBe('Validation failed'); }); it('should support error inheritance pattern', () => { class CustomApplicationError extends Error implements ApplicationError { readonly type: 'application' = 'application'; readonly context: string; readonly kind: string; readonly details?: unknown; constructor(context: string, kind: string, message: string, details?: unknown) { super(message); this.context = context; this.kind = kind; this.details = details; this.name = 'CustomApplicationError'; } } const error = new CustomApplicationError( 'user-service', 'USER_NOT_FOUND', 'User with ID 123 not found', { userId: '123' } ); expect(error.type).toBe('application'); expect(error.context).toBe('user-service'); expect(error.kind).toBe('USER_NOT_FOUND'); expect(error.message).toBe('User with ID 123 not found'); expect(error.details).toEqual({ userId: '123' }); expect(error.name).toBe('CustomApplicationError'); expect(error.stack).toBeDefined(); }); it('should support error serialization', () => { const error: ApplicationError = { name: 'ApplicationError', type: 'application', context: 'payment-service', kind: 'INSUFFICIENT_FUNDS', message: 'Insufficient funds for transaction', details: { balance: 50, required: 100, currency: 'USD' } }; const serialized = JSON.stringify(error); const parsed = JSON.parse(serialized); expect(parsed.type).toBe('application'); expect(parsed.context).toBe('payment-service'); expect(parsed.kind).toBe('INSUFFICIENT_FUNDS'); expect(parsed.message).toBe('Insufficient funds for transaction'); expect(parsed.details).toEqual({ balance: 50, required: 100, currency: 'USD' }); }); it('should support error deserialization', () => { const serialized = JSON.stringify({ type: 'application', context: 'auth-service', kind: 'INVALID_CREDENTIALS', message: 'Invalid username or password', details: { attempt: 3 } }); const parsed: ApplicationError = JSON.parse(serialized); expect(parsed.type).toBe('application'); expect(parsed.context).toBe('auth-service'); expect(parsed.kind).toBe('INVALID_CREDENTIALS'); expect(parsed.message).toBe('Invalid username or password'); expect(parsed.details).toEqual({ attempt: 3 }); }); it('should support error comparison', () => { const error1: ApplicationError = { name: 'ApplicationError', type: 'application', context: 'user-service', kind: 'not_found', message: 'User not found' }; const error2: ApplicationError = { name: 'ApplicationError', type: 'application', context: 'user-service', kind: 'not_found', message: 'User not found' }; const error3: ApplicationError = { name: 'ApplicationError', type: 'application', context: 'order-service', kind: 'not_found', message: 'Order not found' }; // Same kind and context expect(error1.kind).toBe(error2.kind); expect(error1.context).toBe(error2.context); // Different context expect(error1.context).not.toBe(error3.context); }); it('should support error categorization', () => { const errors: ApplicationError[] = [ { name: 'ApplicationError', type: 'application', context: 'user-service', kind: 'not_found', message: 'User not found' }, { name: 'ApplicationError', type: 'application', context: 'admin-service', kind: 'forbidden', message: 'Access denied' }, { name: 'ApplicationError', type: 'application', context: 'order-service', kind: 'conflict', message: 'Order already exists' }, { name: 'ApplicationError', type: 'application', context: 'form-service', kind: 'validation', message: 'Invalid input' } ]; const notFoundErrors = errors.filter(e => e.kind === 'not_found'); const forbiddenErrors = errors.filter(e => e.kind === 'forbidden'); const conflictErrors = errors.filter(e => e.kind === 'conflict'); const validationErrors = errors.filter(e => e.kind === 'validation'); expect(notFoundErrors).toHaveLength(1); expect(forbiddenErrors).toHaveLength(1); expect(conflictErrors).toHaveLength(1); expect(validationErrors).toHaveLength(1); }); it('should support error aggregation by context', () => { const errors: ApplicationError[] = [ { name: 'ApplicationError', type: 'application', context: 'user-service', kind: 'not_found', message: 'User not found' }, { name: 'ApplicationError', type: 'application', context: 'user-service', kind: 'validation', message: 'Invalid user data' }, { name: 'ApplicationError', type: 'application', context: 'order-service', kind: 'not_found', message: 'Order not found' } ]; const userErrors = errors.filter(e => e.context === 'user-service'); const orderErrors = errors.filter(e => e.context === 'order-service'); expect(userErrors).toHaveLength(2); expect(orderErrors).toHaveLength(1); }); }); describe('ApplicationError implementation patterns', () => { it('should support error factory pattern', () => { function createApplicationError( context: string, kind: K, message: string, details?: unknown ): ApplicationError { return { name: 'ApplicationError', type: 'application', context, kind, message, details }; } const notFoundError = createApplicationError( 'user-service', 'USER_NOT_FOUND', 'User not found', { userId: '123' } ); const validationError = createApplicationError( 'form-service', 'VALIDATION_ERROR', 'Invalid form data', { field: 'email', value: 'invalid' } ); expect(notFoundError.kind).toBe('USER_NOT_FOUND'); expect(notFoundError.details).toEqual({ userId: '123' }); expect(validationError.kind).toBe('VALIDATION_ERROR'); expect(validationError.details).toEqual({ field: 'email', value: 'invalid' }); }); it('should support error builder pattern', () => { class ApplicationErrorBuilder { private context: string = ''; private kind: K = '' as K; private message: string = ''; private details?: unknown; withContext(context: string): this { this.context = context; return this; } withKind(kind: K): this { this.kind = kind; return this; } withMessage(message: string): this { this.message = message; return this; } withDetails(details: unknown): this { this.details = details; return this; } build(): ApplicationError { return { name: 'ApplicationError', type: 'application', context: this.context, kind: this.kind, message: this.message, details: this.details }; } } const error = new ApplicationErrorBuilder<'USER_NOT_FOUND'>() .withContext('user-service') .withKind('USER_NOT_FOUND') .withMessage('User not found') .withDetails({ userId: '123' }) .build(); expect(error.type).toBe('application'); expect(error.context).toBe('user-service'); expect(error.kind).toBe('USER_NOT_FOUND'); expect(error.message).toBe('User not found'); expect(error.details).toEqual({ userId: '123' }); }); it('should support error categorization by severity', () => { const errors: ApplicationError[] = [ { name: 'ApplicationError', type: 'application', context: 'auth-service', kind: 'INVALID_CREDENTIALS', message: 'Invalid credentials', details: { severity: 'high' } }, { name: 'ApplicationError', type: 'application', context: 'user-service', kind: 'USER_NOT_FOUND', message: 'User not found', details: { severity: 'medium' } }, { name: 'ApplicationError', type: 'application', context: 'cache-service', kind: 'CACHE_MISS', message: 'Cache miss', details: { severity: 'low' } } ]; const highSeverity = errors.filter(e => (e.details as { severity: string })?.severity === 'high'); const mediumSeverity = errors.filter(e => (e.details as { severity: string })?.severity === 'medium'); const lowSeverity = errors.filter(e => (e.details as { severity: string })?.severity === 'low'); expect(highSeverity).toHaveLength(1); expect(mediumSeverity).toHaveLength(1); expect(lowSeverity).toHaveLength(1); }); }); });