275 lines
7.7 KiB
TypeScript
275 lines
7.7 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import * as errors from '../ts/errors/index.js';
|
|
import {
|
|
PlatformError,
|
|
ValidationError,
|
|
NetworkError,
|
|
ResourceError,
|
|
OperationError
|
|
} from '../ts/errors/base.errors.js';
|
|
import {
|
|
ErrorSeverity,
|
|
ErrorCategory,
|
|
ErrorRecoverability
|
|
} from '../ts/errors/error.codes.js';
|
|
import {
|
|
ErrorHandler
|
|
} from '../ts/errors/error-handler.js';
|
|
|
|
// Test base error classes
|
|
tap.test('Base error classes should set properties correctly', async () => {
|
|
const message = 'Test error message';
|
|
const code = 'TEST_ERROR_CODE';
|
|
const context = {
|
|
component: 'TestComponent',
|
|
operation: 'testOperation',
|
|
data: { foo: 'bar' }
|
|
};
|
|
|
|
// Test PlatformError
|
|
const platformError = new PlatformError(
|
|
message,
|
|
code,
|
|
ErrorSeverity.MEDIUM,
|
|
ErrorCategory.OPERATION,
|
|
ErrorRecoverability.MAYBE_RECOVERABLE,
|
|
context
|
|
);
|
|
|
|
expect(platformError.message).toEqual(message);
|
|
expect(platformError.code).toEqual(code);
|
|
expect(platformError.severity).toEqual(ErrorSeverity.MEDIUM);
|
|
expect(platformError.category).toEqual(ErrorCategory.OPERATION);
|
|
expect(platformError.recoverability).toEqual(ErrorRecoverability.MAYBE_RECOVERABLE);
|
|
expect(platformError.context?.component).toEqual(context.component);
|
|
expect(platformError.context?.operation).toEqual(context.operation);
|
|
expect(platformError.context?.data?.foo).toEqual('bar');
|
|
expect(platformError.name).toEqual('PlatformError');
|
|
|
|
// Test ValidationError
|
|
const validationError = new ValidationError(message, code, context);
|
|
expect(validationError.category).toEqual(ErrorCategory.VALIDATION);
|
|
expect(validationError.severity).toEqual(ErrorSeverity.LOW);
|
|
|
|
// Test NetworkError
|
|
const networkError = new NetworkError(message, code, context);
|
|
expect(networkError.category).toEqual(ErrorCategory.CONNECTIVITY);
|
|
expect(networkError.severity).toEqual(ErrorSeverity.MEDIUM);
|
|
expect(networkError.recoverability).toEqual(ErrorRecoverability.MAYBE_RECOVERABLE);
|
|
|
|
// Test ResourceError
|
|
const resourceError = new ResourceError(message, code, context);
|
|
expect(resourceError.category).toEqual(ErrorCategory.RESOURCE);
|
|
});
|
|
|
|
// Test error handler utility
|
|
tap.test('ErrorHandler should properly handle and format errors', async () => {
|
|
// Configure error handler
|
|
ErrorHandler.configure({
|
|
logErrors: false, // Disable for testing
|
|
includeStacksInProd: false,
|
|
retry: {
|
|
maxAttempts: 5,
|
|
baseDelay: 100,
|
|
maxDelay: 1000,
|
|
backoffFactor: 2
|
|
}
|
|
});
|
|
|
|
// Test converting regular Error to PlatformError
|
|
const regularError = new Error('Something went wrong');
|
|
const platformError = ErrorHandler.toPlatformError(
|
|
regularError,
|
|
'PLATFORM_OPERATION_ERROR',
|
|
{ component: 'TestHandler' }
|
|
);
|
|
|
|
expect(platformError).toBeInstanceOf(PlatformError);
|
|
expect(platformError.code).toEqual('PLATFORM_OPERATION_ERROR');
|
|
expect(platformError.context?.component).toEqual('TestHandler');
|
|
|
|
// Test formatting error for API response
|
|
const formattedError = ErrorHandler.formatErrorForResponse(platformError, true);
|
|
expect(formattedError.code).toEqual('PLATFORM_OPERATION_ERROR');
|
|
expect(formattedError.message).toEqual('An unexpected error occurred.');
|
|
expect(formattedError.details?.rawMessage).toEqual('Something went wrong');
|
|
|
|
// Test executing a function with error handling
|
|
let executed = false;
|
|
try {
|
|
await ErrorHandler.execute(async () => {
|
|
executed = true;
|
|
throw new Error('Execution failed');
|
|
}, 'TEST_EXECUTION_ERROR', { operation: 'testExecution' });
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(PlatformError);
|
|
expect(error.code).toEqual('TEST_EXECUTION_ERROR');
|
|
expect(error.context.operation).toEqual('testExecution');
|
|
}
|
|
expect(executed).toEqual(true);
|
|
|
|
// Test executeWithRetry successful after retries
|
|
let attempts = 0;
|
|
const result = await ErrorHandler.executeWithRetry(
|
|
async () => {
|
|
attempts++;
|
|
if (attempts < 3) {
|
|
throw new Error('Temporary failure');
|
|
}
|
|
return 'success';
|
|
},
|
|
'TEST_RETRY_ERROR',
|
|
{
|
|
maxAttempts: 5,
|
|
baseDelay: 10, // Use small delay for tests
|
|
retryableErrorPatterns: [/Temporary failure/], // Add pattern to make error retryable
|
|
onRetry: (error, attempt, delay) => {
|
|
expect(error).toBeInstanceOf(PlatformError);
|
|
expect(attempt).toBeGreaterThan(0);
|
|
expect(delay).toBeGreaterThan(0);
|
|
}
|
|
}
|
|
);
|
|
|
|
expect(result).toEqual('success');
|
|
expect(attempts).toEqual(3);
|
|
|
|
// Test executeWithRetry that fails after max attempts
|
|
attempts = 0;
|
|
try {
|
|
await ErrorHandler.executeWithRetry(
|
|
async () => {
|
|
attempts++;
|
|
throw new Error('Persistent failure');
|
|
},
|
|
'TEST_RETRY_ERROR',
|
|
{
|
|
maxAttempts: 3,
|
|
baseDelay: 10,
|
|
retryableErrorPatterns: [/Persistent failure/] // Make error retryable so it tries all attempts
|
|
}
|
|
);
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(PlatformError);
|
|
expect(attempts).toEqual(3);
|
|
}
|
|
});
|
|
|
|
// Test retry utilities
|
|
tap.test('Error retry utilities should work correctly', async () => {
|
|
let attempts = 0;
|
|
|
|
try {
|
|
await errors.retry(
|
|
async () => {
|
|
attempts++;
|
|
if (attempts < 3) {
|
|
throw new Error('Temporary error');
|
|
}
|
|
return 'success';
|
|
},
|
|
{
|
|
maxRetries: 5,
|
|
initialDelay: 20,
|
|
backoffFactor: 1.5,
|
|
retryableErrors: [/Temporary/]
|
|
}
|
|
);
|
|
} catch (e) {
|
|
// Should not reach here
|
|
expect(false).toEqual(true);
|
|
}
|
|
|
|
expect(attempts).toEqual(3);
|
|
|
|
// Test retry with non-retryable error
|
|
attempts = 0;
|
|
try {
|
|
await errors.retry(
|
|
async () => {
|
|
attempts++;
|
|
throw new Error('Critical error');
|
|
},
|
|
{
|
|
maxRetries: 3,
|
|
initialDelay: 10,
|
|
retryableErrors: [/Temporary/] // Won't match "Critical"
|
|
}
|
|
);
|
|
} catch (error) {
|
|
expect(error.message).toEqual('Critical error');
|
|
expect(attempts).toEqual(1); // Should only attempt once
|
|
}
|
|
});
|
|
|
|
// Helper function that will reject first n times, then resolve
|
|
interface FlakyFunction {
|
|
(failTimes: number, result?: any): Promise<any>;
|
|
counter: number;
|
|
reset: () => void;
|
|
}
|
|
|
|
const flaky: FlakyFunction = Object.assign(
|
|
async function (failTimes: number, result: any = 'success'): Promise<any> {
|
|
if (flaky.counter < failTimes) {
|
|
flaky.counter++;
|
|
throw new Error(`Flaky failure ${flaky.counter}`);
|
|
}
|
|
return result;
|
|
},
|
|
{
|
|
counter: 0,
|
|
reset: () => { flaky.counter = 0; }
|
|
}
|
|
);
|
|
|
|
// Test error wrapping and retry combination
|
|
tap.test('Error handling can be combined with retry for robust operations', async () => {
|
|
// Reset counter for the test
|
|
flaky.reset();
|
|
|
|
// Create a wrapped version of the flaky function
|
|
const wrapped = errors.withErrorHandling(
|
|
() => flaky(2, 'wrapped success'),
|
|
'TEST_WRAPPED_ERROR',
|
|
{ component: 'TestComponent' }
|
|
);
|
|
|
|
// Execute with retry
|
|
const result = await errors.retry(
|
|
wrapped,
|
|
{
|
|
maxRetries: 3,
|
|
initialDelay: 10,
|
|
retryableErrors: [/Flaky failure/]
|
|
}
|
|
);
|
|
expect(result).toEqual('wrapped success');
|
|
expect(flaky.counter).toEqual(2);
|
|
|
|
// Reset and test failure case
|
|
flaky.reset();
|
|
|
|
try {
|
|
await errors.retry(
|
|
() => flaky(5, 'never reached'),
|
|
{
|
|
maxRetries: 2, // Only retry twice, but we need 5 attempts to succeed
|
|
initialDelay: 10,
|
|
retryableErrors: [/Flaky failure/] // Add pattern to make it retry
|
|
}
|
|
);
|
|
// Should not reach here
|
|
expect(false).toEqual(true);
|
|
} catch (error) {
|
|
expect(error.message).toContain('Flaky failure');
|
|
expect(flaky.counter).toEqual(3); // Initial + 2 retries = 3 attempts
|
|
}
|
|
});
|
|
|
|
tap.test('stop', async () => {
|
|
await tap.stopForcefully();
|
|
});
|
|
|
|
export default tap.start();
|