import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import { apiLogger, redactSecrets } from './apiLogger'; describe('redactSecrets', () => { it('hides OTPs, PINs, encrypted payloads and every kind of token', () => { expect( redactSecrets({ otp: '123456', debugOtp: '123456', mpin: '2580', tpin: '5826', encryptedMpin: 'base64…', encryptedTpin: 'base64…', sessionToken: 'live-session', setupToken: 'setup', resetToken: 'mpin-reset', tpinCreateToken: 'create', tpinResetToken: 'reset', }), ).toEqual({ otp: '', debugOtp: '', mpin: '', tpin: '', encryptedMpin: '', encryptedTpin: '', sessionToken: '', setupToken: '', resetToken: '', tpinCreateToken: '', tpinResetToken: '', }); }); it('keeps the fields that help debugging — expiries, statuses, ids', () => { const data = { tpinResetId: 'e79eb452-32c9-4690-955b-81d8bb223ec3', status: 'PENDING_OTP', otpExpiresAt: '2026-09-30T13:50:31+05:30', tokenExpiresAt: '2026-09-30T13:51:02+05:30', resetTokenExpiresAt: '2026-09-30T13:51:02+05:30', attemptsRemaining: 3, mpinStatus: 'ACTIVE', }; expect(redactSecrets(data)).toEqual(data); }); it('reaches into the envelope and into arrays', () => { expect( redactSecrets({ success: true, data: { tpinResetToken: 'reset', items: [{ otp: '1' }, { name: 'x' }] }, }), ).toEqual({ success: true, data: { tpinResetToken: '', items: [{ otp: '' }, { name: 'x' }] }, }); }); it('leaves an absent secret visibly absent, and non-objects alone', () => { expect(redactSecrets({ debugOtp: null })).toEqual({ debugOtp: null }); expect(redactSecrets('(no body)')).toBe('(no body)'); expect(redactSecrets(undefined)).toBeUndefined(); }); }); describe('apiLogger', () => { let log: jest.SpyInstance; let warn: jest.SpyInstance; beforeEach(() => { log = jest.spyOn(console, 'log').mockImplementation(() => undefined); warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); }); afterEach(() => { log.mockRestore(); warn.mockRestore(); }); const config = (data: unknown) => ({ method: 'post', baseURL: 'https://bank.example/api/v1', url: '/tpin/reset/confirm', headers: { Authorization: 'Bearer live-session-token', 'Content-Type': 'application/json' }, data, }) as unknown as InternalAxiosRequestConfig; it('prints the curl without the session token or the secrets in the body', () => { apiLogger.request(config({ tpinResetToken: 'one-time-reset-token', encryptedTpin: 'cipher' })); const printed = log.mock.calls.map((call) => String(call[0])).join('\n'); expect(printed).toContain('/tpin/reset/confirm'); expect(printed).toContain('Authorization: Bearer '); expect(printed).not.toContain('live-session-token'); expect(printed).not.toContain('one-time-reset-token'); expect(printed).not.toContain('cipher'); }); it('redacts a body the caller serialised themselves', () => { apiLogger.request(config(JSON.stringify({ tpinResetId: 'id-1', otp: '654321' }))); const printed = log.mock.calls.map((call) => String(call[0])).join('\n'); expect(printed).toContain('id-1'); expect(printed).not.toContain('654321'); }); it('redacts secrets in what came back, success or failure', () => { const request = config({}); apiLogger.response({ status: 200, config: request, data: { success: true, data: { tpinResetToken: 'one-time-reset-token' } }, } as AxiosResponse); apiLogger.failure({ status: 400, config: request, data: { success: false, data: { otp: '654321' }, errorCode: 'VALIDATION_ERROR' }, } as AxiosResponse); const printed = [...log.mock.calls, ...warn.mock.calls] .map((call) => String(call[0])) .join('\n'); expect(printed).toContain('VALIDATION_ERROR'); expect(printed).not.toContain('one-time-reset-token'); expect(printed).not.toContain('654321'); }); });