create-roadkit
Version:
Beautiful Next.js roadmap website generator with full-screen kanban boards, dark/light mode, and static export
371 lines (313 loc) • 11.4 kB
text/typescript
/**
* Unit Tests for Logger and ErrorRecovery
*
* Tests the logging framework and error recovery mechanisms
*/
import { test, expect, describe, beforeEach, afterEach } from 'bun:test';
import { join } from 'path';
import { existsSync } from 'fs';
import { mkdir, rm } from 'fs/promises';
import { Logger, LogLevel, ErrorRecovery, createLogger } from '../utils/logger.js';
const TEST_LOG_DIR = join(__dirname, '../../test-logs');
describe('Logger', () => {
let logger: Logger;
beforeEach(async () => {
if (existsSync(TEST_LOG_DIR)) {
await rm(TEST_LOG_DIR, { recursive: true, force: true });
}
await mkdir(TEST_LOG_DIR, { recursive: true });
logger = new Logger({
enableConsole: false, // Disable console for testing
enableFile: true,
logDir: TEST_LOG_DIR,
level: LogLevel.DEBUG,
verbose: true
});
});
afterEach(async () => {
await logger.flush();
if (existsSync(TEST_LOG_DIR)) {
await rm(TEST_LOG_DIR, { recursive: true, force: true });
}
});
test('should create logger with default configuration', () => {
const defaultLogger = new Logger();
expect(defaultLogger).toBeDefined();
});
test('should create logger with custom configuration', () => {
const customLogger = createLogger({
level: LogLevel.ERROR,
enableConsole: false,
enableFile: false
});
expect(customLogger).toBeDefined();
});
test('should log messages at different levels', () => {
expect(() => {
logger.debug('Debug message');
logger.info('Info message');
logger.warn('Warning message');
logger.error('Error message');
}).not.toThrow();
});
test('should log security events', () => {
expect(() => {
logger.security(
'Test security event',
'test_event',
'high',
'user_input',
'sanitized_input',
{ key: 'value' }
);
}).not.toThrow();
});
test('should log operation lifecycle', () => {
expect(() => {
logger.startOperation('Test Operation');
logger.completeOperation('Test Operation', 1000);
logger.failOperation('Failed Operation', new Error('Test error'));
}).not.toThrow();
});
test('should log file operations', () => {
expect(() => {
logger.fileOperation('read', '/test/file.txt', true);
logger.fileOperation('write', '/test/file.txt', false, 'Permission denied');
}).not.toThrow();
});
test('should log template operations', () => {
expect(() => {
logger.templateOperation('validate', '/templates/basic', { files: 10 });
logger.templateOperation('process', '/templates/advanced');
}).not.toThrow();
});
test('should handle progress logging', () => {
expect(() => {
for (let i = 0; i <= 100; i += 10) {
logger.progress('Processing files', i, 100);
}
}).not.toThrow();
});
test('should log success and failure messages', () => {
expect(() => {
logger.success('Operation completed successfully');
logger.failure('Operation failed', new Error('Test error'));
}).not.toThrow();
});
test('should flush logs without errors', async () => {
logger.info('Test message before flush');
logger.security('Security event', 'test', 'medium');
await expect(logger.flush()).resolves.not.toThrow();
});
test('should handle logging configuration changes', () => {
expect(() => {
logger.configure({
level: LogLevel.ERROR,
verbose: false
});
logger.debug('This should not be logged');
logger.error('This should be logged');
}).not.toThrow();
});
test('should handle errors in error logging gracefully', () => {
const circularObject = { name: 'test' } as any;
circularObject.self = circularObject;
expect(() => {
logger.error('Error with circular reference', new Error('Test'), 'Test', circularObject);
}).not.toThrow();
});
});
describe('ErrorRecovery', () => {
let logger: Logger;
let errorRecovery: ErrorRecovery;
beforeEach(() => {
logger = new Logger({ enableConsole: false });
errorRecovery = new ErrorRecovery(logger);
});
test('should create ErrorRecovery instance', () => {
expect(errorRecovery).toBeDefined();
});
test('should recover from file operation errors', async () => {
// Test ENOENT (file not found)
const enoentError = new Error('ENOENT: no such file or directory');
const canRecoverEnoent = await errorRecovery.recoverFromFileError(
enoentError,
'read',
'/nonexistent/file.txt'
);
expect(canRecoverEnoent).toBe(true);
// Test EACCES (permission denied)
const eaccesError = new Error('EACCES: permission denied');
const canRecoverEacces = await errorRecovery.recoverFromFileError(
eaccesError,
'write',
'/restricted/file.txt'
);
expect(canRecoverEacces).toBe(true);
// Test EEXIST (file exists)
const eexistError = new Error('EEXIST: file already exists');
const canRecoverEexist = await errorRecovery.recoverFromFileError(
eexistError,
'create',
'/path/to/existing/file.txt'
);
expect(canRecoverEexist).toBe(true);
// Test unknown error
const unknownError = new Error('Unknown file system error');
const canRecoverUnknown = await errorRecovery.recoverFromFileError(
unknownError,
'unknown',
'/path/to/file.txt'
);
expect(canRecoverUnknown).toBe(false);
});
test('should recover from template processing errors', async () => {
// Test validation error
const validationError = new Error('Template validation failed');
const canRecoverValidation = await errorRecovery.recoverFromTemplateError(
validationError,
'/templates/invalid'
);
expect(canRecoverValidation).toBe(true);
// Test invalid template error
const invalidError = new Error('Invalid template structure');
const canRecoverInvalid = await errorRecovery.recoverFromTemplateError(
invalidError,
'/templates/corrupt'
);
expect(canRecoverInvalid).toBe(true);
// Test unknown template error
const unknownError = new Error('Unknown template error');
const canRecoverUnknown = await errorRecovery.recoverFromTemplateError(
unknownError,
'/templates/unknown'
);
expect(canRecoverUnknown).toBe(false);
});
test('should provide appropriate error recovery suggestions', () => {
// Permission error
const permissionError = new Error('EACCES: permission denied');
const permissionSuggestion = errorRecovery.getErrorRecoverySuggestion(permissionError);
expect(permissionSuggestion).toMatch(/permission/i);
expect(permissionSuggestion).toMatch(/elevated/i);
// Disk space error
const spaceError = new Error('ENOSPC: no space left on device');
const spaceSuggestion = errorRecovery.getErrorRecoverySuggestion(spaceError);
expect(spaceSuggestion).toMatch(/disk space/i);
expect(spaceSuggestion).toMatch(/free up/i);
// File limit error
const fileLimitError = new Error('EMFILE: too many open files');
const fileLimitSuggestion = errorRecovery.getErrorRecoverySuggestion(fileLimitError);
expect(fileLimitSuggestion).toMatch(/open files/i);
expect(fileLimitSuggestion).toMatch(/close/i);
// Network error
const networkError = new Error('Network fetch failed');
const networkSuggestion = errorRecovery.getErrorRecoverySuggestion(networkError);
expect(networkSuggestion).toMatch(/network|connection/i);
// Generic error
const genericError = new Error('Something went wrong');
const genericSuggestion = errorRecovery.getErrorRecoverySuggestion(genericError);
expect(genericSuggestion).toMatch(/unexpected error/i);
expect(genericSuggestion).toMatch(/logs/i);
});
test('should handle multiple concurrent recovery attempts', async () => {
const errors = [
new Error('ENOENT: file not found'),
new Error('EACCES: permission denied'),
new Error('EEXIST: file exists'),
new Error('Unknown error'),
];
const recoveryPromises = errors.map((error, index) =>
errorRecovery.recoverFromFileError(error, 'test', `/test/file${index}.txt`)
);
const results = await Promise.all(recoveryPromises);
// Should handle concurrent recovery attempts
expect(results.length).toBe(4);
expect(results[0]).toBe(true); // ENOENT
expect(results[1]).toBe(true); // EACCES
expect(results[2]).toBe(true); // EEXIST
expect(results[3]).toBe(false); // Unknown
});
test('should handle error recovery with null or undefined errors', async () => {
// Should not crash with null/undefined errors
expect(async () => {
await errorRecovery.recoverFromFileError(
null as any,
'test',
'/test/file.txt'
);
}).not.toThrow();
expect(() => {
errorRecovery.getErrorRecoverySuggestion(null as any);
}).not.toThrow();
});
});
describe('Logger Integration', () => {
test('should work together with ErrorRecovery', async () => {
const logger = new Logger({ enableConsole: false });
const errorRecovery = new ErrorRecovery(logger);
// Simulate error recovery scenario
const error = new Error('EACCES: permission denied, open /restricted/file');
// Log the error attempt
logger.error('File operation failed', error, 'FileSystem');
// Attempt recovery
const canRecover = await errorRecovery.recoverFromFileError(
error,
'write',
'/restricted/file'
);
if (canRecover) {
logger.info('Error recovery successful', 'ErrorRecovery');
} else {
logger.error('Error recovery failed', error, 'ErrorRecovery');
}
// Should not throw during this workflow
expect(canRecover).toBe(true);
await logger.flush();
});
test('should handle high-frequency logging without memory leaks', () => {
const logger = new Logger({
enableConsole: false,
enableFile: false // Disable file logging for performance
});
// Log many messages rapidly
for (let i = 0; i < 1000; i++) {
logger.debug(`Debug message ${i}`);
logger.info(`Info message ${i}`);
if (i % 100 === 0) {
logger.security(
`Security event ${i}`,
'test_event',
'low',
`input_${i}`,
`sanitized_${i}`
);
}
}
// Should not crash or consume excessive memory
expect(logger).toBeDefined();
});
test('should maintain log entry ordering under concurrent access', async () => {
const logger = new Logger({ enableConsole: false });
// Create concurrent logging operations
const logOperations = Array.from({ length: 50 }, (_, i) =>
Promise.resolve().then(() => {
logger.info(`Concurrent message ${i}`, 'ConcurrencyTest');
logger.debug(`Debug message ${i}`, 'ConcurrencyTest');
logger.security(
`Security event ${i}`,
'concurrency_test',
'low',
undefined,
undefined,
{ index: i }
);
})
);
// Wait for all operations to complete
await Promise.all(logOperations);
await logger.flush();
// Should complete without errors
expect(logOperations.length).toBe(50);
});
});