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).mockResolvedValue(null); }); it("interdit l'upload au profil non administrateur", async () => { (sdk.authenticateRequest as ReturnType).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).mockResolvedValue({ id: 1, role: "admin" }); (pdfParse as ReturnType).mockResolvedValue({ text: EXTRACTED_TEXT }); (storagePut as ReturnType).mockResolvedValue({ key: "salaires/2026/05/liasse_opaque.enc" }); (createSalaireLiasse as ReturnType).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).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).mockResolvedValue({ id: 1, role: "admin" }); const plaintext = Buffer.from("%PDF-1.7 contenu de bulletin", "utf8"); const encrypted = encryptPayrollPdf(plaintext); (getSalaireLiasseById as ReturnType).mockResolvedValue({ id: 30001, stockageKey: "opaque", ivBase64: encrypted.ivBase64, authTagBase64: encrypted.authTagBase64, annee: 2026, mois: 5 }); (storageGetBuffer as ReturnType).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).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).mockResolvedValue({ id: 1, role: "admin" }); const encrypted = encryptPayrollPdf(Buffer.from("%PDF-1.7 contenu de bulletin", "utf8")); (getSalaireLiasseById as ReturnType).mockResolvedValue({ id: 30001, stockageKey: "opaque", ivBase64: encrypted.ivBase64, authTagBase64: encrypted.authTagBase64, annee: 2026, mois: 5 }); (storageGetBuffer as ReturnType).mockResolvedValue(encrypted.ciphertext); (pdfParse as ReturnType).mockResolvedValue({ text: EXTRACTED_TEXT }); (replaceSalaireLiasseIndex as ReturnType).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(); }); });