import { beforeEach, describe, expect, it, vi } from "vitest"; import type { Mock } from "vitest"; import type { Session } from "next-auth"; // Mirrors tests/requests.test.ts's @/auth mocking pattern — see that file's // comment for why auth() (which reads cookies via next/headers) can't be // exercised directly when tests import and call GET()/POST() outside of // Next's own request-handling machinery. vi.mock("@/auth", () => ({ auth: vi.fn() })); import { GET, POST } from "@/app/api/employees/route"; import { prisma } from "@/lib/prisma"; import { auth } from "@/auth"; 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/employees", { 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.employee.deleteMany(); // Default to a logged-in session — the tests below exercise business // logic, not auth gating (that's covered by the "auth gating" describe // block further down). mockedAuth.mockResolvedValue(testSession); }); describe("POST /api/employees", () => { it("creates an employee with no manager on valid input", async () => { const res = await POST(postRequest({ name: "Alice Nguyen", department: "IT" })); expect(res.status).toBe(201); const body = await res.json(); expect(body).toMatchObject({ name: "Alice Nguyen", department: "IT", manager_id: null, }); 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("creates an employee with a valid managerId reference", async () => { const managerRes = await POST(postRequest({ name: "Bob Tran", department: "IT" })); const manager = await managerRes.json(); const res = await POST( postRequest({ name: "Alice Nguyen", department: "IT", managerId: manager.id }) ); expect(res.status).toBe(201); const body = await res.json(); expect(body.manager_id).toBe(manager.id); }); it("rejects an empty name", async () => { const res = await POST(postRequest({ name: "", department: "IT" })); expect(res.status).toBe(400); const body = await res.json(); expect(typeof body.error).toBe("string"); expect(body.error.length).toBeGreaterThan(0); }); it("rejects an empty department", async () => { const res = await POST(postRequest({ name: "Alice Nguyen", department: "" })); expect(res.status).toBe(400); const body = await res.json(); expect(typeof body.error).toBe("string"); expect(body.error.length).toBeGreaterThan(0); }); it("rejects a managerId that is not a valid UUID", async () => { const res = await POST( postRequest({ name: "Alice Nguyen", department: "IT", managerId: "not-a-uuid" }) ); expect(res.status).toBe(400); const body = await res.json(); expect(body.error).toMatch(/managerId/i); }); it("rejects a managerId that is a well-formed UUID but doesn't reference an existing employee", async () => { const res = await POST( postRequest({ name: "Alice Nguyen", department: "IT", managerId: "00000000-0000-0000-0000-000000000000", }) ); expect(res.status).toBe(400); const body = await res.json(); expect(body.error).toMatch(/managerId/i); expect(body.error).toMatch(/does not reference an existing employee/i); }); it("allows an employee to reference itself as manager (not disallowed by the data model)", async () => { // Two-step: create the row first (no self-FK possible on insert since // the id doesn't exist yet), then verify the FK accepts an update to // point at itself, directly via Prisma (no PATCH endpoint exists yet — // this is a data-model sanity check, not an API contract test). const created = await prisma.employee.create({ data: { name: "Self Manager", department: "IT" }, }); const updated = await prisma.employee.update({ where: { id: created.id }, data: { managerId: created.id }, }); expect(updated.managerId).toBe(created.id); }); }); describe("GET /api/employees", () => { it("lists created employees, newest first", async () => { await POST(postRequest({ name: "Alice Nguyen", department: "IT" })); // Ensure a distinct created_at ordering even on fast filesystems/clocks. await new Promise((resolve) => setTimeout(resolve, 5)); await POST(postRequest({ name: "Bob Tran", department: "Finance" })); 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({ name: "Bob Tran", department: "Finance" }); expect(body[1]).toMatchObject({ name: "Alice Nguyen", department: "IT" }); }); it("returns an empty list when there are no employees", async () => { const res = await GET(); expect(res.status).toBe(200); const body = await res.json(); expect(body).toEqual([]); }); }); describe("auth gating on /api/employees", () => { // Mirrors tests/requests.test.ts's "auth gating" block — only checks // session presence, per the flat permission model (no role/permission // logic). it("POST /api/employees returns 401 without a session", async () => { mockedAuth.mockResolvedValue(null); const res = await POST(postRequest({ name: "Alice Nguyen", department: "IT" })); 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.employee.findMany(); expect(remaining).toHaveLength(0); }); it("GET /api/employees 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" }); }); });