Files
ubi-dan-itam/tests/requests.test.ts
T
toanhoang 1770f68379 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.
2026-07-31 12:38:47 +07:00

159 lines
5.8 KiB
TypeScript

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<Session | null>>;
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", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}
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", () => {
it("creates a request in Draft status on valid input", async () => {
const res = await POST(
postRequest({ employee_id: "emp-1", type: "asset", reason: "New laptop for onboarding" })
);
expect(res.status).toBe(201);
const body = await res.json();
expect(body).toMatchObject({
employee_id: "emp-1",
type: "asset",
reason: "New laptop for onboarding",
status: "Draft",
});
expect(typeof body.id).toBe("string");
expect(body.id.length).toBeGreaterThan(0);
expect(typeof body.created_at).toBe("string");
expect(Number.isNaN(Date.parse(body.created_at))).toBe(false);
});
it("rejects requests with missing required fields", async () => {
const res = await POST(postRequest({ employee_id: "", type: "asset", reason: "" }));
expect(res.status).toBe(400);
const body = await res.json();
expect(typeof body.error).toBe("string");
expect(body.error.length).toBeGreaterThan(0);
});
it("rejects requests missing the reason field entirely", async () => {
const res = await POST(postRequest({ employee_id: "emp-1", type: "asset" }));
expect(res.status).toBe(400);
const body = await res.json();
expect(typeof body.error).toBe("string");
});
it("rejects an invalid type value", async () => {
const res = await POST(
postRequest({ employee_id: "emp-1", type: "not-a-real-type", reason: "Something" })
);
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toMatch(/type/i);
});
});
describe("GET /api/requests", () => {
it("lists created requests, newest first", async () => {
await POST(postRequest({ employee_id: "emp-1", type: "asset", reason: "First request" }));
// Ensure a distinct created_at ordering even on fast filesystems/clocks.
await new Promise((resolve) => setTimeout(resolve, 5));
await POST(postRequest({ employee_id: "emp-2", type: "license", reason: "Second request" }));
const res = await GET();
expect(res.status).toBe(200);
const body = await res.json();
expect(Array.isArray(body)).toBe(true);
expect(body).toHaveLength(2);
// newest first
expect(body[0]).toMatchObject({ employee_id: "emp-2", type: "license", status: "Draft" });
expect(body[1]).toMatchObject({ employee_id: "emp-1", type: "asset", status: "Draft" });
});
it("returns an empty list when there are no requests", async () => {
const res = await GET();
expect(res.status).toBe(200);
const body = await res.json();
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" });
});
});