ErrorTestUtils.ts•11.1 kB
import { ClientConnectionError, ClientNotFoundError, MCPError, ValidationError } from '@src/utils/core/errorTypes.js';
import { expect, vi } from 'vitest';
/**
* Utilities for testing error handling and error scenarios
*/
export class ErrorTestUtils {
/**
* Test that a function throws a specific error type
*/
static expectThrows<T extends Error>(
fn: () => any,
errorType: new (...args: any[]) => T,
message?: string | RegExp,
): void {
let thrownError: Error | undefined;
try {
fn();
} catch (_error) {
thrownError = _error as Error;
}
expect(thrownError).toBeDefined();
expect(thrownError).toBeInstanceOf(errorType);
if (message) {
if (typeof message === 'string') {
expect(thrownError!.message).toContain(message);
} else {
expect(thrownError!.message).toMatch(message);
}
}
}
/**
* Test that an async function throws a specific error type
*/
static async expectAsyncThrows<T extends Error>(
fn: () => Promise<any>,
errorType: new (...args: any[]) => T,
message?: string | RegExp,
): Promise<void> {
let thrownError: Error | undefined;
try {
await fn();
} catch (_error) {
thrownError = _error as Error;
}
expect(thrownError).toBeDefined();
expect(thrownError).toBeInstanceOf(errorType);
if (message) {
if (typeof message === 'string') {
expect(thrownError!.message).toContain(message);
} else {
expect(thrownError!.message).toMatch(message);
}
}
}
/**
* Test that a function does not throw any error
*/
static expectDoesNotThrow(fn: () => any): void {
expect(fn).not.toThrow();
}
/**
* Test that an async function does not throw any error
*/
static async expectAsyncDoesNotThrow(fn: () => Promise<any>): Promise<void> {
await expect(fn()).resolves.not.toThrow();
}
/**
* Create a mock error with specific properties
*/
static createMockError(message: string, code?: string, details?: any): Error {
const error = new Error(message);
if (code) {
(error as any).code = code;
}
if (details) {
(error as any).details = details;
}
return error;
}
/**
* Create a mock MCP error
*/
static createMockMCPError(message: string, code: number = -1, details?: any): MCPError {
return new MCPError(message, code, details);
}
/**
* Create a mock client connection error
*/
static createMockClientConnectionError(
clientName: string,
reason: string = 'Connection failed',
): ClientConnectionError {
return new ClientConnectionError(clientName, new Error(reason));
}
/**
* Create a mock client not found error
*/
static createMockClientNotFoundError(clientName: string): ClientNotFoundError {
return new ClientNotFoundError(clientName);
}
/**
* Create a mock validation error
*/
static createMockValidationError(message: string, field?: string): ValidationError {
return new ValidationError(message, field);
}
/**
* Create a function that throws an error after a delay
*/
static createDelayedErrorFunction(delay: number, error: Error): () => Promise<never> {
return async () => {
await new Promise((resolve) => setTimeout(resolve, delay));
throw error;
};
}
/**
* Create a function that throws an error on the nth call
*/
static createNthCallErrorFunction(n: number, errorToThrow: Error, returnValue?: any): () => any {
let callCount = 0;
return () => {
callCount++;
if (callCount === n) {
throw errorToThrow;
}
return returnValue;
};
}
/**
* Create a function that throws an error intermittently
*/
static createIntermittentErrorFunction(errorProbability: number, errorToThrow: Error, returnValue?: any): () => any {
return () => {
if (Math.random() < errorProbability) {
throw errorToThrow;
}
return returnValue;
};
}
/**
* Test error propagation through a chain of functions
*/
static async testErrorPropagation(
functions: Array<(arg: any) => Promise<any>>,
initialValue: any,
expectedError: Error,
): Promise<void> {
let currentValue = initialValue;
let caughtError: Error | undefined;
try {
for (const fn of functions) {
currentValue = await fn(currentValue);
}
} catch (_error) {
caughtError = _error as Error;
}
expect(caughtError).toBeDefined();
expect(caughtError).toBeInstanceOf(expectedError.constructor);
expect(caughtError!.message).toBe(expectedError.message);
}
/**
* Test error handling with retry logic
*/
static async testErrorRetry(
fn: () => Promise<any>,
maxRetries: number,
expectedAttempts: number,
shouldSucceed: boolean = false,
): Promise<void> {
let attempts = 0;
let lastError: Error | undefined;
const wrappedFn = async () => {
attempts++;
return await fn();
};
try {
for (let i = 0; i < maxRetries; i++) {
try {
await wrappedFn();
if (shouldSucceed) {
break;
}
} catch (_error) {
lastError = _error as Error;
if (i === maxRetries - 1) {
throw _error;
}
}
}
} catch (_error) {
// Expected if shouldSucceed is false
}
expect(attempts).toBe(expectedAttempts);
if (!shouldSucceed) {
expect(lastError).toBeDefined();
}
}
/**
* Test error handling with circuit breaker pattern
*/
static testCircuitBreakerErrorHandling(
fn: () => Promise<any>,
errorThreshold: number,
timeoutMs: number,
): {
execute: () => Promise<any>;
getState: () => 'closed' | 'open' | 'half-open';
getFailureCount: () => number;
reset: () => void;
} {
let failureCount = 0;
let state: 'closed' | 'open' | 'half-open' = 'closed';
let lastFailureTime = 0;
const execute = async () => {
if (state === 'open') {
if (Date.now() - lastFailureTime > timeoutMs) {
state = 'half-open';
} else {
throw new Error('Circuit breaker is open');
}
}
try {
const result = await fn();
if (state === 'half-open') {
state = 'closed';
failureCount = 0;
}
return result;
} catch (_error) {
failureCount++;
lastFailureTime = Date.now();
if (failureCount >= errorThreshold) {
state = 'open';
}
throw _error;
}
};
return {
execute,
getState: () => state,
getFailureCount: () => failureCount,
reset: () => {
failureCount = 0;
state = 'closed';
lastFailureTime = 0;
},
};
}
/**
* Test error logging and reporting
*/
static testErrorLogging(
fn: () => any,
mockLogger: any,
expectedLogLevel: 'error' | 'warn' | 'info' | 'debug' = 'error',
): void {
let caughtError: Error | undefined;
try {
fn();
} catch (_error) {
caughtError = _error as Error;
}
expect(caughtError).toBeDefined();
expect(mockLogger[expectedLogLevel]).toHaveBeenCalledWith(expect.stringContaining(caughtError!.message));
}
/**
* Test async error logging and reporting
*/
static async testAsyncErrorLogging(
fn: () => Promise<any>,
mockLogger: any,
expectedLogLevel: 'error' | 'warn' | 'info' | 'debug' = 'error',
): Promise<void> {
let caughtError: Error | undefined;
try {
await fn();
} catch (_error) {
caughtError = _error as Error;
}
expect(caughtError).toBeDefined();
expect(mockLogger[expectedLogLevel]).toHaveBeenCalledWith(expect.stringContaining(caughtError!.message));
}
/**
* Mock console.error to capture error outputs
*/
static mockConsoleError(): {
mock: ReturnType<typeof vi.fn>;
restore: () => void;
getErrorMessages: () => string[];
} {
const originalConsoleError = console.error;
const mockConsoleError = vi.fn();
console.error = mockConsoleError;
return {
mock: mockConsoleError,
restore: () => {
console.error = originalConsoleError;
},
getErrorMessages: () => {
return mockConsoleError.mock.calls.map((call) => call.join(' '));
},
};
}
/**
* Test error boundary behavior
*/
static testErrorBoundary(
fn: () => any,
errorBoundary: (error: Error) => any,
expectedHandling: 'catch' | 'rethrow' | 'transform',
): void {
let originalError: Error | undefined;
let boundaryError: Error | undefined;
let result: any;
try {
fn();
} catch (_error) {
originalError = _error as Error;
try {
result = errorBoundary(originalError);
} catch (handledError) {
boundaryError = handledError as Error;
}
}
expect(originalError).toBeDefined();
switch (expectedHandling) {
case 'catch':
expect(boundaryError).toBeUndefined();
expect(result).toBeDefined();
break;
case 'rethrow':
expect(boundaryError).toBeDefined();
expect(boundaryError).toBe(originalError);
break;
case 'transform':
expect(boundaryError).toBeDefined();
expect(boundaryError).not.toBe(originalError);
break;
}
}
/**
* Common error scenarios for testing
*/
static readonly ERROR_SCENARIOS = {
NETWORK_ERROR: () => ErrorTestUtils.createMockError('Network error', 'NETWORK_ERROR'),
TIMEOUT_ERROR: () => ErrorTestUtils.createMockError('Timeout error', 'TIMEOUT_ERROR'),
PERMISSION_ERROR: () => ErrorTestUtils.createMockError('Permission denied', 'PERMISSION_ERROR'),
FILE_NOT_FOUND: () => ErrorTestUtils.createMockError('File not found', 'ENOENT'),
INVALID_JSON: () => ErrorTestUtils.createMockError('Invalid JSON', 'JSON_PARSE_ERROR'),
DATABASE_ERROR: () => ErrorTestUtils.createMockError('Database error', 'DATABASE_ERROR'),
VALIDATION_ERROR: () => ErrorTestUtils.createMockValidationError('Validation failed', 'field'),
AUTHENTICATION_ERROR: () => ErrorTestUtils.createMockError('Authentication failed', 'INVALID_TOKEN'),
CONFIGURATION_ERROR: () => ErrorTestUtils.createMockError('Configuration error', 'missing_field'),
};
/**
* Test error message formatting
*/
static testErrorMessageFormatting(error: Error, expectedFormat: RegExp): void {
expect(error.message).toMatch(expectedFormat);
}
/**
* Test error stack trace presence
*/
static testErrorStackTrace(error: Error): void {
expect(error.stack).toBeDefined();
expect(error.stack).toContain(error.message);
}
/**
* Test error serialization
*/
static testErrorSerialization(error: Error): void {
const serialized = JSON.stringify(error);
const deserialized = JSON.parse(serialized);
expect(deserialized.message).toBe(error.message);
expect(deserialized.name).toBe(error.name);
}
}