import { Controller, Get, HttpException, HttpStatus, INestApplication, } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { Test } from '@nestjs/testing'; import { PinoLogger } from 'nestjs-pino'; import * as request from 'supertest'; import { lastValueFrom, of, throwError } from 'rxjs'; import { ApiRequestLogInterceptor, LokiLogPublisherService, ToolsService, } from '../../src/common'; @Controller('probe') class ProbeController { @Get() probe() { return { ok: true }; } } function createHttpContext(request: Record, response: Record) { return { getType: () => 'http', switchToHttp: () => ({ getRequest: () => request, getResponse: () => response, }), } as any; } function createResponse(statusCode = 200) { return { getHeader: jest.fn(), setHeader: jest.fn(), statusCode, }; } function createLokiLogPublisherMock() { return { pushHttpRequestLog: jest.fn().mockResolvedValue(undefined), }; } describe('ApiRequestLogInterceptor', () => { it('writes structured HTTP request fields for successful controller calls', async () => { const logger = { error: jest.fn(), info: jest.fn(), setContext: jest.fn(), warn: jest.fn(), }; const lokiLogPublisher = createLokiLogPublisherMock(); const interceptor = new ApiRequestLogInterceptor( logger as any, lokiLogPublisher as any, new ToolsService(), ); const request: Record = { headers: { 'x-request-id': 'req-1', }, method: 'GET', originalUrl: '/status?source=test', }; const response = createResponse(200); await lastValueFrom( interceptor.intercept(createHttpContext(request, response), { handle: () => of({ ok: true }), }), ); expect(response.setHeader).toHaveBeenCalledWith('x-request-id', 'req-1'); expect(request.id).toBe('req-1'); expect(logger.info).toHaveBeenCalledWith( expect.objectContaining({ method: 'GET', path: '/status', requestId: 'req-1', statusCode: 200, }), 'HTTP request completed', ); expect(lokiLogPublisher.pushHttpRequestLog).toHaveBeenCalledWith( expect.objectContaining({ context: ApiRequestLogInterceptor.name, level: 'info', message: 'HTTP request completed', payload: expect.objectContaining({ method: 'GET', path: '/status', requestId: 'req-1', statusCode: 200, }), }), ); }); it('does not publish system log query requests back to Loki', async () => { const logger = { error: jest.fn(), info: jest.fn(), setContext: jest.fn(), warn: jest.fn(), }; const lokiLogPublisher = createLokiLogPublisherMock(); const interceptor = new ApiRequestLogInterceptor( logger as any, lokiLogPublisher as any, new ToolsService(), ); const request: Record = { headers: { 'x-request-id': 'req-system-log', }, method: 'GET', originalUrl: '/system/logs?pageNo=1', }; const response = createResponse(200); await lastValueFrom( interceptor.intercept(createHttpContext(request, response), { handle: () => of({ ok: true }), }), ); expect(logger.info).toHaveBeenCalledWith( expect.objectContaining({ method: 'GET', path: '/system/logs', requestId: 'req-system-log', statusCode: 200, }), 'HTTP request completed', ); expect(lokiLogPublisher.pushHttpRequestLog).not.toHaveBeenCalled(); }); it('writes warning logs with HTTP status for controller errors', async () => { const logger = { error: jest.fn(), info: jest.fn(), setContext: jest.fn(), warn: jest.fn(), }; const lokiLogPublisher = createLokiLogPublisherMock(); const interceptor = new ApiRequestLogInterceptor( logger as any, lokiLogPublisher as any, new ToolsService(), ); const request: Record = { headers: {}, method: 'POST', path: '/dict/save', }; const response = createResponse(200); const error = new HttpException('not found', HttpStatus.NOT_FOUND); await expect( lastValueFrom( interceptor.intercept(createHttpContext(request, response), { handle: () => throwError(() => error), }), ), ).rejects.toBe(error); expect(request.id).toEqual(expect.any(String)); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ method: 'POST', path: '/dict/save', requestId: request.id, statusCode: 404, }), 'HTTP request completed', ); expect(lokiLogPublisher.pushHttpRequestLog).toHaveBeenCalledWith( expect.objectContaining({ level: 'warning', payload: expect.objectContaining({ method: 'POST', path: '/dict/save', requestId: request.id, statusCode: 404, }), }), ); }); it('captures a real local HTTP request in a Nest application', async () => { const logger = { error: jest.fn(), info: jest.fn(), setContext: jest.fn(), warn: jest.fn(), }; let app: INestApplication | undefined; try { const moduleRef = await Test.createTestingModule({ controllers: [ProbeController], providers: [ ToolsService, { provide: PinoLogger, useValue: logger, }, { provide: LokiLogPublisherService, useValue: createLokiLogPublisherMock(), }, { provide: APP_INTERCEPTOR, useClass: ApiRequestLogInterceptor, }, ], }).compile(); app = moduleRef.createNestApplication(); await app.init(); await request(app.getHttpServer()) .get('/probe?source=test') .set('x-request-id', 'req-real') .expect(200); expect(logger.info).toHaveBeenCalledWith( expect.objectContaining({ method: 'GET', path: '/probe', requestId: 'req-real', statusCode: 200, }), 'HTTP request completed', ); } finally { await app?.close(); } }); });