From 1770f683793b4ddf608af60f1bb13fe6ceac6b68 Mon Sep 17 00:00:00 2001 From: Toan Hoang Minh Date: Fri, 31 Jul 2026 12:38:32 +0700 Subject: [PATCH] test: add auth gating tests for /api/requests Mocks @/auth (vi.mock) instead of exercising the real auth(): that helper reads cookies via next/headers under the hood, which only works inside Next.js own request-handling machinery, not when these tests import and call GET()/POST() directly. beforeEach mocks a valid session by default so the existing business-logic tests are unaffected, and two new tests mock a null session to assert both GET and POST return 401 with { error: Unauthorized } and that POST does not create a row. middleware.ts itself has no automated test - it runs on the Edge Runtime and is not exercised by vitest calling route handlers directly. Verified manually instead via next build + next start: curl confirmed GET / redirects to /login (307), GET /login stays 200 with no redirect loop, GET/POST /api/requests return 401 JSON unauthenticated, and GET /api/auth/session (needed for the login flow) is not blocked. --- tests/requests.test.ts | 67 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) 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" }); + }); +});