import { ConfigService } from '@nestjs/config'; import { DataSource } from 'typeorm'; import { RetentionService } from './retention.service'; import { AuditService } from './audit.service'; import { AuditAction } from '@domain/entities/audit-log.entity'; import { RETENTION_RULES, purgeableRules } from '@domain/services/data-retention'; /** * La purge supprime définitivement des lignes. Ces tests portent sur ce qu'elle * touche, et surtout sur ce qu'elle doit épargner. */ const ROWS_REMOVED = 4; function buildService(enabled = true) { const executed: { sql: string; parameters: unknown[] }[] = []; const dataSource = { query: jest.fn(async (sql: string, parameters: unknown[]) => { executed.push({ sql, parameters }); return sql.trimStart().startsWith('SELECT') ? [{ expired: ROWS_REMOVED, oldest: '2020-01-01T00:00:00.000Z' }] : [[], ROWS_REMOVED]; }), } as unknown as DataSource; const config = { get: jest.fn(() => (enabled ? 'true' : 'false')), } as unknown as ConfigService; const audit = { log: jest.fn(async () => undefined) } as unknown as AuditService; return { service: new RetentionService(dataSource, config, audit), executed, audit }; } describe('RetentionService', () => { it("n'applique un délai qu'aux tables qui en ont un", async () => { const { service, executed } = buildService(); await service.purge(); const purgeable = purgeableRules().map(rule => rule.table); expect(executed).toHaveLength(purgeable.length); for (const rule of RETENTION_RULES) { const touched = executed.some(query => query.sql.includes(rule.table)); // Une durée liée à la vie du compte n'a pas de point de départ en base : // elle est traitée par l'effacement, pas par la purge. expect(touched).toBe(purgeable.includes(rule.table)); } }); it("épargne les traces de traitement des demandes de droits", async () => { const { service, executed } = buildService(); await service.purge(); const auditPurge = executed.find(query => query.sql.includes('audit_logs')); // Sans cette clause, la purge des journaux à douze mois effacerait la // preuve, exigée par l'art. 5.2, qu'un effacement a été honoré. expect(auditPurge?.sql).toContain("action NOT LIKE 'gdpr\\_%'"); }); it('journalise ce qui a été supprimé', async () => { const { service, audit } = buildService(); const report = await service.purge(); expect(report.lines.every(line => line.expired === ROWS_REMOVED)).toBe(true); const [entry] = (audit.log as jest.Mock).mock.calls[0]; expect(entry.action).toBe(AuditAction.GDPR_RETENTION_PURGE); }); it("ne supprime rien quand la purge automatique n'est pas activée", async () => { const { service, executed } = buildService(false); await service.scheduledPurge(); expect(executed).toHaveLength(0); }); it('ne compte pas la forme du résultat à la place des lignes', async () => { const { service } = buildService(); const report = await service.purge(); // Le pilote renvoie `[lignes, nombre]` : mesurer la longueur donnerait 2. expect(report.lines.map(line => line.expired)).not.toContain(2); }); it('signale ce qui serait supprimé sans rien supprimer', async () => { const { service, executed } = buildService(); const report = await service.preview(); expect(executed.every(query => query.sql.trimStart().startsWith('SELECT'))).toBe(true); expect(report.lines.every(line => line.expired === ROWS_REMOVED)).toBe(true); }); });