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); }); });