Checkpoint: Pagination serveur des établissements et des messages, tri serveur, navigation utilisateur, limitation de débit sur connexion locale, messages et demandes, avec 46 tests Vitest, typage TypeScript et build validés.

This commit is contained in:
Manus
2026-08-18 07:34:59 +00:00
parent 7c97bec6c1
commit 9d9ae68b18
8 changed files with 429 additions and 51 deletions

View File

@@ -1,8 +1,9 @@
import { describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it } from "vitest";
import { appRouter } from "./routers";
import { COOKIE_NAME } from "../shared/const";
import type { TrpcContext } from "./_core/context";
import type { User } from "../drizzle/schema";
import { CHANNEL_MESSAGE_RULE, rateLimiter } from "./rateLimit";
// ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -41,6 +42,19 @@ function makeCtx(user: User | null = null): TrpcContext {
};
}
beforeEach(() => {
// Les limites sont intentionnellement globales à l'instance : isoler chaque scénario de test.
rateLimiter.clear();
});
async function expectNotRateLimited(operation: Promise<unknown>) {
try {
await operation;
} catch (error) {
expect(error).not.toMatchObject({ code: "TOO_MANY_REQUESTS" });
}
}
// ─── Tests : auth.me ──────────────────────────────────────────────────────────
describe("auth.me", () => {
@@ -287,3 +301,68 @@ describe("validation des contenus libres", () => {
).rejects.toMatchObject({ code: "BAD_REQUEST" });
});
});
// ─── Tests : contrats de pagination ───────────────────────────────────────────
describe("contrats de pagination", () => {
it("rejette une page d'établissements invalide avant tout accès aux données", async () => {
const caller = appRouter.createCaller(makeCtx(makeUser()));
await expect(
caller.etablissements.search({ page: 0, pageSize: 25, sortBy: "nom", sortDirection: "asc" })
).rejects.toMatchObject({ code: "BAD_REQUEST" });
});
it("rejette une taille de page de messages hors borne", async () => {
const caller = appRouter.createCaller(makeCtx(makeUser()));
await expect(
caller.canaux.messages({ canalId: 1, page: 1, pageSize: 101 })
).rejects.toMatchObject({ code: "BAD_REQUEST" });
});
});
// ─── Tests : application tRPC des limites de débit ─────────────────────────────
describe("limitation de débit des actions sensibles", () => {
it("bloque la sixième tentative de connexion locale pour le même identifiant", async () => {
const caller = appRouter.createCaller(makeCtx(null));
const credentials = { email: "brute-force@test.fr", password: "mot-de-passe-invalide" };
for (let index = 0; index < 5; index += 1) {
await expect(caller.auth.loginLocal(credentials)).rejects.toMatchObject({ code: "UNAUTHORIZED" });
}
await expect(caller.auth.loginLocal(credentials)).rejects.toMatchObject({ code: "TOO_MANY_REQUESTS" });
});
it("bloque les demandes de contact répétées", async () => {
const caller = appRouter.createCaller(makeCtx(makeUser()));
const input = { etablissementCibleId: 999_999, message: "Message de test" };
for (let index = 0; index < 5; index += 1) {
await expectNotRateLimited(caller.contact.envoyer(input));
}
await expect(caller.contact.envoyer(input)).rejects.toMatchObject({ code: "TOO_MANY_REQUESTS" });
});
it("bloque les demandes de mise en relation répétées", async () => {
const caller = appRouter.createCaller(makeCtx(makeUser()));
const input = { sujet: "Demande de test", message: "Message de test" };
for (let index = 0; index < 5; index += 1) {
await expectNotRateLimited(caller.miseEnRelation.soumettre(input));
}
await expect(caller.miseEnRelation.soumettre(input)).rejects.toMatchObject({ code: "TOO_MANY_REQUESTS" });
});
it("applique la limite de messagerie à la procédure tRPC", async () => {
const caller = appRouter.createCaller(makeCtx(makeUser({ sonumRole: "gestionnaire" })));
const input = { canalId: 1, contenu: "Message de test" };
// Préremplit la fenêtre : le test vérifie que la procédure utilise bien cette règle et cette clé utilisateur.
for (let index = 0; index < 30; index += 1) {
rateLimiter.consume(CHANNEL_MESSAGE_RULE, "1");
}
await expect(caller.canaux.sendMessage(input)).rejects.toMatchObject({ code: "TOO_MANY_REQUESTS" });
});
});