import { tap, expect } from '@push.rocks/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 { EmailServiceError, EmailTemplateError, EmailValidationError, EmailSendError, EmailReceiveError } from '../ts/errors/email.errors.js'; import { MtaConnectionError, MtaAuthenticationError, MtaDeliveryError, MtaConfigurationError } from '../ts/errors/mta.errors.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 email error classes tap.test('Email error classes should be properly constructed', async () => { // Test EmailServiceError const emailServiceError = new EmailServiceError('Email service error', { component: 'EmailService', operation: 'sendEmail' }); expect(emailServiceError.code).toEqual('EMAIL_SERVICE_ERROR'); expect(emailServiceError.name).toEqual('EmailServiceError'); // Test EmailTemplateError const templateError = new EmailTemplateError('Template not found: welcome_email', { data: { templateId: 'welcome_email' } }); expect(templateError.code).toEqual('EMAIL_TEMPLATE_ERROR'); expect(templateError.context.data.templateId).toEqual('welcome_email'); // Test EmailSendError with permanent flag const permanentError = EmailSendError.permanent( 'Invalid recipient', 'user@example.com', { data: { details: 'DNS not found' } } ); expect(permanentError.code).toEqual('EMAIL_SEND_ERROR'); expect(permanentError.isPermanent()).toEqual(true); expect(permanentError.context.data.permanent).toEqual(true); // Test EmailSendError with temporary flag and retry const tempError = EmailSendError.temporary( 'Server busy', 3, 0, 1000, { data: { server: 'smtp.example.com' } } ); expect(tempError.isPermanent()).toEqual(false); expect(tempError.context.data.permanent).toEqual(false); expect(tempError.context.retry.maxRetries).toEqual(3); expect(tempError.shouldRetry()).toEqual(true); }); // Test MTA error classes tap.test('MTA error classes should be properly constructed', async () => { // Test MtaConnectionError const dnsError = MtaConnectionError.dnsError('mail.example.com', new Error('DNS lookup failed')); expect(dnsError.code).toEqual('MTA_CONNECTION_ERROR'); expect(dnsError.category).toEqual(ErrorCategory.CONNECTIVITY); expect(dnsError.context.data.hostname).toEqual('mail.example.com'); // Test MtaTimeoutError via MtaConnectionError.timeout const timeoutError = MtaConnectionError.timeout('mail.example.com', 25, 30000); expect(timeoutError.code).toEqual('MTA_CONNECTION_ERROR'); expect(timeoutError.context.data.timeout).toEqual(30000); // Test MtaAuthenticationError const authError = MtaAuthenticationError.invalidCredentials('mail.example.com', 'user@example.com'); expect(authError.code).toEqual('MTA_AUTHENTICATION_ERROR'); expect(authError.category).toEqual(ErrorCategory.AUTHENTICATION); expect(authError.context.data.username).toEqual('user@example.com'); // Test MtaDeliveryError const permDeliveryError = MtaDeliveryError.permanent( 'User unknown', 'nonexistent@example.com', '550', '550 5.1.1 User unknown' ); expect(permDeliveryError.code).toEqual('MTA_DELIVERY_ERROR'); expect(permDeliveryError.isPermanent()).toEqual(true); expect(permDeliveryError.getRecipientAddress()).toEqual('nonexistent@example.com'); expect(permDeliveryError.getStatusCode()).toEqual('550'); // Test temporary delivery error with retry const tempDeliveryError = MtaDeliveryError.temporary( 'Mailbox temporarily unavailable', 'user@example.com', '450', '450 4.2.1 Mailbox temporarily unavailable', 3, 1, 5000 ); expect(tempDeliveryError.isPermanent()).toEqual(false); expect(tempDeliveryError.shouldRetry()).toEqual(true); expect(tempDeliveryError.context.retry.currentRetry).toEqual(1); expect(tempDeliveryError.context.retry.maxRetries).toEqual(3); }); // 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 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 } ); } 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; const start = Date.now(); 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 async function flaky(failTimes: number, result: any = 'success'): Promise { if (flaky.counter < failTimes) { flaky.counter++; throw new Error(`Flaky failure ${flaky.counter}`); } return result; } flaky.counter = 0; flaky.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 try { const result = await errors.retry( wrapped, { maxRetries: 3, initialDelay: 10, } ); expect(result).toEqual('wrapped success'); expect(flaky.counter).toEqual(2); } catch (error) { // Should not reach here expect(false).toEqual(true); } // 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, } ); // 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 () => { // This is a placeholder test to ensure we call tap.stopForcefully() }); export default tap.stopForcefully();