Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
99 lines
3.5 KiB
TypeScript
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);
|
|
});
|
|
});
|