xpeditis2.0/apps/backend/src/application/services/retention.service.spec.ts
2026-09-07 21:40:57 +02:00

99 lines
3.5 KiB
TypeScript

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);
});
});