UNPKG

@ahmedhegazee/nestjs-telescope

Version:

Advanced observability and monitoring solution for NestJS applications with ML-powered analytics, enterprise features, and production-ready scaling

360 lines 18.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const testing_1 = require("@nestjs/testing"); const common_1 = require("@nestjs/common"); const exception_watcher_filter_1 = require("./exception-watcher.filter"); const exception_watcher_service_1 = require("./exception-watcher.service"); const exception_watcher_config_1 = require("./exception-watcher.config"); describe('ExceptionWatcherFilter', () => { let filter; let exceptionWatcherService; let mockArgumentsHost; let mockRequest; let mockResponse; beforeEach(async () => { mockRequest = { id: 'req-123', method: 'GET', url: '/test', path: '/test', headers: { 'user-agent': 'test-agent', 'authorization': 'Bearer token123', 'x-custom-header': 'custom-value', }, body: { username: 'testuser', password: 'secret123', email: 'test@example.com', }, params: { id: '123' }, query: { filter: 'active' }, ip: '127.0.0.1', user: { id: 'user-456' }, session: { id: 'session-789' }, startTime: Date.now() - 100, traceId: 'trace-abc', }; mockResponse = { status: jest.fn().mockReturnThis(), json: jest.fn(), getHeaders: jest.fn().mockReturnValue({ 'content-type': 'application/json', 'x-response-time': '100ms', }), }; const mockHttpContext = { getRequest: jest.fn().mockReturnValue(mockRequest), getResponse: jest.fn().mockReturnValue(mockResponse), }; mockArgumentsHost = { switchToHttp: jest.fn().mockReturnValue(mockHttpContext), getType: jest.fn().mockReturnValue('http'), getArgs: jest.fn(), getArgByIndex: jest.fn(), switchToRpc: jest.fn(), switchToWs: jest.fn(), }; const mockExceptionWatcherService = { trackException: jest.fn(), }; const module = await testing_1.Test.createTestingModule({ providers: [ exception_watcher_filter_1.ExceptionWatcherFilter, { provide: exception_watcher_service_1.ExceptionWatcherService, useValue: mockExceptionWatcherService, }, { provide: 'EXCEPTION_WATCHER_CONFIG', useValue: exception_watcher_config_1.defaultExceptionWatcherConfig, }, ], }).compile(); filter = module.get(exception_watcher_filter_1.ExceptionWatcherFilter); exceptionWatcherService = module.get(exception_watcher_service_1.ExceptionWatcherService); }); afterEach(() => { jest.clearAllMocks(); }); describe('exception catching', () => { it('should catch and process HttpException', () => { const exception = new common_1.HttpException('Bad Request', common_1.HttpStatus.BAD_REQUEST); filter.catch(exception, mockArgumentsHost); expect(exceptionWatcherService.trackException).toHaveBeenCalledWith(expect.objectContaining({ errorType: 'HttpException', errorMessage: 'Bad Request', statusCode: common_1.HttpStatus.BAD_REQUEST, })); expect(mockResponse.status).toHaveBeenCalledWith(common_1.HttpStatus.BAD_REQUEST); expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({ statusCode: common_1.HttpStatus.BAD_REQUEST, message: 'Bad Request', })); }); it('should catch and process generic Error', () => { const exception = new Error('Internal server error'); filter.catch(exception, mockArgumentsHost); expect(exceptionWatcherService.trackException).toHaveBeenCalledWith(expect.objectContaining({ errorType: 'Error', errorMessage: 'Internal server error', statusCode: common_1.HttpStatus.INTERNAL_SERVER_ERROR, })); expect(mockResponse.status).toHaveBeenCalledWith(common_1.HttpStatus.INTERNAL_SERVER_ERROR); }); it('should not process exception if disabled', () => { const disabledConfig = { ...exception_watcher_config_1.defaultExceptionWatcherConfig, enabled: false, }; const disabledFilter = new exception_watcher_filter_1.ExceptionWatcherFilter(exceptionWatcherService, disabledConfig); const exception = new Error('Test error'); disabledFilter.catch(exception, mockArgumentsHost); expect(exceptionWatcherService.trackException).not.toHaveBeenCalled(); }); it('should respect sampling rate', () => { const samplingConfig = { ...exception_watcher_config_1.defaultExceptionWatcherConfig, sampleRate: 0, }; const samplingFilter = new exception_watcher_filter_1.ExceptionWatcherFilter(exceptionWatcherService, samplingConfig); const exception = new Error('Test error'); samplingFilter.catch(exception, mockArgumentsHost); expect(exceptionWatcherService.trackException).not.toHaveBeenCalled(); }); }); describe('context extraction', () => { it('should extract complete HTTP context', () => { const exception = new Error('Test error'); filter.catch(exception, mockArgumentsHost); const capturedContext = exceptionWatcherService.trackException.mock.calls[0][0]; expect(capturedContext.request).toEqual(expect.objectContaining({ id: 'req-123', method: 'GET', url: '/test', path: '/test', userAgent: 'test-agent', ip: '127.0.0.1', userId: 'user-456', sessionId: 'session-789', })); expect(capturedContext.response).toEqual(expect.objectContaining({ statusCode: common_1.HttpStatus.INTERNAL_SERVER_ERROR, duration: expect.any(Number), })); expect(capturedContext.traceId).toBe('trace-abc'); expect(capturedContext.requestId).toBe('req-123'); expect(capturedContext.userId).toBe('user-456'); expect(capturedContext.sessionId).toBe('session-789'); }); it('should sanitize sensitive headers', () => { const exception = new Error('Test error'); filter.catch(exception, mockArgumentsHost); const capturedContext = exceptionWatcherService.trackException.mock.calls[0][0]; expect(capturedContext.request?.headers?.authorization).toBe('[REDACTED]'); expect(capturedContext.request?.headers?.['x-custom-header']).toBe('custom-value'); }); it('should sanitize sensitive body fields', () => { const sanitizingConfig = { ...exception_watcher_config_1.defaultExceptionWatcherConfig, captureBody: true, }; const sanitizingFilter = new exception_watcher_filter_1.ExceptionWatcherFilter(exceptionWatcherService, sanitizingConfig); const exception = new Error('Test error'); sanitizingFilter.catch(exception, mockArgumentsHost); const capturedContext = exceptionWatcherService.trackException.mock.calls[0][0]; expect(capturedContext.request?.body?.password).toBe('[REDACTED]'); expect(capturedContext.request?.body?.email).toBe('test@example.com'); expect(capturedContext.request?.body?.username).toBe('testuser'); }); it('should extract environment context when enabled', () => { const exception = new Error('Test error'); filter.catch(exception, mockArgumentsHost); const capturedContext = exceptionWatcherService.trackException.mock.calls[0][0]; expect(capturedContext.environment).toEqual(expect.objectContaining({ nodeVersion: expect.any(String), platform: expect.any(String), hostname: expect.any(String), memory: expect.any(Object), uptime: expect.any(Number), })); }); it('should extract performance context when enabled', () => { const exception = new Error('Test error'); filter.catch(exception, mockArgumentsHost); const capturedContext = exceptionWatcherService.trackException.mock.calls[0][0]; expect(capturedContext.performance).toEqual(expect.objectContaining({ memoryUsage: expect.any(Number), cpuUsage: expect.any(Number), })); }); }); describe('stack trace parsing', () => { it('should parse stack trace frames', () => { const exception = new Error('Test error'); exception.stack = `Error: Test error at Object.testFunction (/path/to/file.js:10:5) at /path/to/another/file.js:20:10 at Function.anonymous (/path/to/third/file.js:30:15)`; filter.catch(exception, mockArgumentsHost); const capturedContext = exceptionWatcherService.trackException.mock.calls[0][0]; expect(capturedContext.stackTrace).toBe(exception.stack); expect(capturedContext.stackFrames).toHaveLength(3); expect(capturedContext.stackFrames?.[0]).toEqual({ function: 'Object.testFunction', file: '/path/to/file.js', line: 10, column: 5, }); }); it('should handle malformed stack traces', () => { const exception = new Error('Test error'); exception.stack = 'Invalid stack trace format'; filter.catch(exception, mockArgumentsHost); const capturedContext = exceptionWatcherService.trackException.mock.calls[0][0]; expect(capturedContext.stackTrace).toBe(exception.stack); expect(capturedContext.stackFrames).toEqual([]); }); it('should limit stack trace depth', () => { const limitedConfig = { ...exception_watcher_config_1.defaultExceptionWatcherConfig, maxStackTraceDepth: 2, }; const limitedFilter = new exception_watcher_filter_1.ExceptionWatcherFilter(exceptionWatcherService, limitedConfig); const exception = new Error('Test error'); exception.stack = `Error: Test error at Function1 (/file1.js:1:1) at Function2 (/file2.js:2:2) at Function3 (/file3.js:3:3) at Function4 (/file4.js:4:4)`; limitedFilter.catch(exception, mockArgumentsHost); const capturedContext = exceptionWatcherService.trackException.mock.calls[0][0]; expect(capturedContext.stackFrames).toHaveLength(2); }); }); describe('error classification', () => { it('should classify HTTP errors correctly', () => { const authException = new common_1.HttpException('Unauthorized', common_1.HttpStatus.UNAUTHORIZED); const validationException = new common_1.HttpException('Validation failed', common_1.HttpStatus.UNPROCESSABLE_ENTITY); const serverException = new common_1.HttpException('Internal error', common_1.HttpStatus.INTERNAL_SERVER_ERROR); filter.catch(authException, mockArgumentsHost); filter.catch(validationException, mockArgumentsHost); filter.catch(serverException, mockArgumentsHost); const calls = exceptionWatcherService.trackException.mock.calls; expect(calls[0][0].classification?.category).toBe(exception_watcher_filter_1.ErrorCategory.AUTHENTICATION_ERROR); expect(calls[0][0].classification?.severity).toBe(exception_watcher_filter_1.ErrorSeverity.MEDIUM); expect(calls[1][0].classification?.category).toBe(exception_watcher_filter_1.ErrorCategory.VALIDATION_ERROR); expect(calls[1][0].classification?.severity).toBe(exception_watcher_filter_1.ErrorSeverity.MEDIUM); expect(calls[2][0].classification?.category).toBe(exception_watcher_filter_1.ErrorCategory.SERVER_ERROR); expect(calls[2][0].classification?.severity).toBe(exception_watcher_filter_1.ErrorSeverity.HIGH); }); it('should classify database errors', () => { const databaseException = new Error('Database connection failed'); databaseException.name = 'DatabaseError'; filter.catch(databaseException, mockArgumentsHost); const capturedContext = exceptionWatcherService.trackException.mock.calls[0][0]; expect(capturedContext.classification?.category).toBe(exception_watcher_filter_1.ErrorCategory.DATABASE_ERROR); expect(capturedContext.classification?.severity).toBe(exception_watcher_filter_1.ErrorSeverity.HIGH); }); it('should generate fingerprints for similar errors', () => { const error1 = new Error('Database connection failed'); const error2 = new Error('Database connection failed'); filter.catch(error1, mockArgumentsHost); filter.catch(error2, mockArgumentsHost); const calls = exceptionWatcherService.trackException.mock.calls; const fingerprint1 = calls[0][0].classification?.fingerprint; const fingerprint2 = calls[1][0].classification?.fingerprint; expect(fingerprint1).toBe(fingerprint2); }); it('should generate group IDs for similar errors', () => { const error1 = new Error('Validation failed for field X'); const error2 = new Error('Validation failed for field Y'); filter.catch(error1, mockArgumentsHost); filter.catch(error2, mockArgumentsHost); const calls = exceptionWatcherService.trackException.mock.calls; const groupId1 = calls[0][0].classification?.groupId; const groupId2 = calls[1][0].classification?.groupId; expect(groupId1).toBe(groupId2); }); }); describe('error exclusion', () => { it('should exclude errors by type', () => { const exclusionConfig = { ...exception_watcher_config_1.defaultExceptionWatcherConfig, excludeErrorTypes: ['ValidationError'], }; const exclusionFilter = new exception_watcher_filter_1.ExceptionWatcherFilter(exceptionWatcherService, exclusionConfig); const validationError = new Error('Validation failed'); validationError.name = 'ValidationError'; exclusionFilter.catch(validationError, mockArgumentsHost); expect(exceptionWatcherService.trackException).not.toHaveBeenCalled(); }); it('should exclude errors by message', () => { const exclusionConfig = { ...exception_watcher_config_1.defaultExceptionWatcherConfig, excludeErrorMessages: ['timeout'], }; const exclusionFilter = new exception_watcher_filter_1.ExceptionWatcherFilter(exceptionWatcherService, exclusionConfig); const timeoutError = new Error('Connection timeout occurred'); exclusionFilter.catch(timeoutError, mockArgumentsHost); expect(exceptionWatcherService.trackException).not.toHaveBeenCalled(); }); }); describe('non-HTTP contexts', () => { it('should handle non-HTTP exceptions', () => { const nonHttpHost = { ...mockArgumentsHost, getType: jest.fn().mockReturnValue('rpc'), }; const exception = new Error('RPC error'); filter.catch(exception, nonHttpHost); expect(exceptionWatcherService.trackException).toHaveBeenCalledWith(expect.objectContaining({ errorType: 'Error', errorMessage: 'RPC error', request: undefined, response: undefined, })); }); }); describe('body size limitation', () => { it('should truncate large request bodies', () => { const largeBodyConfig = { ...exception_watcher_config_1.defaultExceptionWatcherConfig, captureBody: true, maxContextSize: 100, }; const largeBodyFilter = new exception_watcher_filter_1.ExceptionWatcherFilter(exceptionWatcherService, largeBodyConfig); const largeBody = { data: 'x'.repeat(1000) }; mockRequest.body = largeBody; const exception = new Error('Test error'); largeBodyFilter.catch(exception, mockArgumentsHost); const capturedContext = exceptionWatcherService.trackException.mock.calls[0][0]; expect(capturedContext.request?.body).toEqual({ _truncated: true, _size: expect.any(Number), }); }); }); describe('error handling', () => { it('should handle filter errors gracefully', () => { exceptionWatcherService.trackException.mockImplementation(() => { throw new Error('Service error'); }); const exception = new Error('Test error'); expect(() => filter.catch(exception, mockArgumentsHost)).not.toThrow(); }); it('should handle missing request properties', () => { const incompleteRequest = { method: 'GET', }; mockArgumentsHost.switchToHttp().getRequest.mockReturnValue(incompleteRequest); const exception = new Error('Test error'); expect(() => filter.catch(exception, mockArgumentsHost)).not.toThrow(); const capturedContext = exceptionWatcherService.trackException.mock.calls[0][0]; expect(capturedContext.request?.method).toBe('GET'); expect(capturedContext.request?.url).toBeUndefined(); expect(capturedContext.request?.headers).toBeUndefined(); }); }); }); //# sourceMappingURL=exception-watcher.filter.spec.js.map