import { Logger } from '@nestjs/common'; import { Prisma } from '../../../generated/prisma'; import { PrismaService } from '../../prisma/prisma.service'; import { IdempotencyCleanupService } from './idempotency-cleanup.service'; describe('IdempotencyCleanupService', () => { let service: IdempotencyCleanupService; let prisma: { idempotencyRecord: { deleteMany: jest.Mock }; consumedToken: { deleteMany: jest.Mock }; }; beforeEach(() => { prisma = { idempotencyRecord: { deleteMany: jest.fn().mockResolvedValue({ count: 0 }), }, consumedToken: { deleteMany: jest.fn().mockResolvedValue({ count: 0 }) }, }; service = new IdempotencyCleanupService(prisma as unknown as PrismaService); }); afterEach(() => { jest.restoreAllMocks(); }); it('removes records older than the retention period and expired tokens', async () => { prisma.idempotencyRecord.deleteMany.mockResolvedValue({ count: 3 }); const now = new Date('2026-10-12T04:00:00.000Z'); await expect(service.removeExpired(now)).resolves.toBe(3); expect(prisma.idempotencyRecord.deleteMany).toHaveBeenCalledWith({ where: { createdAt: { lt: new Date('2026-07-14T04:00:00.000Z') } }, }); expect(prisma.consumedToken.deleteMany).toHaveBeenCalledWith({ where: { expiresAt: { lt: now } }, }); }); // The cron tick's own errors would otherwise reach the scheduler's logger // with the full Prisma message and stack. it('the daily tick never throws and logs a failure without the Prisma message', async () => { const errorSpy = jest .spyOn(Logger.prototype, 'error') .mockImplementation(() => undefined); prisma.idempotencyRecord.deleteMany.mockRejectedValue( new Prisma.PrismaClientKnownRequestError('key "Max Mustermann"', { code: 'P2024', clientVersion: '7.9.1', meta: { modelName: 'IdempotencyRecord' }, }), ); await expect(service.scheduledCleanup()).resolves.toBeUndefined(); const [message, stack] = errorSpy.mock.calls[0] as [string, unknown]; expect(message).toContain('P2024'); expect(message).not.toContain('Mustermann'); expect(stack).toBeUndefined(); }); });