mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
The integration suite runs with `vitest isolate: false` (one worker,
one container — migration cost demands it). Each test file declared
its own `vi.mock("next/headers", ...)` whose factory closed over a
file-local `cookieJar` Map. Vitest resolves a mock factory ONCE per
worker — whichever file loaded first won the registration, and the
other files' Map writes silently disappeared.
That made `admin-data-wipe.test.ts` flake intermittently (~12.5% per
empirical 8-run sample) when `idempotency-replay.test.ts` happened to
load first: its `vi.mock("@/lib/auth/session", ...)` then leaked,
returning a stub user without an admin role, so `requireAdmin()`
returned 401 and the wipe assertion failed with `expected 401 to be
200`.
Fix: extract cookieJar + headerJar into module-singleton Maps in
`tests/integration/mock-next-headers.ts`. All three integration files
import them and resolve them via dynamic-import inside the
`vi.mock("next/headers", ...)` factory body, so the factory always
sees the same backing Maps regardless of which file's registration
won the race. The `vi.mock("@/lib/auth/session", ...)` stub in
idempotency-replay was also removed; the test now seeds a real
Session row + cookie like the other two files.
After: 10/10 consecutive `pnpm test:integration` runs green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
/**
|
|
* Integration regression guard for `src/lib/auth/session.ts`.
|
|
*
|
|
* The session helpers couple Postgres (`Session` row), the `cookies()`
|
|
* adapter from `next/headers`, and the `db-compat` schema-migration
|
|
* fallback. A unit test with a mocked Prisma client cannot verify the
|
|
* end-to-end flow — particularly the expired-session purge behaviour,
|
|
* which silently deletes the row before returning null.
|
|
*/
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { cookieJar } from "./mock-next-headers";
|
|
import { getPrismaClient, truncateAllTables } from "./setup";
|
|
|
|
// Stateful in-memory cookie jar so `createSession()` (which calls
|
|
// cookies().set) hands the value off to a subsequent `getSession()`
|
|
// (which calls cookies().get), exactly as the request lifecycle does.
|
|
// The Map lives in `mock-next-headers.ts` so all integration files
|
|
// share the same backing store under vitest `isolate: false`.
|
|
vi.mock("next/headers", async () => {
|
|
const { cookieJar, headerJar } = await import("./mock-next-headers");
|
|
return {
|
|
headers: vi.fn(async () => ({
|
|
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
|
|
})),
|
|
cookies: vi.fn(async () => ({
|
|
get: (name: string) => {
|
|
const value = cookieJar.get(name);
|
|
return value ? { name, value } : undefined;
|
|
},
|
|
set: (name: string, value: string) => {
|
|
cookieJar.set(name, value);
|
|
},
|
|
delete: (name: string) => {
|
|
cookieJar.delete(name);
|
|
},
|
|
})),
|
|
};
|
|
});
|
|
|
|
// `db-compat` runs ALTER TABLE IF NOT EXISTS statements that are no-ops
|
|
// against a freshly-migrated schema but slow tests down — short-circuit.
|
|
vi.mock("@/lib/db-compat", () => ({
|
|
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
|
|
beforeEach(async () => {
|
|
await truncateAllTables(getPrismaClient());
|
|
cookieJar.clear();
|
|
});
|
|
|
|
describe("session lifecycle (real Postgres)", () => {
|
|
it("createSession writes a row and getSession reads it back", async () => {
|
|
const { createSession, getSession } = await import("@/lib/auth/session");
|
|
|
|
const user = await getPrismaClient().user.create({
|
|
data: { username: "session-test", email: "session@example.test" },
|
|
});
|
|
|
|
const sessionId = await createSession(user.id, "127.0.0.1", "vitest/1.0");
|
|
|
|
const row = await getPrismaClient().session.findUnique({
|
|
where: { id: sessionId },
|
|
});
|
|
expect(row).not.toBeNull();
|
|
expect(row?.userId).toBe(user.id);
|
|
expect(row?.ipAddress).toBe("127.0.0.1");
|
|
expect(row?.userAgent).toBe("vitest/1.0");
|
|
|
|
const result = await getSession();
|
|
expect(result).not.toBeNull();
|
|
expect(result?.user.id).toBe(user.id);
|
|
expect(result?.session.id).toBe(sessionId);
|
|
});
|
|
|
|
it("rejects expired sessions and purges the stale row", async () => {
|
|
const { createSession, getSession } = await import("@/lib/auth/session");
|
|
|
|
const user = await getPrismaClient().user.create({
|
|
data: { username: "expired-session", email: "expired@example.test" },
|
|
});
|
|
|
|
const sessionId = await createSession(user.id);
|
|
|
|
// Force the row past its expiry — getSession must purge it.
|
|
await getPrismaClient().session.update({
|
|
where: { id: sessionId },
|
|
data: { expiresAt: new Date(Date.now() - 60_000) },
|
|
});
|
|
|
|
const result = await getSession();
|
|
expect(result).toBeNull();
|
|
|
|
const purged = await getPrismaClient().session.findUnique({
|
|
where: { id: sessionId },
|
|
});
|
|
expect(purged).toBeNull();
|
|
});
|
|
|
|
it("returns null when no session cookie is present", async () => {
|
|
const { getSession } = await import("@/lib/auth/session");
|
|
expect(await getSession()).toBeNull();
|
|
});
|
|
});
|