Files
gridpilot.gg/core/shared/application/ErrorReporter.test.ts
Marc Mintel 093eece3d7
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m47s
Contract Testing / contract-snapshot (pull_request) Has been skipped
core tests
2026-01-22 18:20:33 +01:00

367 lines
11 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { ErrorReporter } from './ErrorReporter';
describe('ErrorReporter', () => {
describe('ErrorReporter interface', () => {
it('should have report method', () => {
const errors: Array<{ error: Error; context?: unknown }> = [];
const reporter: ErrorReporter = {
report: (error: Error, context?: unknown) => {
errors.push({ error, context });
}
};
const testError = new Error('Test error');
reporter.report(testError, { userId: 123 });
expect(errors).toHaveLength(1);
expect(errors[0].error).toBe(testError);
expect(errors[0].context).toEqual({ userId: 123 });
});
it('should support reporting without context', () => {
const errors: Error[] = [];
const reporter: ErrorReporter = {
report: (error: Error) => {
errors.push(error);
}
};
const testError = new Error('Test error');
reporter.report(testError);
expect(errors).toHaveLength(1);
expect(errors[0]).toBe(testError);
});
it('should support different error types', () => {
const errors: Array<{ error: Error; context?: unknown }> = [];
const reporter: ErrorReporter = {
report: (error: Error, context?: unknown) => {
errors.push({ error, context });
}
};
// Standard Error
const standardError = new Error('Standard error');
reporter.report(standardError, { type: 'standard' });
// Custom Error
class CustomError extends Error {
constructor(message: string, public code: string) {
super(message);
this.name = 'CustomError';
}
}
const customError = new CustomError('Custom error', 'CUSTOM_CODE');
reporter.report(customError, { type: 'custom' });
// TypeError
const typeError = new TypeError('Type error');
reporter.report(typeError, { type: 'type' });
expect(errors).toHaveLength(3);
expect(errors[0].error).toBe(standardError);
expect(errors[1].error).toBe(customError);
expect(errors[2].error).toBe(typeError);
});
it('should support complex context objects', () => {
const errors: Array<{ error: Error; context?: unknown }> = [];
const reporter: ErrorReporter = {
report: (error: Error, context?: unknown) => {
errors.push({ error, context });
}
};
const complexContext = {
user: {
id: 'user-123',
name: 'John Doe',
email: 'john@example.com'
},
request: {
method: 'POST',
url: '/api/users',
headers: {
'content-type': 'application/json',
'authorization': 'Bearer token'
}
},
timestamp: new Date('2024-01-01T12:00:00Z'),
metadata: {
retryCount: 3,
timeout: 5000
}
};
const error = new Error('Request failed');
reporter.report(error, complexContext);
expect(errors).toHaveLength(1);
expect(errors[0].error).toBe(error);
expect(errors[0].context).toEqual(complexContext);
});
});
describe('ErrorReporter behavior', () => {
it('should support logging error with stack trace', () => {
const logs: Array<{ message: string; stack?: string; context?: unknown }> = [];
const reporter: ErrorReporter = {
report: (error: Error, context?: unknown) => {
logs.push({
message: error.message,
stack: error.stack,
context
});
}
};
const error = new Error('Database connection failed');
reporter.report(error, { retryCount: 3 });
expect(logs).toHaveLength(1);
expect(logs[0].message).toBe('Database connection failed');
expect(logs[0].stack).toBeDefined();
expect(logs[0].context).toEqual({ retryCount: 3 });
});
it('should support error aggregation', () => {
const errorCounts: Record<string, number> = {};
const reporter: ErrorReporter = {
report: (error: Error) => {
const errorType = error.name || 'Unknown';
errorCounts[errorType] = (errorCounts[errorType] || 0) + 1;
}
};
const error1 = new Error('Error 1');
const error2 = new TypeError('Type error');
const error3 = new Error('Error 2');
const error4 = new TypeError('Another type error');
reporter.report(error1);
reporter.report(error2);
reporter.report(error3);
reporter.report(error4);
expect(errorCounts['Error']).toBe(2);
expect(errorCounts['TypeError']).toBe(2);
});
it('should support error filtering', () => {
const criticalErrors: Error[] = [];
const warnings: Error[] = [];
const reporter: ErrorReporter = {
report: (error: Error, context?: unknown) => {
const isCritical = context && typeof context === 'object' && 'severity' in context &&
(context as { severity: string }).severity === 'critical';
if (isCritical) {
criticalErrors.push(error);
} else {
warnings.push(error);
}
}
};
const criticalError = new Error('Critical failure');
const warningError = new Error('Warning');
reporter.report(criticalError, { severity: 'critical' });
reporter.report(warningError, { severity: 'warning' });
expect(criticalErrors).toHaveLength(1);
expect(criticalErrors[0]).toBe(criticalError);
expect(warnings).toHaveLength(1);
expect(warnings[0]).toBe(warningError);
});
it('should support error enrichment', () => {
const enrichedErrors: Array<{ error: Error; enrichedContext: unknown }> = [];
const reporter: ErrorReporter = {
report: (error: Error, context?: unknown) => {
const enrichedContext: Record<string, unknown> = {
errorName: error.name,
errorMessage: error.message,
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV || 'development'
};
if (context && typeof context === 'object') {
Object.assign(enrichedContext, context);
}
enrichedErrors.push({ error, enrichedContext });
}
};
const error = new Error('Something went wrong');
reporter.report(error, { userId: 'user-123', action: 'login' });
expect(enrichedErrors).toHaveLength(1);
expect(enrichedErrors[0].error).toBe(error);
expect(enrichedErrors[0].enrichedContext).toMatchObject({
userId: 'user-123',
action: 'login',
errorName: 'Error',
errorMessage: 'Something went wrong',
environment: 'development'
});
});
it('should support error deduplication', () => {
const uniqueErrors: Error[] = [];
const seenMessages = new Set<string>();
const reporter: ErrorReporter = {
report: (error: Error) => {
if (!seenMessages.has(error.message)) {
uniqueErrors.push(error);
seenMessages.add(error.message);
}
}
};
const error1 = new Error('Duplicate error');
const error2 = new Error('Duplicate error');
const error3 = new Error('Unique error');
reporter.report(error1);
reporter.report(error2);
reporter.report(error3);
expect(uniqueErrors).toHaveLength(2);
expect(uniqueErrors[0].message).toBe('Duplicate error');
expect(uniqueErrors[1].message).toBe('Unique error');
});
it('should support error rate limiting', () => {
const errors: Error[] = [];
let errorCount = 0;
const rateLimit = 5;
const reporter: ErrorReporter = {
report: (error: Error) => {
errorCount++;
if (errorCount <= rateLimit) {
errors.push(error);
}
// Silently drop errors beyond rate limit
}
};
// Report 10 errors
for (let i = 0; i < 10; i++) {
reporter.report(new Error(`Error ${i}`));
}
expect(errors).toHaveLength(rateLimit);
expect(errorCount).toBe(10);
});
});
describe('ErrorReporter implementation patterns', () => {
it('should support console logger implementation', () => {
const consoleErrors: string[] = [];
const originalConsoleError = console.error;
// Mock console.error
console.error = (...args: unknown[]) => consoleErrors.push(args.join(' '));
const consoleReporter: ErrorReporter = {
report: (error: Error, context?: unknown) => {
console.error('Error:', error.message, 'Context:', context);
}
};
const error = new Error('Test error');
consoleReporter.report(error, { userId: 123 });
// Restore console.error
console.error = originalConsoleError;
expect(consoleErrors).toHaveLength(1);
expect(consoleErrors[0]).toContain('Error:');
expect(consoleErrors[0]).toContain('Test error');
expect(consoleErrors[0]).toContain('Context:');
});
it('should support file logger implementation', () => {
const fileLogs: Array<{ timestamp: string; error: string; context?: unknown }> = [];
const fileReporter: ErrorReporter = {
report: (error: Error, context?: unknown) => {
fileLogs.push({
timestamp: new Date().toISOString(),
error: error.message,
context
});
}
};
const error = new Error('File error');
fileReporter.report(error, { file: 'test.txt', line: 42 });
expect(fileLogs).toHaveLength(1);
expect(fileLogs[0].error).toBe('File error');
expect(fileLogs[0].context).toEqual({ file: 'test.txt', line: 42 });
});
it('should support remote reporter implementation', async () => {
const remoteErrors: Array<{ error: string; context?: unknown }> = [];
const remoteReporter: ErrorReporter = {
report: async (error: Error, context?: unknown) => {
remoteErrors.push({ error: error.message, context });
await new Promise(resolve => setTimeout(resolve, 1));
}
};
const error = new Error('Remote error');
await remoteReporter.report(error, { endpoint: '/api/data' });
expect(remoteErrors).toHaveLength(1);
expect(remoteErrors[0].error).toBe('Remote error');
expect(remoteErrors[0].context).toEqual({ endpoint: '/api/data' });
});
it('should support batch error reporting', () => {
const batchErrors: Error[] = [];
const batchSize = 3;
let currentBatch: Error[] = [];
const reporter: ErrorReporter = {
report: (error: Error) => {
currentBatch.push(error);
if (currentBatch.length >= batchSize) {
batchErrors.push(...currentBatch);
currentBatch = [];
}
}
};
// Report 7 errors
for (let i = 0; i < 7; i++) {
reporter.report(new Error(`Error ${i}`));
}
// Add remaining errors
if (currentBatch.length > 0) {
batchErrors.push(...currentBatch);
}
expect(batchErrors).toHaveLength(7);
});
});
});