From 7779dabfedc9ec37973e19aecb16ad1cd36c11a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 23 Jul 2026 15:16:48 +0200 Subject: [PATCH] test(oidc): cover the session resolver behind the duplicate-callback path Integration coverage for `validateSessionFromCookieValue` against real Postgres: a live secret-backed session resolves to its user, an expired one and an unknown/forged/empty value return null, and a legacy cuid cookie resolves only while its row still carries no hashed secret. This is the hashing + row lookup + expiry the callback's duplicate detection turns on, which the unit suite stubs out. Refs #600 --- .../session-cookie-validation.test.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/integration/session-cookie-validation.test.ts diff --git a/tests/integration/session-cookie-validation.test.ts b/tests/integration/session-cookie-validation.test.ts new file mode 100644 index 000000000..65139d918 --- /dev/null +++ b/tests/integration/session-cookie-validation.test.ts @@ -0,0 +1,84 @@ +/** + * Integration guard for `validateSessionFromCookieValue` against a real + * Postgres. + * + * The OIDC callback's duplicate-detection path relies on this helper to tell a + * duplicate of a completed sign-in (a live session → redirect into the app) + * apart from a forged/expired callback (no session → fail closed). The unit + * suite stubs the resolver out, so the hashing + row lookup + expiry that + * actually decide that branch are only exercised here. + */ +import { beforeEach, describe, expect, it } from "vitest"; + +process.env.ENCRYPTION_KEY ??= + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +import { getPrismaClient, truncateAllTables } from "./setup"; +import { hashToken } from "@/lib/auth/hmac"; +import { validateSessionFromCookieValue } from "@/lib/auth/session"; + +const prisma = getPrismaClient(); + +beforeEach(async () => { + await truncateAllTables(prisma); +}); + +async function makeUser(username: string) { + return prisma.user.create({ + data: { username, email: `${username}@example.test`, role: "USER" }, + }); +} + +describe("validateSessionFromCookieValue (integration)", () => { + it("resolves the user for a live secret-backed session", async () => { + const user = await makeUser("oidc-dup-live"); + const secret = "hls_" + "a".repeat(64); + await prisma.session.create({ + data: { + userId: user.id, + tokenHash: hashToken(secret), + expiresAt: new Date(Date.now() + 60_000), + }, + }); + + const resolved = await validateSessionFromCookieValue(secret); + expect(resolved?.user.id).toBe(user.id); + }); + + it("returns null for an expired secret-backed session (fail closed)", async () => { + const user = await makeUser("oidc-dup-expired"); + const secret = "hls_" + "b".repeat(64); + await prisma.session.create({ + data: { + userId: user.id, + tokenHash: hashToken(secret), + expiresAt: new Date(Date.now() - 1_000), + }, + }); + + expect(await validateSessionFromCookieValue(secret)).toBeNull(); + }); + + it("returns null for an unknown / forged / empty cookie value", async () => { + expect( + await validateSessionFromCookieValue("hls_" + "c".repeat(64)), + ).toBeNull(); + expect(await validateSessionFromCookieValue(undefined)).toBeNull(); + expect(await validateSessionFromCookieValue("")).toBeNull(); + }); + + it("resolves a legacy cuid-cookie session while its tokenHash is null", async () => { + const user = await makeUser("oidc-dup-legacy"); + const legacy = await prisma.session.create({ + data: { + userId: user.id, + expiresAt: new Date(Date.now() + 60_000), + }, + }); + + // The pre-upgrade cookie carries the row id; it is honoured only while the + // row has not yet retired its id in favour of a hashed secret. + const resolved = await validateSessionFromCookieValue(legacy.id); + expect(resolved?.user.id).toBe(user.id); + }); +});