network-performance-analyzer
Version:
Automated analysis tool for network performance test datasets containing DNS testing results and iperf3 performance measurements
428 lines (353 loc) • 14.5 kB
text/typescript
// Tests for ErrorHandler utility
import { DefaultErrorHandler, ErrorSeverity, ErrorLogEntry } from '../../src/utils/ErrorHandler';
import { FileSystemError, ParsingError, AnalysisError } from '../../src/models';
import * as fs from 'fs';
import * as path from 'path';
// Mock fs module
jest.mock('fs', () => ({
existsSync: jest.fn(),
mkdirSync: jest.fn(),
writeFileSync: jest.fn()
}));
describe('ErrorHandler', () => {
let errorHandler: DefaultErrorHandler;
let consoleErrorSpy: jest.SpyInstance;
beforeEach(() => {
errorHandler = new DefaultErrorHandler();
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
jest.clearAllMocks();
});
afterEach(() => {
consoleErrorSpy.mockRestore();
});
describe('constructor', () => {
it('should create log directory if it does not exist', () => {
// Arrange
(fs.existsSync as jest.Mock).mockReturnValue(false);
// Act
new DefaultErrorHandler({ logToFile: true, logDirectory: './custom-logs' });
// Assert
expect(fs.existsSync).toHaveBeenCalledWith('./custom-logs');
expect(fs.mkdirSync).toHaveBeenCalledWith('./custom-logs', { recursive: true });
});
it('should not create log directory if it already exists', () => {
// Arrange
(fs.existsSync as jest.Mock).mockReturnValue(true);
// Act
new DefaultErrorHandler({ logToFile: true });
// Assert
expect(fs.existsSync).toHaveBeenCalledWith('./logs');
expect(fs.mkdirSync).not.toHaveBeenCalled();
});
it('should handle error when creating log directory fails', () => {
// Arrange
(fs.existsSync as jest.Mock).mockReturnValue(false);
(fs.mkdirSync as jest.Mock).mockImplementation(() => {
throw new Error('Permission denied');
});
// Act & Assert
expect(() => {
new DefaultErrorHandler({ logToFile: true });
}).not.toThrow();
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Failed to create log directory')
);
});
});
describe('handleFileSystemError', () => {
it('should log file system errors with path information', () => {
// Arrange
const error = new Error('File not found') as FileSystemError;
error.code = 'ENOENT';
error.path = '/path/to/file';
// Act
const result = errorHandler.handleFileSystemError(error);
// Assert
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('File system error at path: /path/to/file')
);
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.any(String));
expect(result).toBe(true); // ENOENT errors are recoverable
});
it('should correctly determine severity for different file system errors', () => {
// Arrange
const errorHandler = new DefaultErrorHandler();
const errors = [
{ code: 'ENOENT', path: '/path/to/file', message: 'File not found', expected: ErrorSeverity.MEDIUM },
{ code: 'EACCES', path: '/path/to/file', message: 'Permission denied', expected: ErrorSeverity.HIGH },
{ code: 'EISDIR', path: '/path/to/file', message: 'Is a directory', expected: ErrorSeverity.MEDIUM },
{ code: 'ENOSPC', path: '/path/to/file', message: 'No space left', expected: ErrorSeverity.CRITICAL }
];
// Act & Assert
errors.forEach(errorData => {
const error = new Error(errorData.message) as FileSystemError;
error.code = errorData.code;
error.path = errorData.path;
errorHandler.handleFileSystemError(error);
const log = errorHandler.getErrorLog();
expect(log.length).toBeGreaterThan(0);
// Safe access with explicit check
const lastEntry = log.length > 0 ? log[log.length - 1] : null;
if (lastEntry) {
expect(lastEntry.severity).toBe(errorData.expected);
}
});
});
it('should correctly determine recoverability for different file system errors', () => {
// Arrange
const errorHandler = new DefaultErrorHandler();
const errors = [
{ code: 'ENOENT', path: '/path/to/file', message: 'File not found', expected: true },
{ code: 'EACCES', path: '/path/to/file', message: 'Permission denied', expected: true },
{ code: 'ENOSPC', path: '/path/to/file', message: 'No space left', expected: false }
];
// Act & Assert
errors.forEach(errorData => {
const error = new Error(errorData.message) as FileSystemError;
error.code = errorData.code;
error.path = errorData.path;
const result = errorHandler.handleFileSystemError(error);
expect(result).toBe(errorData.expected);
});
});
});
describe('handleParsingError', () => {
it('should log parsing errors with file and location information', () => {
// Arrange
const error = new Error('Invalid JSON') as ParsingError;
error.filePath = '/path/to/data.json';
error.lineNumber = 42;
error.columnNumber = 10;
// Act
const result = errorHandler.handleParsingError(error);
// Assert
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Parsing error in file /path/to/data.json at line 42, column 10')
);
expect(result).toBe(true); // Parsing errors are recoverable
const log = errorHandler.getErrorLog();
expect(log.length).toBeGreaterThan(0);
if (log.length > 0 && log[0]) {
expect(log[0].additionalInfo).toEqual({
errorType: 'ParsingError',
lineNumber: 42,
columnNumber: 10
});
}
});
it('should handle parsing errors without column information', () => {
// Arrange
const error = new Error('Invalid JSON') as ParsingError;
error.filePath = '/path/to/data.json';
error.lineNumber = 42;
// Act
errorHandler.handleParsingError(error);
// Assert
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Parsing error in file /path/to/data.json at line 42')
);
});
it('should handle parsing errors without line or column information', () => {
// Arrange
const error = new Error('Invalid JSON') as ParsingError;
error.filePath = '/path/to/data.json';
// Act
errorHandler.handleParsingError(error);
// Assert
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Parsing error in file /path/to/data.json at unknown location')
);
});
});
describe('handleAnalysisError', () => {
it('should log analysis errors with type and dataset information', () => {
// Arrange
const error = new Error('Analysis failed') as AnalysisError;
error.analysisType = 'IperfAnalysis';
error.datasetName = 'coredns-mtu1500-aws-logs_disabled';
// Act
const result = errorHandler.handleAnalysisError(error);
// Assert
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Analysis error in IperfAnalysis for dataset coredns-mtu1500-aws-logs_disabled')
);
expect(result).toBe(true); // Analysis errors are recoverable
const log = errorHandler.getErrorLog();
expect(log.length).toBeGreaterThan(0);
if (log.length > 0 && log[0]) {
expect(log[0].additionalInfo).toEqual({
errorType: 'AnalysisError',
analysisType: 'IperfAnalysis',
datasetName: 'coredns-mtu1500-aws-logs_disabled'
});
}
});
it('should handle analysis errors without dataset information', () => {
// Arrange
const error = new Error('Analysis failed') as AnalysisError;
error.analysisType = 'IperfAnalysis';
// Act
errorHandler.handleAnalysisError(error);
// Assert
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Analysis error in IperfAnalysis')
);
});
});
describe('handleValidationError', () => {
it('should log validation errors with entity information', () => {
// Arrange
const error = new Error('Invalid data format');
const entityType = 'TestResults';
const entityId = 'coredns-mtu1500-aws-logs_disabled/results_20250717_122222.json';
// Act
const result = errorHandler.handleValidationError(error, entityType, entityId);
// Assert
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining(`Validation error for ${entityType}: ${entityId}`)
);
expect(result).toBe(true); // Validation errors are recoverable
const log = errorHandler.getErrorLog();
expect(log.length).toBeGreaterThan(0);
if (log.length > 0 && log[0]) {
expect(log[0].additionalInfo).toEqual({
errorType: 'ValidationError',
entityType,
entityId
});
}
});
});
describe('logError', () => {
it('should log errors with context and stack trace', () => {
// Arrange
const error = new Error('Something went wrong');
const context = 'Test context';
// Act
errorHandler.logError(error, context);
// Assert
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('[ERROR] Test context: Something went wrong')
);
expect(consoleErrorSpy).toHaveBeenCalledWith(error.stack);
});
it('should handle errors without stack traces', () => {
// Arrange
const error = { message: 'Custom error without stack' } as Error;
const context = 'Test context';
// Act
errorHandler.logError(error, context);
// Assert
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('[ERROR] Test context: Custom error without stack')
);
// Should not throw when stack is undefined
expect(() => errorHandler.logError(error, context)).not.toThrow();
});
});
describe('error log management', () => {
it('should store errors in the error log', () => {
// Arrange
const fileError = new Error('File not found') as FileSystemError;
fileError.code = 'ENOENT';
fileError.path = '/path/to/file';
const parseError = new Error('Invalid JSON') as ParsingError;
parseError.filePath = '/path/to/data.json';
// Act
errorHandler.handleFileSystemError(fileError);
errorHandler.handleParsingError(parseError);
// Assert
const log = errorHandler.getErrorLog();
expect(log.length).toBe(2);
if (log.length >= 2 && log[0] && log[1]) {
expect(log[0].additionalInfo?.errorType).toBe('FileSystemError');
expect(log[1].additionalInfo?.errorType).toBe('ParsingError');
}
});
it('should limit the error log to maxLogEntries', () => {
// Arrange
const errorHandler = new DefaultErrorHandler({ maxLogEntries: 2 });
// Act
for (let i = 0; i < 5; i++) {
const error = new Error(`Error ${i}`) as FileSystemError;
error.code = 'ENOENT';
error.path = `/path/to/file${i}`;
errorHandler.handleFileSystemError(error);
}
// Assert
const log = errorHandler.getErrorLog();
expect(log.length).toBe(2);
if (log.length >= 2 && log[0] && log[1]) {
expect(log[0].message).toBe('Error 3');
expect(log[1].message).toBe('Error 4');
}
});
it('should clear the error log', () => {
// Arrange
const error = new Error('Test error') as FileSystemError;
error.code = 'ENOENT';
error.path = '/path/to/file';
errorHandler.handleFileSystemError(error);
// Act
errorHandler.clearErrorLog();
// Assert
expect(errorHandler.getErrorLog().length).toBe(0);
});
it('should detect errors of specified severity', () => {
// Arrange
const errorHandler = new DefaultErrorHandler();
const lowError = new Error('Low severity') as FileSystemError;
lowError.code = 'ENOENT';
lowError.path = '/path/to/file1';
const highError = new Error('High severity') as FileSystemError;
highError.code = 'EACCES';
highError.path = '/path/to/file2';
// Act
errorHandler.handleFileSystemError(lowError);
errorHandler.handleFileSystemError(highError);
// Assert
expect(errorHandler.hasErrorsOfSeverity(ErrorSeverity.LOW)).toBe(true);
expect(errorHandler.hasErrorsOfSeverity(ErrorSeverity.MEDIUM)).toBe(true);
expect(errorHandler.hasErrorsOfSeverity(ErrorSeverity.HIGH)).toBe(true);
expect(errorHandler.hasErrorsOfSeverity(ErrorSeverity.CRITICAL)).toBe(false);
});
it('should write error log to file when configured', () => {
// Arrange
jest.spyOn(Date.prototype, 'toISOString').mockReturnValue('2025-07-17T12:00:00.000Z');
(fs.existsSync as jest.Mock).mockReturnValue(true);
const errorHandler = new DefaultErrorHandler({
logToFile: true,
logDirectory: './test-logs'
});
const error = new Error('Test error') as FileSystemError;
error.code = 'ENOENT';
error.path = '/path/to/file';
// Act
errorHandler.handleFileSystemError(error);
// Assert
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.stringContaining('test-logs/error-log-2025-07-17T12-00-00.000Z.json'),
expect.any(String),
{ flag: 'w' }
);
});
it('should handle errors when writing to log file', () => {
// Arrange
(fs.existsSync as jest.Mock).mockReturnValue(true);
(fs.writeFileSync as jest.Mock).mockImplementation(() => {
throw new Error('Write failed');
});
const errorHandler = new DefaultErrorHandler({ logToFile: true });
const error = new Error('Test error') as FileSystemError;
error.code = 'ENOENT';
error.path = '/path/to/file';
// Act & Assert
expect(() => {
errorHandler.handleFileSystemError(error);
}).not.toThrow();
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Failed to write to error log file')
);
});
});
});