import { BadRequestException, Logger } from '@nestjs/common'; import { ApplicationErrorLoggerService } from './application-error-logger.service'; import { sanitizeContext, maskEmail } from './application-error-sanitizer'; import { ApplicationErrorCategory, ApplicationErrorCode } from './application-error-codes'; describe('ApplicationErrorLoggerService', () => { const config = { get: jest.fn((key: string) => (key === 'NODE_ENV' ? 'test' : undefined)) }; function createService(save = jest.fn().mockResolvedValue(undefined)) { const repo = { create: jest.fn((value) => value), save, }; return { service: new ApplicationErrorLoggerService(repo as any, config as any), repo }; } it('stores normal Error objects with stack and type', async () => { const { service, repo } = createService(); const error = new Error('SMTP failed'); await service.log({ error, code: 'TEST_ERROR', category: ApplicationErrorCategory.EMAIL, handled: true }); expect(repo.save).toHaveBeenCalledWith(expect.objectContaining({ message: 'SMTP failed', errorType: 'Error', code: 'TEST_ERROR', handled: true })); expect(repo.save.mock.calls[0][0].stackTrace).toContain('Error: SMTP failed'); }); it('handles NestJS HTTP exceptions', async () => { const { service, repo } = createService(); await service.log({ error: new BadRequestException({ code: 'BAD_INPUT', message: 'Invalid data' }) }); expect(repo.save).toHaveBeenCalledWith(expect.objectContaining({ httpStatusCode: 400, code: 'BAD_INPUT', level: 'warning' })); }); it('handles unknown error values safely', async () => { const { service, repo } = createService(); await service.log({ error: { reason: 'broken' } }); expect(repo.save).toHaveBeenCalledWith(expect.objectContaining({ message: '{"reason":"broken"}', errorType: 'object' })); }); it('sanitizes nested sensitive fields and masks email addresses', () => { const circular: Record = { email: 'max.mustermann@example.com', nested: { token: 'secret', password: 'x' } }; circular.self = circular; expect(sanitizeContext(circular)).toEqual({ email: 'm***@example.com', nested: {}, self: '[Circular]', }); expect(maskEmail('maria@example.com')).toBe('m***@example.com'); }); it('stores request, user and tenant context', async () => { const { service, repo } = createService(); await service.log({ error: new Error('failed'), requestContext: { correlationId: 'corr-1', userId: 'user-1', tenantId: 'tenant-1', method: 'POST', path: '/api/test', }, }); expect(repo.save).toHaveBeenCalledWith(expect.objectContaining({ correlationId: 'corr-1', userId: 'user-1', tenantId: 'tenant-1' })); }); it('swallows database write errors', async () => { jest.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined); const { service } = createService(jest.fn().mockRejectedValue(new Error('db down'))); await expect(service.log({ error: new Error('original') })).resolves.toBeUndefined(); }); it('does not persist the same error twice', async () => { const { service, repo } = createService(); const error = new Error('only once'); await service.log({ error }); await service.log({ error }); expect(repo.save).toHaveBeenCalledTimes(1); }); it('can record background job failures', async () => { const { service, repo } = createService(); await service.log({ error: new Error('job failed'), category: ApplicationErrorCategory.BACKGROUND_JOB, code: ApplicationErrorCode.BACKGROUND_JOB_FAILED, operation: 'dailyImport', handled: true, }); expect(repo.save).toHaveBeenCalledWith(expect.objectContaining({ category: 'BACKGROUND_JOB', code: 'BACKGROUND_JOB_FAILED', operation: 'dailyImport' })); }); });