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
Some checks failed
Validation applicative / TypeScript, tests et build (push) Failing after 1m54s
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user