@ufdevsllc/authme2.0
Version:
SDK for license management and remote monitoring with automatic system tracking, license validation, and remote control capabilities
341 lines (265 loc) • 12.4 kB
JavaScript
/**
* Tests for Error Handler
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import ErrorHandler from '../error-handler.js';
describe('ErrorHandler', () => {
let errorHandler;
beforeEach(() => {
errorHandler = new ErrorHandler();
// Mock console methods to avoid noise in tests
vi.spyOn(console, 'error').mockImplementation(() => { });
vi.spyOn(console, 'warn').mockImplementation(() => { });
vi.spyOn(console, 'log').mockImplementation(() => { });
});
afterEach(async () => {
await errorHandler.cleanup();
vi.restoreAllMocks();
});
describe('Error Handling', () => {
it('should handle basic errors', async () => {
const error = new Error('Test error');
const context = { component: 'test', operation: 'test-op' };
const result = await errorHandler.handleError(error, context);
expect(result).toMatchObject({
handled: true,
severity: 'error'
});
expect(result.errorId).toBeDefined();
expect(result.timestamp).toBeInstanceOf(Date);
});
it('should handle errors with different severity levels', async () => {
const error = new Error('Warning error');
const context = { component: 'test', operation: 'test-op' };
const result = await errorHandler.handleError(error, context, errorHandler.logLevels.WARN);
expect(result.severity).toBe('warn');
});
it('should sanitize sensitive context data', async () => {
const error = new Error('Test error');
const context = {
component: 'test',
operation: 'test-op',
password: 'secret123',
apiKey: 'key123',
normalData: 'visible'
};
const result = await errorHandler.handleError(error, context);
expect(result.handled).toBe(true);
// Sensitive data should be redacted in logs
});
});
describe('License Error Handling', () => {
it('should handle license validation errors', async () => {
const error = new Error('License not found');
const licenseKey = 'test-license-key';
const result = await errorHandler.handleLicenseError(error, licenseKey);
expect(result).toMatchObject({
errorType: 'LICENSE_NOT_FOUND',
canRetry: false
});
expect(result.userMessage).toContain('license key was not found');
expect(result.technicalMessage).toBe('License not found');
});
it('should handle expired license errors', async () => {
const error = new Error('License expired on 2023-01-01');
const licenseKey = 'test-license-key';
const result = await errorHandler.handleLicenseError(error, licenseKey);
expect(result.errorType).toBe('LICENSE_EXPIRED');
expect(result.userMessage).toContain('license has expired');
});
it('should handle distribution limit errors', async () => {
const error = new Error('Distribution limit exceeded');
const licenseKey = 'test-license-key';
const result = await errorHandler.handleLicenseError(error, licenseKey);
expect(result.errorType).toBe('DISTRIBUTION_LIMIT_EXCEEDED');
expect(result.userMessage).toContain('distribution limit has been reached');
});
it('should handle connection errors as retryable', async () => {
const error = new Error('Connection failed');
const licenseKey = 'test-license-key';
const result = await errorHandler.handleLicenseError(error, licenseKey);
expect(result.errorType).toBe('CONNECTION_ERROR');
expect(result.canRetry).toBe(true);
});
});
describe('Database Error Handling', () => {
it('should handle retryable database errors', async () => {
const error = new Error('Connection timeout');
error.name = 'MongoTimeoutError';
const operation = { type: 'find', collection: 'test' };
const result = await errorHandler.handleDatabaseError(error, operation, 1);
expect(result.isRetryable).toBe(true);
expect(result.shouldRetry).toBe(true);
expect(result.retryDelay).toBeGreaterThan(0);
});
it('should handle non-retryable database errors', async () => {
const error = new Error('Validation failed');
error.name = 'ValidationError';
const operation = { type: 'insert', collection: 'test' };
const result = await errorHandler.handleDatabaseError(error, operation, 1);
expect(result.isRetryable).toBe(false);
expect(result.shouldRetry).toBe(false);
});
it('should stop retrying after max attempts', async () => {
const error = new Error('Connection timeout');
error.name = 'MongoTimeoutError';
const operation = { type: 'find', collection: 'test' };
const result = await errorHandler.handleDatabaseError(error, operation, 3);
expect(result.shouldRetry).toBe(false);
expect(result.attemptNumber).toBe(3);
});
});
describe('Retry Logic', () => {
it('should execute operation successfully on first try', async () => {
const operation = vi.fn().mockResolvedValue('success');
const context = { component: 'test', operation: 'test-op' };
const result = await errorHandler.executeWithRetry(operation, context);
expect(result).toBe('success');
expect(operation).toHaveBeenCalledTimes(1);
});
it('should retry failed operations', async () => {
const operation = vi.fn()
.mockRejectedValueOnce(new Error('ECONNRESET'))
.mockResolvedValue('success');
const context = { component: 'test', operation: 'test-op' };
const result = await errorHandler.executeWithRetry(operation, context, { maxRetries: 2 });
expect(result).toBe('success');
expect(operation).toHaveBeenCalledTimes(2);
});
it('should fail after max retries', async () => {
const operation = vi.fn().mockRejectedValue(new Error('ECONNRESET'));
const context = { component: 'test', operation: 'test-op' };
await expect(
errorHandler.executeWithRetry(operation, context, { maxRetries: 2 })
).rejects.toThrow('ECONNRESET');
expect(operation).toHaveBeenCalledTimes(3); // Initial + 2 retries
});
it('should not retry non-retryable errors', async () => {
const operation = vi.fn().mockRejectedValue(new Error('Validation failed'));
const context = { component: 'test', operation: 'test-op' };
await expect(
errorHandler.executeWithRetry(operation, context, { maxRetries: 2 })
).rejects.toThrow('Validation failed');
expect(operation).toHaveBeenCalledTimes(1);
});
});
describe('Input Validation', () => {
it('should validate string inputs', () => {
const result = errorHandler.validateInput('test', {
type: 'string',
required: true,
minLength: 2,
maxLength: 10
});
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should fail validation for invalid string length', () => {
const result = errorHandler.validateInput('a', {
type: 'string',
required: true,
minLength: 2
});
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Minimum length is 2, got 1');
});
it('should validate required fields', () => {
const result = errorHandler.validateInput(null, {
type: 'string',
required: true
});
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Field is required');
});
it('should validate number ranges', () => {
const result = errorHandler.validateInput(5, {
type: 'number',
min: 1,
max: 10
});
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should fail validation for numbers out of range', () => {
const result = errorHandler.validateInput(15, {
type: 'number',
min: 1,
max: 10
});
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Maximum value is 10, got 15');
});
it('should detect potentially unsafe content', () => {
const result = errorHandler.validateInput('<script>alert("xss")</script>', {
type: 'string'
});
expect(result.warnings).toContain('Potentially unsafe content detected');
});
it('should sanitize data', () => {
const result = errorHandler.validateInput('<script>test</script>', {
type: 'string'
});
expect(result.sanitizedData).not.toContain('<script>');
});
});
describe('Circuit Breaker', () => {
it('should track error frequency', async () => {
const error = new Error('Test error');
const context = { component: 'test-component', operation: 'test-op' };
// Generate multiple errors to trigger circuit breaker
for (let i = 0; i < 12; i++) {
await errorHandler.handleError(error, context);
}
const stats = errorHandler.getErrorStatistics();
expect(stats.circuitBreakers['test-component']).toBeDefined();
expect(stats.circuitBreakers['test-component'].isOpen).toBe(true);
});
});
describe('Logging', () => {
it('should log info messages', async () => {
await errorHandler.logInfo('Test info message', {
component: 'test',
operation: 'test-op'
});
// Should not throw and should call console.log
expect(console.log).toHaveBeenCalled();
});
it('should log warning messages', async () => {
await errorHandler.logWarn('Test warning message', {
component: 'test',
operation: 'test-op'
});
expect(console.warn).toHaveBeenCalled();
});
it('should log debug messages', async () => {
await errorHandler.logDebug('Test debug message', {
component: 'test',
operation: 'test-op'
});
expect(console.log).toHaveBeenCalled();
});
});
describe('Error Statistics', () => {
it('should provide error statistics', async () => {
const error1 = new Error('Error 1');
const error2 = new Error('Error 2');
await errorHandler.handleError(error1, { component: 'comp1', operation: 'op1' });
await errorHandler.handleError(error2, { component: 'comp1', operation: 'op2' });
await errorHandler.handleError(error1, { component: 'comp2', operation: 'op1' });
const stats = errorHandler.getErrorStatistics();
expect(stats.totalErrors).toBe(3);
expect(stats.errorsByComponent.comp1).toBe(2);
expect(stats.errorsByComponent.comp2).toBe(1);
expect(stats.timestamp).toBeInstanceOf(Date);
});
});
describe('Cleanup', () => {
it('should cleanup resources', async () => {
// Add some errors to track
await errorHandler.handleError(new Error('Test'), { component: 'test' });
await errorHandler.cleanup();
const stats = errorHandler.getErrorStatistics();
expect(stats.totalErrors).toBe(0);
});
});
});