diff --git a/tests/requests.test.ts b/tests/requests.test.ts index 983e49e..24bbc63 100644 --- a/tests/requests.test.ts +++ b/tests/requests.test.ts @@ -1,6 +1,34 @@ -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { Mock } from "vitest"; +import type { Session } from "next-auth"; + +// The route handlers call auth() (src/auth.ts) directly to gate access (see +// src/app/api/requests/route.ts). That auth() reads cookies via +// next/headers under the hood, which only works inside Next.js's own +// request-handling machinery — not when these tests import and call +// GET()/POST() directly, bypassing Next entirely (the same reason +// middleware.ts itself isn't tested here; see CLAUDE.md's task notes). +// Mocking @/auth sidesteps next/headers altogether and lets each test +// control the session directly: a valid session by default (so the +// existing business-logic tests below are unaffected), and no session for +// the dedicated "returns 401" tests. +vi.mock("@/auth", () => ({ auth: vi.fn() })); + import { GET, POST } from "@/app/api/requests/route"; import { prisma } from "@/lib/prisma"; +import { auth } from "@/auth"; + +// auth's real type is next-auth's overloaded call signature (RSC helper, +// middleware wrapper, API-route form, ...) — vi.mocked() picks the wrong +// overload to infer mock*Value's parameter type from, so cast to the one +// shape actually used here: the no-arg session-helper call the route +// handlers make. +const mockedAuth = auth as unknown as Mock<() => Promise>; + +const testSession: Session = { + user: { id: "test-user-id" }, + expires: new Date(Date.now() + 60 * 60 * 1000).toISOString(), +}; function postRequest(body: unknown): Request { return new Request("http://localhost/api/requests", { @@ -14,6 +42,10 @@ beforeEach(async () => { // Each test starts from an empty table so listing/order assertions are // deterministic. await prisma.request.deleteMany(); + // Default to a logged-in session — the tests below exercise request + // business logic, not auth gating (that's covered by the "auth gating" + // describe block further down). + mockedAuth.mockResolvedValue(testSession); }); describe("POST /api/requests", () => { @@ -91,3 +123,36 @@ describe("GET /api/requests", () => { expect(body).toEqual([]); }); }); + +describe("auth gating on /api/requests", () => { + // Route-level coverage for the "logged in or not" gate added alongside + // middleware.ts (src/middleware.ts) — see that file's comment for why + // page routes are gated via middleware but API routes gate themselves. + // No role/permission logic here, per the flat permission model (any + // logged-in user can do anything) — this only checks session presence. + it("POST /api/requests returns 401 without a session", async () => { + mockedAuth.mockResolvedValue(null); + + const res = await POST( + postRequest({ employee_id: "emp-1", type: "asset", reason: "New laptop for onboarding" }) + ); + + expect(res.status).toBe(401); + const body = await res.json(); + expect(body).toEqual({ error: "Unauthorized" }); + + // And it must not have created anything. + const remaining = await prisma.request.findMany(); + expect(remaining).toHaveLength(0); + }); + + it("GET /api/requests returns 401 without a session", async () => { + mockedAuth.mockResolvedValue(null); + + const res = await GET(); + + expect(res.status).toBe(401); + const body = await res.json(); + expect(body).toEqual({ error: "Unauthorized" }); + }); +});