Files
gridpilot.gg/core/shared/errors/ApplicationError.test.ts
Marc Mintel 093eece3d7
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m47s
Contract Testing / contract-snapshot (pull_request) Has been skipped
core tests
2026-01-22 18:20:33 +01:00

472 lines
14 KiB
TypeScript

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);
});
});
});