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

33
server/rateLimit.test.ts Normal file
View File

@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import { getClientIp, SlidingWindowRateLimiter, type RateLimitRule } from "./rateLimit";
const shortRule: RateLimitRule = {
scope: "test",
maxAttempts: 2,
windowMs: 1_000,
};
describe("SlidingWindowRateLimiter", () => {
it("bloque après la limite et autorise à nouveau une fois la fenêtre expirée", () => {
const limiter = new SlidingWindowRateLimiter();
expect(limiter.consume(shortRule, "user-1", 1_000)).toMatchObject({ allowed: true, remaining: 1 });
expect(limiter.consume(shortRule, "user-1", 1_100)).toMatchObject({ allowed: true, remaining: 0 });
expect(limiter.consume(shortRule, "user-1", 1_200)).toMatchObject({ allowed: false, retryAfterMs: 800 });
// À 2 001 ms, la tentative de 1 100 ms est encore dans la fenêtre glissante.
expect(limiter.consume(shortRule, "user-1", 2_001)).toMatchObject({ allowed: true, remaining: 0 });
});
it("isole les compteurs de chaque clé", () => {
const limiter = new SlidingWindowRateLimiter();
limiter.consume(shortRule, "user-1", 1_000);
limiter.consume(shortRule, "user-1", 1_100);
expect(limiter.consume(shortRule, "user-2", 1_200)).toMatchObject({ allowed: true, remaining: 1 });
});
it("extrait uniquement la première adresse client transmise par le proxy", () => {
expect(getClientIp({ "x-forwarded-for": "203.0.113.8, 10.0.0.1" })).toBe("203.0.113.8");
expect(getClientIp({})).toBe("unknown-client");
});
});