import { ArgumentsHost, HttpException, Logger } from '@nestjs/common'; import { ApiExceptionFilter } from '../src/common/errors/api-exception.filter'; import { AppError } from '../src/common/errors/app-error'; import { ERRORS } from '../src/common/errors/error-catalog'; import { classifyError } from '../src/common/errors/classify-error'; import { Prisma } from '../src/generated/prisma/client'; describe('safe error responses and logs', () => { beforeEach(() => { jest.spyOn(Logger.prototype, 'warn').mockImplementation(); jest.spyOn(Logger.prototype, 'error').mockImplementation(); }); afterEach(() => jest.restoreAllMocks()); function dispatch(error: unknown, headersSent = false) { const response = { locals: { requestId: 'server-generated-id' }, headersSent, setHeader: jest.fn(), status: jest.fn().mockReturnThis(), json: jest.fn(), }; const host = { switchToHttp: () => ({ getRequest: () => ({ method: 'POST', route: { path: '/products/:id' }, body: { password: 'SECRET' }, headers: { authorization: 'Bearer SECRET' }, originalUrl: '/products?token=SECRET', }), getResponse: () => response, }), } as unknown as ArgumentsHost; new ApiExceptionFilter().catch(error, host); return response; } it('returns stable codes and correlation IDs without leaking raw faults', () => { const response = dispatch(new Error('SQL password=SECRET token=SECRET')); expect(response.json).toHaveBeenCalledWith( expect.objectContaining({ code: 'INTERNAL_FAILURE', requestId: 'server-generated-id', statusCode: 500, }), ); const output = JSON.stringify([ response.json.mock.calls, jest.mocked(Logger.prototype.error).mock.calls, ]); expect(output).not.toContain('SECRET'); expect(output).not.toContain('password='); expect(output).toContain('faultId'); }); it('keeps validation field names and unique business error messages', () => { const response = dispatch( new AppError('REQUEST_INVALID', 'SCHEMA_REJECTED', ['price']), ); expect(response.json).toHaveBeenCalledWith( expect.objectContaining({ fields: ['price'], code: 'REQUEST_INVALID' }), ); const messages = Object.values(ERRORS).map((value) => value[1]); expect(new Set(messages).size).toBe(messages.length); }); it('does not try to write a second response', () => { expect( dispatch(new AppError('STOCK_INSUFFICIENT'), true).json, ).not.toHaveBeenCalled(); }); it('adds retry guidance for throttling', () => { expect( dispatch(new AppError('RATE_LIMITED')).setHeader, ).toHaveBeenCalledWith('Retry-After', '60'); }); it.each([ ['P2002', 'RECORD_CONFLICT'], ['P2003', 'REFERENCE_INVALID'], ['P2004', 'DATA_CONSTRAINT'], ['P2025', 'RECORD_NOT_FOUND'], ['P2028', 'DATABASE_BUSY'], ['P2034', 'TRANSACTION_CONFLICT'], ['P9999', 'INTERNAL_FAILURE'], ])('classifies database error %s as %s', (databaseCode, code) => { const error = new Prisma.PrismaClientKnownRequestError('SECRET', { code: databaseCode, clientVersion: 'test', }); expect(classifyError(error).code).toBe(code); }); it('classifies parser, transport and connection failures', () => { expect(classifyError({ type: 'entity.too.large' }).code).toBe( 'REQUEST_TOO_LARGE', ); expect(classifyError({ type: 'entity.parse.failed' }).code).toBe( 'REQUEST_MALFORMED', ); expect(classifyError(new HttpException('SECRET', 404)).code).toBe( 'ROUTE_NOT_FOUND', ); expect(classifyError(new HttpException('SECRET', 502)).code).toBe( 'INTERNAL_FAILURE', ); expect( classifyError( new Prisma.PrismaClientInitializationError('SECRET', 'test'), ).code, ).toBe('DATABASE_BUSY'); expect(classifyError(null).code).toBe('INTERNAL_FAILURE'); }); });