126 lines
4.3 KiB
TypeScript
126 lines
4.3 KiB
TypeScript
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);
|
|
});
|
|
});
|