Initial commit : monorepo seiche (serveur Fastify + web Vite/React, Docker, config VS Code)

This commit is contained in:
bot-claude
2026-08-25 09:55:13 +02:00
commit 5174c37a31
61 changed files with 7117 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
import { createHash, randomBytes } from "node:crypto";
import { eq } from "drizzle-orm";
import { beforeAll, describe, expect, it } from "vitest";
import { SessionManager } from "../src/auth/session";
import type { Database } from "../src/db/client";
import { sessions, users } from "../src/db/schema";
import { makeConfig, openTestDb } from "./helpers";
function sha256Hex(value: string): string {
return createHash("sha256").update(value).digest("hex");
}
function newToken(): string {
return randomBytes(32).toString("hex");
}
describe("SessionManager", () => {
let db: Database;
let manager: SessionManager;
let userId: number;
beforeAll(async () => {
db = await openTestDb();
manager = new SessionManager(db, makeConfig());
const inserted = await db
.insert(users)
.values({ username: "alice", passwordHash: "x" })
.returning();
const user = inserted[0];
if (user === undefined) throw new Error("utilisateur de test non inséré");
userId = user.id;
});
it("create : token + CSRF 256 bits, getValid renvoie le contexte", async () => {
const { token, csrfToken } = await manager.create(userId);
expect(token).toMatch(/^[a-f0-9]{64}$/);
expect(csrfToken).toMatch(/^[a-f0-9]{64}$/);
const ctx = await manager.getValid(token);
expect(ctx?.userId).toBe(userId);
expect(ctx?.username).toBe("alice");
expect(ctx?.sessionExpiresAt).toBeInstanceOf(Date);
});
it("getValid : token absent ou inconnu → null", async () => {
expect(await manager.getValid(undefined)).toBeNull();
expect(await manager.getValid(newToken())).toBeNull();
});
it("le token est stocké haché (pas en clair)", async () => {
const { token } = await manager.create(userId);
const rows = await db
.select()
.from(sessions)
.where(eq(sessions.tokenHash, sha256Hex(token)));
expect(rows).toHaveLength(1);
expect(rows[0]?.tokenHash).not.toBe(token);
});
it("TTL glissant : expiration = maintenant + ttl", async () => {
const { token } = await manager.create(userId);
const before = Date.now();
const ctx = await manager.getValid(token);
expect(ctx?.sessionExpiresAt.getTime()).toBeGreaterThanOrEqual(before + 8 * 3600 * 1000 - 2000);
expect(ctx?.sessionExpiresAt.getTime()).toBeLessThanOrEqual(before + 8 * 3600 * 1000 + 5000);
});
it("TTL glissant borné par l'expiration absolue", async () => {
const m = new SessionManager(
db,
makeConfig({ sessionTtlMs: 3_600_000, sessionAbsoluteTtlMs: 300_000 }),
);
const { token } = await m.create(userId);
const before = Date.now();
const ctx = await m.getValid(token);
expect(ctx?.sessionExpiresAt.getTime()).toBeGreaterThanOrEqual(before + 300_000 - 1_000);
expect(ctx?.sessionExpiresAt.getTime()).toBeLessThanOrEqual(before + 300_000 + 1_000);
});
it("expiration absolue dépassée → null + ligne supprimée", async () => {
const token = newToken();
const past = new Date(Date.now() - 1000);
await db.insert(sessions).values({
tokenHash: sha256Hex(token),
userId,
createdAt: past,
expiresAt: past,
absoluteExpiryAt: past,
lastActivityAt: past,
});
expect(await manager.getValid(token)).toBeNull();
const rows = await db
.select()
.from(sessions)
.where(eq(sessions.tokenHash, sha256Hex(token)));
expect(rows).toHaveLength(0);
});
it("destroy invalidate le token", async () => {
const { token } = await manager.create(userId);
await manager.destroy(token);
expect(await manager.getValid(token)).toBeNull();
});
it("5 sessions max par utilisateur (exactement 5 des 6 restent valides)", async () => {
const inserted = await db
.insert(users)
.values({ username: "bob", passwordHash: "x" })
.returning();
const bob = inserted[0];
if (bob === undefined) throw new Error("utilisateur bob non inséré");
const m2 = new SessionManager(db, makeConfig());
const tokens: string[] = [];
for (let i = 0; i < 6; i += 1) {
tokens.push((await m2.create(bob.id)).token);
}
const rows = await db.select().from(sessions).where(eq(sessions.userId, bob.id));
expect(rows).toHaveLength(5);
let validCount = 0;
for (const t of tokens) {
const ctx = await m2.getValid(t);
if (ctx !== null) validCount += 1;
}
expect(validCount).toBe(5);
});
});