509 lines
15 KiB
TypeScript
509 lines
15 KiB
TypeScript
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<K extends string>(
|
|
context: string,
|
|
kind: K,
|
|
message: string,
|
|
details?: unknown
|
|
): DomainError<K> {
|
|
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<K extends string> {
|
|
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<K> {
|
|
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'
|
|
});
|
|
});
|
|
});
|
|
});
|