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

118
server/salairesHttp.test.ts Normal file
View File

@@ -0,0 +1,118 @@
import express from "express";
import request from "supertest";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("./_core/sdk", () => ({ sdk: { authenticateRequest: vi.fn() } }));
vi.mock("./db", () => ({
getSalaireLiasseByPeriod: vi.fn(),
createSalaireLiasse: vi.fn(),
getSalaireLiasseById: vi.fn(),
replaceSalaireLiasseIndex: vi.fn(),
}));
vi.mock("./storage", () => ({ storagePut: vi.fn(), storageGetBuffer: vi.fn() }));
vi.mock("pdf-parse/lib/pdf-parse.js", () => ({ default: vi.fn() }));
import { sdk } from "./_core/sdk";
import { createSalaireLiasse, getSalaireLiasseById, getSalaireLiasseByPeriod } from "./db";
import { encryptPayrollPdf } from "./payrollCrypto";
import { registerSalairesHttpRoutes } from "./salairesHttp";
import { storageGetBuffer, storagePut } from "./storage";
import pdfParse from "pdf-parse/lib/pdf-parse.js";
const EXTRACTED_TEXT = `##BULLETIN##05-2026##009999##DUPONT##TEST##
Matricule : 009999
Emploi : RESPONSABLE APPLICATIF Monsieur TEST DUPONT
Salaire de base 3 650.00
Salaire brut 3 650.00`;
function makeApp() {
const app = express();
registerSalairesHttpRoutes(app);
return app;
}
describe("routes HTTP Salaires", () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.PAYROLL_ENCRYPTION_KEY = Buffer.alloc(32, 8).toString("base64");
(getSalaireLiasseByPeriod as ReturnType<typeof vi.fn>).mockResolvedValue(null);
});
it("interdit l'upload au profil non administrateur", async () => {
(sdk.authenticateRequest as ReturnType<typeof vi.fn>).mockResolvedValue({ id: 2, role: "standard" });
await request(makeApp())
.post("/api/salaires/liasses")
.field("annee", "2026")
.field("mois", "5")
.attach("file", Buffer.from("%PDF-1.7"), { filename: "mai.pdf", contentType: "application/pdf" })
.expect(403);
expect(createSalaireLiasse).not.toHaveBeenCalled();
expect(storagePut).not.toHaveBeenCalled();
});
it("chiffre et archive une liasse administrateur sans persister de donnée sensible", async () => {
(sdk.authenticateRequest as ReturnType<typeof vi.fn>).mockResolvedValue({ id: 1, role: "admin" });
(pdfParse as ReturnType<typeof vi.fn>).mockResolvedValue({ text: EXTRACTED_TEXT });
(storagePut as ReturnType<typeof vi.fn>).mockResolvedValue({ key: "salaires/2026/05/liasse_opaque.enc" });
(createSalaireLiasse as ReturnType<typeof vi.fn>).mockResolvedValue({ liasseId: 12, bulletins: 1 });
const response = await request(makeApp())
.post("/api/salaires/liasses")
.field("annee", "2026")
.field("mois", "5")
.attach("file", Buffer.from("%PDF-1.7 document de test"), { filename: "mai.pdf", contentType: "application/pdf" })
.expect(201);
expect(response.body).toMatchObject({ id: 12, bulletins: 1, statutExtraction: "ready" });
expect(createSalaireLiasse).toHaveBeenCalledWith(expect.objectContaining({
annee: 2026,
mois: 5,
importePar: 1,
stockageKey: "salaires/2026/05/liasse_opaque.enc",
bulletins: [expect.objectContaining({ matricule: "009999", brutMensuelCents: 365000 })],
}));
expect(JSON.stringify((createSalaireLiasse as ReturnType<typeof vi.fn>).mock.calls[0]?.[0])).not.toContain("N° SS");
});
it("sert un PDF déchiffré uniquement à un administrateur et sans mise en cache", async () => {
(sdk.authenticateRequest as ReturnType<typeof vi.fn>).mockResolvedValue({ id: 1, role: "admin" });
const plaintext = Buffer.from("%PDF-1.7 contenu de bulletin", "utf8");
const encrypted = encryptPayrollPdf(plaintext);
(getSalaireLiasseById as ReturnType<typeof vi.fn>).mockResolvedValue({ id: 30001, stockageKey: "opaque", ivBase64: encrypted.ivBase64, authTagBase64: encrypted.authTagBase64, annee: 2026, mois: 5 });
(storageGetBuffer as ReturnType<typeof vi.fn>).mockResolvedValue(encrypted.ciphertext);
const response = await request(makeApp()).get("/api/salaires/liasses/30001/pdf").expect(200);
expect(response.headers["cache-control"]).toContain("no-store");
expect(response.headers["x-content-type-options"]).toBe("nosniff");
expect(response.headers["content-type"]).toContain("application/pdf");
expect(response.body).toEqual(plaintext);
});
it("interdit la consultation d'une liasse au profil lecture seule", async () => {
(sdk.authenticateRequest as ReturnType<typeof vi.fn>).mockResolvedValue({ id: 3, role: "readonly" });
await request(makeApp()).get("/api/salaires/liasses/12/pdf").expect(403);
expect(getSalaireLiasseById).not.toHaveBeenCalled();
});
it("réindexe une liasse archivée sans téléverser ni remplacer son PDF", async () => {
const { replaceSalaireLiasseIndex } = await import("./db");
(sdk.authenticateRequest as ReturnType<typeof vi.fn>).mockResolvedValue({ id: 1, role: "admin" });
const encrypted = encryptPayrollPdf(Buffer.from("%PDF-1.7 contenu de bulletin", "utf8"));
(getSalaireLiasseById as ReturnType<typeof vi.fn>).mockResolvedValue({ id: 30001, stockageKey: "opaque", ivBase64: encrypted.ivBase64, authTagBase64: encrypted.authTagBase64, annee: 2026, mois: 5 });
(storageGetBuffer as ReturnType<typeof vi.fn>).mockResolvedValue(encrypted.ciphertext);
(pdfParse as ReturnType<typeof vi.fn>).mockResolvedValue({ text: EXTRACTED_TEXT });
(replaceSalaireLiasseIndex as ReturnType<typeof vi.fn>).mockResolvedValue({ bulletins: 1 });
await request(makeApp()).post("/api/salaires/liasses/30001/reindex").expect(200);
expect(replaceSalaireLiasseIndex).toHaveBeenCalledWith(30001, expect.objectContaining({
statutExtraction: "ready",
bulletins: [expect.objectContaining({ matricule: "009999" })],
}));
expect(storagePut).not.toHaveBeenCalled();
});
});