Initial commit : monorepo seiche (serveur Fastify + web Vite/React, Docker, config VS Code)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user