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:
118
server/salairesHttp.test.ts
Normal file
118
server/salairesHttp.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user