Checkpoint: Fenêtre administrateur Salaires finalisée : import multipart limité, chiffrement AES-256-GCM avant stockage persistant hors conteneur, métadonnées/index métier minimisés en base, consultation PDF déchiffrée côté serveur avec no-store, filtres année/mois et comparaison M-1 expliquée. Réindexation sûre des archives partielles sans écrasement du PDF. Validation réelle sur deux liasses mensuelles : index partiel signalé sans invention de données, comparaison affichée et lecture après redémarrage. 55 tests Vitest, TypeScript et build de production validés.
Some checks failed
Validation applicative / TypeScript, tests et build (push) Failing after 1m54s

This commit is contained in:
Manus
2026-08-29 22:40:59 +00:00
parent 8a58f2c2bd
commit 82de3181df
29 changed files with 6179 additions and 1 deletions

View File

@@ -74,6 +74,7 @@ vi.mock("./db", async (importOriginal) => {
getMasseSalariale: vi.fn(),
importMasseSalariale: vi.fn(),
setMasseSalarialeEvolution: vi.fn(),
getSalairesMensuels: vi.fn(),
};
});
@@ -434,3 +435,72 @@ describe("masseSalariale", () => {
expect(db.setMasseSalarialeEvolution).not.toHaveBeenCalled();
});
});
// ─── Tests Salaires : sonde de chiffrement et confidentialité ─────────────────
describe("salaires.getSecurityStatus", () => {
it("valide la clé de chiffrement configurée via une procédure administrateur", async () => {
const { ctx } = createAuthCtx({ role: "admin" });
await expect(appRouter.createCaller(ctx).salaires.getSecurityStatus())
.resolves.toEqual({ ready: true, algorithm: "AES-256-GCM" });
});
it("interdit la sonde de sécurité aux profils non administrateurs", async () => {
const { ctx } = createAuthCtx({ role: "standard" });
await expect(appRouter.createCaller(ctx).salaires.getSecurityStatus()).rejects.toThrow();
});
});
describe("salaires.list", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("retourne l'index métier et explique la variation uniquement à l'administrateur", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
const mayLiasse = {
id: 9, annee: 2026, mois: 5, nomFichier: "mai.pdf", stockageKey: "secret-key",
empreinteSha256: "a".repeat(64), tailleOctets: 1200, ivBase64: "iv", authTagBase64: "tag",
versionCle: 1, statutExtraction: "ready", erreurExtraction: null, importePar: 1, importeLe: new Date(),
};
const juneLiasse = { ...mayLiasse, id: 10, mois: 6, nomFichier: "juin.pdf", stockageKey: "secret-key-2" };
const mayBulletin = {
id: 1, liasseId: 9, matricule: "000001", nom: "DUPONT", prenom: "TEST", poste: "Responsable",
brutMensuelCents: 300000, brutAvecPrimesCents: 320000, primeAstreinteCents: 20000,
explicationEcart: null, numeroPage: 1, createdAt: new Date(), updatedAt: new Date(),
};
const juneBulletin = { ...mayBulletin, id: 2, liasseId: 10, brutAvecPrimesCents: 350000, primeAstreinteCents: 40000 };
(db.getSalairesMensuels as ReturnType<typeof vi.fn>).mockResolvedValue({
liasses: [juneLiasse, mayLiasse],
rows: [{ liasse: juneLiasse, bulletin: juneBulletin }, { liasse: mayLiasse, bulletin: mayBulletin }],
});
const { ctx } = createAuthCtx({ role: "admin" });
const result = await appRouter.createCaller(ctx).salaires.list({ annee: 2026, mois: 6 });
expect(result.indicateurs).toEqual({ nombreSalaries: 1, totalBrutCents: 350000, totalEcartCents: 30000 });
expect(result.bulletins[0]).toMatchObject({ ecartBrutCents: 30000, brutPrecedentCents: 320000 });
expect(result.bulletins[0]?.explication).toContain("astreintes");
expect(JSON.stringify(result)).not.toContain("secret-key");
expect(JSON.stringify(result)).not.toContain("empreinteSha256");
expect(result.liasses[0]?.pdfPath).toBe("/api/salaires/liasses/10/pdf");
});
it("interdit l'index salarial aux profils standard et lecture seule", async () => {
const standard = createAuthCtx({ role: "standard" });
const readonly = createAuthCtx({ role: "readonly" });
await expect(appRouter.createCaller(standard.ctx).salaires.list({})).rejects.toThrow();
await expect(appRouter.createCaller(readonly.ctx).salaires.list({})).rejects.toThrow();
});
it("rejette un mois invalide avant tout accès aux données", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
const { ctx } = createAuthCtx({ role: "admin" });
await expect(appRouter.createCaller(ctx).salaires.list({ mois: 13 })).rejects.toThrow();
expect(db.getSalairesMensuels).not.toHaveBeenCalled();
});
});