core tests
This commit is contained in:
471
core/shared/errors/ApplicationError.test.ts
Normal file
471
core/shared/errors/ApplicationError.test.ts
Normal file
@@ -0,0 +1,471 @@
|
||||
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<K extends string>(
|
||||
context: string,
|
||||
kind: K,
|
||||
message: string,
|
||||
details?: unknown
|
||||
): ApplicationError<K> {
|
||||
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<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(): ApplicationError<K> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
335
core/shared/errors/ApplicationErrorCode.test.ts
Normal file
335
core/shared/errors/ApplicationErrorCode.test.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ApplicationErrorCode } from './ApplicationErrorCode';
|
||||
|
||||
describe('ApplicationErrorCode', () => {
|
||||
describe('ApplicationErrorCode type', () => {
|
||||
it('should create error code with code only', () => {
|
||||
const errorCode: ApplicationErrorCode<'USER_NOT_FOUND'> = {
|
||||
code: 'USER_NOT_FOUND'
|
||||
};
|
||||
|
||||
expect(errorCode.code).toBe('USER_NOT_FOUND');
|
||||
});
|
||||
|
||||
it('should create error code with code and details', () => {
|
||||
const errorCode: ApplicationErrorCode<'INSUFFICIENT_FUNDS', { balance: number; required: number }> = {
|
||||
code: 'INSUFFICIENT_FUNDS',
|
||||
details: { balance: 50, required: 100 }
|
||||
};
|
||||
|
||||
expect(errorCode.code).toBe('INSUFFICIENT_FUNDS');
|
||||
expect(errorCode.details).toEqual({ balance: 50, required: 100 });
|
||||
});
|
||||
|
||||
it('should support different error code types', () => {
|
||||
const notFoundCode: ApplicationErrorCode<'USER_NOT_FOUND'> = {
|
||||
code: 'USER_NOT_FOUND'
|
||||
};
|
||||
|
||||
const validationCode: ApplicationErrorCode<'VALIDATION_ERROR', { field: string }> = {
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: { field: 'email' }
|
||||
};
|
||||
|
||||
const permissionCode: ApplicationErrorCode<'PERMISSION_DENIED', { resource: string }> = {
|
||||
code: 'PERMISSION_DENIED',
|
||||
details: { resource: 'admin-panel' }
|
||||
};
|
||||
|
||||
expect(notFoundCode.code).toBe('USER_NOT_FOUND');
|
||||
expect(validationCode.code).toBe('VALIDATION_ERROR');
|
||||
expect(validationCode.details).toEqual({ field: 'email' });
|
||||
expect(permissionCode.code).toBe('PERMISSION_DENIED');
|
||||
expect(permissionCode.details).toEqual({ resource: 'admin-panel' });
|
||||
});
|
||||
|
||||
it('should support complex details types', () => {
|
||||
interface PaymentErrorDetails {
|
||||
amount: number;
|
||||
currency: string;
|
||||
retryAfter?: number;
|
||||
attempts: number;
|
||||
}
|
||||
|
||||
const paymentErrorCode: ApplicationErrorCode<'PAYMENT_FAILED', PaymentErrorDetails> = {
|
||||
code: 'PAYMENT_FAILED',
|
||||
details: {
|
||||
amount: 100,
|
||||
currency: 'USD',
|
||||
retryAfter: 60,
|
||||
attempts: 3
|
||||
}
|
||||
};
|
||||
|
||||
expect(paymentErrorCode.code).toBe('PAYMENT_FAILED');
|
||||
expect(paymentErrorCode.details).toEqual({
|
||||
amount: 100,
|
||||
currency: 'USD',
|
||||
retryAfter: 60,
|
||||
attempts: 3
|
||||
});
|
||||
});
|
||||
|
||||
it('should support optional details', () => {
|
||||
const errorCodeWithDetails: ApplicationErrorCode<'ERROR', { message: string }> = {
|
||||
code: 'ERROR',
|
||||
details: { message: 'Something went wrong' }
|
||||
};
|
||||
|
||||
const errorCodeWithoutDetails: ApplicationErrorCode<'ERROR', undefined> = {
|
||||
code: 'ERROR'
|
||||
};
|
||||
|
||||
expect(errorCodeWithDetails.code).toBe('ERROR');
|
||||
expect(errorCodeWithDetails.details).toEqual({ message: 'Something went wrong' });
|
||||
expect(errorCodeWithoutDetails.code).toBe('ERROR');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ApplicationErrorCode behavior', () => {
|
||||
it('should be assignable to Result error type', () => {
|
||||
// ApplicationErrorCode is designed to be used with Result type
|
||||
// This test verifies the type compatibility
|
||||
type MyErrorCodes = 'USER_NOT_FOUND' | 'VALIDATION_ERROR' | 'PERMISSION_DENIED';
|
||||
|
||||
const userNotFound: ApplicationErrorCode<'USER_NOT_FOUND'> = {
|
||||
code: 'USER_NOT_FOUND'
|
||||
};
|
||||
|
||||
const validationError: ApplicationErrorCode<'VALIDATION_ERROR', { field: string }> = {
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: { field: 'email' }
|
||||
};
|
||||
|
||||
const permissionError: ApplicationErrorCode<'PERMISSION_DENIED', { resource: string }> = {
|
||||
code: 'PERMISSION_DENIED',
|
||||
details: { resource: 'admin-panel' }
|
||||
};
|
||||
|
||||
expect(userNotFound.code).toBe('USER_NOT_FOUND');
|
||||
expect(validationError.code).toBe('VALIDATION_ERROR');
|
||||
expect(validationError.details).toEqual({ field: 'email' });
|
||||
expect(permissionError.code).toBe('PERMISSION_DENIED');
|
||||
expect(permissionError.details).toEqual({ resource: 'admin-panel' });
|
||||
});
|
||||
|
||||
it('should support error code patterns', () => {
|
||||
// Common error code patterns
|
||||
const notFoundPattern: ApplicationErrorCode<'NOT_FOUND', { resource: string; id?: string }> = {
|
||||
code: 'NOT_FOUND',
|
||||
details: { resource: 'user', id: '123' }
|
||||
};
|
||||
|
||||
const conflictPattern: ApplicationErrorCode<'CONFLICT', { resource: string; existingId: string }> = {
|
||||
code: 'CONFLICT',
|
||||
details: { resource: 'order', existingId: '456' }
|
||||
};
|
||||
|
||||
const validationPattern: ApplicationErrorCode<'VALIDATION_ERROR', { field: string; value: unknown; reason: string }> = {
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: { field: 'email', value: 'invalid', reason: 'must contain @' }
|
||||
};
|
||||
|
||||
expect(notFoundPattern.code).toBe('NOT_FOUND');
|
||||
expect(notFoundPattern.details).toEqual({ resource: 'user', id: '123' });
|
||||
expect(conflictPattern.code).toBe('CONFLICT');
|
||||
expect(conflictPattern.details).toEqual({ resource: 'order', existingId: '456' });
|
||||
expect(validationPattern.code).toBe('VALIDATION_ERROR');
|
||||
expect(validationPattern.details).toEqual({ field: 'email', value: 'invalid', reason: 'must contain @' });
|
||||
});
|
||||
|
||||
it('should support error code with metadata', () => {
|
||||
interface ErrorMetadata {
|
||||
timestamp: string;
|
||||
requestId?: string;
|
||||
userId?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
const errorCode: ApplicationErrorCode<'AUTH_ERROR', ErrorMetadata> = {
|
||||
code: 'AUTH_ERROR',
|
||||
details: {
|
||||
timestamp: new Date().toISOString(),
|
||||
requestId: 'req-123',
|
||||
userId: 'user-456',
|
||||
sessionId: 'session-789'
|
||||
}
|
||||
};
|
||||
|
||||
expect(errorCode.code).toBe('AUTH_ERROR');
|
||||
expect(errorCode.details).toBeDefined();
|
||||
expect(errorCode.details?.timestamp).toBeDefined();
|
||||
expect(errorCode.details?.requestId).toBe('req-123');
|
||||
});
|
||||
|
||||
it('should support error code with retry information', () => {
|
||||
interface RetryInfo {
|
||||
retryAfter: number;
|
||||
maxRetries: number;
|
||||
currentAttempt: number;
|
||||
}
|
||||
|
||||
const retryableError: ApplicationErrorCode<'RATE_LIMIT_EXCEEDED', RetryInfo> = {
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
details: {
|
||||
retryAfter: 60,
|
||||
maxRetries: 3,
|
||||
currentAttempt: 1
|
||||
}
|
||||
};
|
||||
|
||||
expect(retryableError.code).toBe('RATE_LIMIT_EXCEEDED');
|
||||
expect(retryableError.details).toEqual({
|
||||
retryAfter: 60,
|
||||
maxRetries: 3,
|
||||
currentAttempt: 1
|
||||
});
|
||||
});
|
||||
|
||||
it('should support error code with validation details', () => {
|
||||
interface ValidationErrorDetails {
|
||||
field: string;
|
||||
value: unknown;
|
||||
constraints: string[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
const validationError: ApplicationErrorCode<'VALIDATION_ERROR', ValidationErrorDetails> = {
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: {
|
||||
field: 'email',
|
||||
value: 'invalid-email',
|
||||
constraints: ['must be a valid email', 'must not be empty'],
|
||||
message: 'Email validation failed'
|
||||
}
|
||||
};
|
||||
|
||||
expect(validationError.code).toBe('VALIDATION_ERROR');
|
||||
expect(validationError.details).toEqual({
|
||||
field: 'email',
|
||||
value: 'invalid-email',
|
||||
constraints: ['must be a valid email', 'must not be empty'],
|
||||
message: 'Email validation failed'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ApplicationErrorCode implementation patterns', () => {
|
||||
it('should support error code factory pattern', () => {
|
||||
function createErrorCode<Code extends string, Details = unknown>(
|
||||
code: Code,
|
||||
details?: Details
|
||||
): ApplicationErrorCode<Code, Details> {
|
||||
return details ? { code, details } : { code };
|
||||
}
|
||||
|
||||
const notFound = createErrorCode('USER_NOT_FOUND');
|
||||
const validation = createErrorCode('VALIDATION_ERROR', { field: 'email' });
|
||||
const permission = createErrorCode('PERMISSION_DENIED', { resource: 'admin' });
|
||||
|
||||
expect(notFound.code).toBe('USER_NOT_FOUND');
|
||||
expect(validation.code).toBe('VALIDATION_ERROR');
|
||||
expect(validation.details).toEqual({ field: 'email' });
|
||||
expect(permission.code).toBe('PERMISSION_DENIED');
|
||||
expect(permission.details).toEqual({ resource: 'admin' });
|
||||
});
|
||||
|
||||
it('should support error code builder pattern', () => {
|
||||
class ErrorCodeBuilder<Code extends string, Details = unknown> {
|
||||
private code: Code = '' as Code;
|
||||
private details?: Details;
|
||||
|
||||
withCode(code: Code): this {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
|
||||
withDetails(details: Details): this {
|
||||
this.details = details;
|
||||
return this;
|
||||
}
|
||||
|
||||
build(): ApplicationErrorCode<Code, Details> {
|
||||
return this.details ? { code: this.code, details: this.details } : { code: this.code };
|
||||
}
|
||||
}
|
||||
|
||||
const errorCode = new ErrorCodeBuilder<'USER_NOT_FOUND'>()
|
||||
.withCode('USER_NOT_FOUND')
|
||||
.build();
|
||||
|
||||
const errorCodeWithDetails = new ErrorCodeBuilder<'VALIDATION_ERROR', { field: string }>()
|
||||
.withCode('VALIDATION_ERROR')
|
||||
.withDetails({ field: 'email' })
|
||||
.build();
|
||||
|
||||
expect(errorCode.code).toBe('USER_NOT_FOUND');
|
||||
expect(errorCodeWithDetails.code).toBe('VALIDATION_ERROR');
|
||||
expect(errorCodeWithDetails.details).toEqual({ field: 'email' });
|
||||
});
|
||||
|
||||
it('should support error code categorization', () => {
|
||||
const errorCodes: ApplicationErrorCode<string, unknown>[] = [
|
||||
{ code: 'USER_NOT_FOUND' },
|
||||
{ code: 'VALIDATION_ERROR', details: { field: 'email' } },
|
||||
{ code: 'PERMISSION_DENIED', details: { resource: 'admin' } },
|
||||
{ code: 'NETWORK_ERROR' }
|
||||
];
|
||||
|
||||
const notFoundCodes = errorCodes.filter(e => e.code === 'USER_NOT_FOUND');
|
||||
const validationCodes = errorCodes.filter(e => e.code === 'VALIDATION_ERROR');
|
||||
const permissionCodes = errorCodes.filter(e => e.code === 'PERMISSION_DENIED');
|
||||
const networkCodes = errorCodes.filter(e => e.code === 'NETWORK_ERROR');
|
||||
|
||||
expect(notFoundCodes).toHaveLength(1);
|
||||
expect(validationCodes).toHaveLength(1);
|
||||
expect(permissionCodes).toHaveLength(1);
|
||||
expect(networkCodes).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should support error code with complex details', () => {
|
||||
interface ComplexErrorDetails {
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
};
|
||||
context: {
|
||||
service: string;
|
||||
operation: string;
|
||||
timestamp: string;
|
||||
};
|
||||
metadata: {
|
||||
retryCount: number;
|
||||
timeout: number;
|
||||
};
|
||||
}
|
||||
|
||||
const complexError: ApplicationErrorCode<'SYSTEM_ERROR', ComplexErrorDetails> = {
|
||||
code: 'SYSTEM_ERROR',
|
||||
details: {
|
||||
error: {
|
||||
code: 'E001',
|
||||
message: 'System failure',
|
||||
stack: 'Error stack trace...'
|
||||
},
|
||||
context: {
|
||||
service: 'payment-service',
|
||||
operation: 'processPayment',
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
metadata: {
|
||||
retryCount: 3,
|
||||
timeout: 5000
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expect(complexError.code).toBe('SYSTEM_ERROR');
|
||||
expect(complexError.details).toBeDefined();
|
||||
expect(complexError.details?.error.code).toBe('E001');
|
||||
expect(complexError.details?.context.service).toBe('payment-service');
|
||||
expect(complexError.details?.metadata.retryCount).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
508
core/shared/errors/DomainError.test.ts
Normal file
508
core/shared/errors/DomainError.test.ts
Normal file
@@ -0,0 +1,508 @@
|
||||
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'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user