import { describe, it, expect } from 'vitest'; import { DomainError, CommonDomainErrorKind, IDomainError, DomainErrorAlias } from './DomainError'; describe('DomainError', () => { describe('DomainError interface', () => { it('should have required properties', () => { const error: DomainError = { name: 'DomainError', type: 'domain', context: 'user-service', kind: 'validation', message: 'Invalid user data' }; expect(error.type).toBe('domain'); expect(error.context).toBe('user-service'); expect(error.kind).toBe('validation'); expect(error.message).toBe('Invalid user data'); }); it('should support different error kinds', () => { const validationError: DomainError = { name: 'DomainError', type: 'domain', context: 'form-service', kind: 'validation', message: 'Invalid input' }; const invariantError: DomainError = { name: 'DomainError', type: 'domain', context: 'order-service', kind: 'invariant', message: 'Order total must be positive' }; const customError: DomainError = { name: 'DomainError', type: 'domain', context: 'payment-service', kind: 'BUSINESS_RULE_VIOLATION', message: 'Payment exceeds limit' }; expect(validationError.kind).toBe('validation'); expect(invariantError.kind).toBe('invariant'); expect(customError.kind).toBe('BUSINESS_RULE_VIOLATION'); }); it('should support custom error kinds', () => { const customError: DomainError<'INVALID_STATE'> = { name: 'DomainError', type: 'domain', context: 'state-machine', kind: 'INVALID_STATE', message: 'Cannot transition from current state' }; expect(customError.kind).toBe('INVALID_STATE'); }); }); describe('CommonDomainErrorKind type', () => { it('should include standard error kinds', () => { const kinds: CommonDomainErrorKind[] = ['validation', 'invariant']; kinds.forEach(kind => { const error: DomainError = { name: 'DomainError', type: 'domain', context: 'test', kind, message: `Test ${kind} error` }; expect(error.kind).toBe(kind); }); }); it('should support string extension for custom kinds', () => { const customKinds: CommonDomainErrorKind[] = [ 'BUSINESS_RULE_VIOLATION', 'DOMAIN_CONSTRAINT_VIOLATION', 'AGGREGATE_ROOT_VIOLATION' ]; customKinds.forEach(kind => { const error: DomainError = { name: 'DomainError', type: 'domain', context: 'test', kind, message: `Test ${kind} error` }; expect(error.kind).toBe(kind); }); }); }); describe('DomainError behavior', () => { it('should be assignable to Error interface', () => { const error: DomainError = { type: 'domain', context: 'user-service', kind: 'validation', message: 'Validation failed' }; // DomainError extends Error expect(error.type).toBe('domain'); expect(error.message).toBe('Validation failed'); }); it('should support error inheritance pattern', () => { class CustomDomainError extends Error implements DomainError { readonly type: 'domain' = 'domain'; readonly context: string; readonly kind: string; constructor(context: string, kind: string, message: string) { super(message); this.context = context; this.kind = kind; this.name = 'CustomDomainError'; } } const error = new CustomDomainError( 'user-service', 'VALIDATION_ERROR', 'User email is invalid' ); expect(error.type).toBe('domain'); expect(error.context).toBe('user-service'); expect(error.kind).toBe('VALIDATION_ERROR'); expect(error.message).toBe('User email is invalid'); expect(error.name).toBe('CustomDomainError'); expect(error.stack).toBeDefined(); }); it('should support error serialization', () => { const error: DomainError = { type: 'domain', context: 'order-service', kind: 'invariant', message: 'Order total must be positive', details: { total: -100 } }; const serialized = JSON.stringify(error); const parsed = JSON.parse(serialized); expect(parsed.type).toBe('domain'); expect(parsed.context).toBe('order-service'); expect(parsed.kind).toBe('invariant'); expect(parsed.message).toBe('Order total must be positive'); expect(parsed.details).toEqual({ total: -100 }); }); it('should support error deserialization', () => { const serialized = JSON.stringify({ type: 'domain', context: 'payment-service', kind: 'BUSINESS_RULE_VIOLATION', message: 'Payment exceeds limit', details: { limit: 1000, amount: 1500 } }); const parsed: DomainError = JSON.parse(serialized); expect(parsed.type).toBe('domain'); expect(parsed.context).toBe('payment-service'); expect(parsed.kind).toBe('BUSINESS_RULE_VIOLATION'); expect(parsed.message).toBe('Payment exceeds limit'); expect(parsed.details).toEqual({ limit: 1000, amount: 1500 }); }); it('should support error comparison', () => { const error1: DomainError = { type: 'domain', context: 'user-service', kind: 'validation', message: 'Invalid email' }; const error2: DomainError = { type: 'domain', context: 'user-service', kind: 'validation', message: 'Invalid email' }; const error3: DomainError = { type: 'domain', context: 'order-service', kind: 'invariant', message: 'Order total must be positive' }; // 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: DomainError[] = [ { type: 'domain', context: 'user-service', kind: 'validation', message: 'Invalid email' }, { type: 'domain', context: 'order-service', kind: 'invariant', message: 'Order total must be positive' }, { type: 'domain', context: 'payment-service', kind: 'BUSINESS_RULE_VIOLATION', message: 'Payment exceeds limit' } ]; const validationErrors = errors.filter(e => e.kind === 'validation'); const invariantErrors = errors.filter(e => e.kind === 'invariant'); const businessRuleErrors = errors.filter(e => e.kind === 'BUSINESS_RULE_VIOLATION'); expect(validationErrors).toHaveLength(1); expect(invariantErrors).toHaveLength(1); expect(businessRuleErrors).toHaveLength(1); }); it('should support error aggregation by context', () => { const errors: DomainError[] = [ { type: 'domain', context: 'user-service', kind: 'validation', message: 'Invalid email' }, { type: 'domain', context: 'user-service', kind: 'invariant', message: 'User must have at least one role' }, { type: 'domain', context: 'order-service', kind: 'invariant', message: 'Order total must be positive' } ]; 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('IDomainError interface', () => { it('should be assignable to DomainError', () => { const error: IDomainError = { type: 'domain', context: 'user-service', kind: 'validation', message: 'Invalid email' }; expect(error.type).toBe('domain'); expect(error.context).toBe('user-service'); expect(error.kind).toBe('validation'); expect(error.message).toBe('Invalid email'); }); it('should support different error kinds', () => { const validationError: IDomainError = { type: 'domain', context: 'form-service', kind: 'validation', message: 'Invalid input' }; const invariantError: IDomainError = { type: 'domain', context: 'order-service', kind: 'invariant', message: 'Order total must be positive' }; expect(validationError.kind).toBe('validation'); expect(invariantError.kind).toBe('invariant'); }); }); describe('DomainErrorAlias type', () => { it('should be assignable to DomainError', () => { const alias: DomainErrorAlias = { type: 'domain', context: 'user-service', kind: 'validation', message: 'Invalid email' }; expect(alias.type).toBe('domain'); expect(alias.context).toBe('user-service'); expect(alias.kind).toBe('validation'); expect(alias.message).toBe('Invalid email'); }); it('should support different error kinds', () => { const validationError: DomainErrorAlias = { type: 'domain', context: 'form-service', kind: 'validation', message: 'Invalid input' }; const invariantError: DomainErrorAlias = { type: 'domain', context: 'order-service', kind: 'invariant', message: 'Order total must be positive' }; expect(validationError.kind).toBe('validation'); expect(invariantError.kind).toBe('invariant'); }); }); describe('DomainError implementation patterns', () => { it('should support error factory pattern', () => { function createDomainError( context: string, kind: K, message: string, details?: unknown ): DomainError { return { type: 'domain', context, kind, message, details }; } const validationError = createDomainError( 'user-service', 'VALIDATION_ERROR', 'Invalid email', { field: 'email', value: 'invalid' } ); const invariantError = createDomainError( 'order-service', 'INVARIANT_VIOLATION', 'Order total must be positive' ); expect(validationError.kind).toBe('VALIDATION_ERROR'); expect(validationError.details).toEqual({ field: 'email', value: 'invalid' }); expect(invariantError.kind).toBe('INVARIANT_VIOLATION'); expect(invariantError.details).toBeUndefined(); }); it('should support error builder pattern', () => { class DomainErrorBuilder { 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(): DomainError { return { type: 'domain', context: this.context, kind: this.kind, message: this.message, details: this.details }; } } const error = new DomainErrorBuilder<'VALIDATION_ERROR'>() .withContext('user-service') .withKind('VALIDATION_ERROR') .withMessage('Invalid email') .withDetails({ field: 'email', value: 'invalid' }) .build(); expect(error.type).toBe('domain'); expect(error.context).toBe('user-service'); expect(error.kind).toBe('VALIDATION_ERROR'); expect(error.message).toBe('Invalid email'); expect(error.details).toEqual({ field: 'email', value: 'invalid' }); }); it('should support error categorization by severity', () => { const errors: DomainError[] = [ { type: 'domain', context: 'user-service', kind: 'VALIDATION_ERROR', message: 'Invalid email', details: { severity: 'low' } }, { type: 'domain', context: 'order-service', kind: 'INVARIANT_VIOLATION', message: 'Order total must be positive', details: { severity: 'high' } }, { type: 'domain', context: 'payment-service', kind: 'BUSINESS_RULE_VIOLATION', message: 'Payment exceeds limit', details: { severity: 'critical' } } ]; const lowSeverity = errors.filter(e => (e.details as { severity: string })?.severity === 'low'); const highSeverity = errors.filter(e => (e.details as { severity: string })?.severity === 'high'); const criticalSeverity = errors.filter(e => (e.details as { severity: string })?.severity === 'critical'); expect(lowSeverity).toHaveLength(1); expect(highSeverity).toHaveLength(1); expect(criticalSeverity).toHaveLength(1); }); it('should support domain invariant violations', () => { const invariantError: DomainError<'INVARIANT_VIOLATION'> = { type: 'domain', context: 'order-service', kind: 'INVARIANT_VIOLATION', message: 'Order total must be positive', details: { invariant: 'total > 0', actualValue: -100, expectedValue: '> 0' } }; expect(invariantError.kind).toBe('INVARIANT_VIOLATION'); expect(invariantError.details).toEqual({ invariant: 'total > 0', actualValue: -100, expectedValue: '> 0' }); }); it('should support business rule violations', () => { const businessRuleError: DomainError<'BUSINESS_RULE_VIOLATION'> = { type: 'domain', context: 'payment-service', kind: 'BUSINESS_RULE_VIOLATION', message: 'Payment exceeds limit', details: { rule: 'payment_limit', limit: 1000, attempted: 1500, currency: 'USD' } }; expect(businessRuleError.kind).toBe('BUSINESS_RULE_VIOLATION'); expect(businessRuleError.details).toEqual({ rule: 'payment_limit', limit: 1000, attempted: 1500, currency: 'USD' }); }); }); });