import { beforeEach, describe, expect, it, vi } from "vitest"; import type { Mock } from "vitest"; import type { Session } from "next-auth"; // Mirrors tests/employees.test.ts's @/auth mocking pattern. vi.mock("@/auth", () => ({ auth: vi.fn() })); import { GET, POST } from "@/app/api/areas/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/areas", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); } beforeEach(async () => { await prisma.area.deleteMany(); mockedAuth.mockResolvedValue(testSession); }); describe("POST /api/areas", () => { it("creates an area with no team on valid input", async () => { const res = await POST(postRequest({ name: "Phong hop A" })); expect(res.status).toBe(201); const body = await res.json(); expect(body).toMatchObject({ name: "Phong hop A", team: null }); expect(typeof body.id).toBe("string"); }); it("creates an area with a team", async () => { const res = await POST(postRequest({ name: "Seat 12", team: "Backend" })); expect(res.status).toBe(201); const body = await res.json(); expect(body).toMatchObject({ name: "Seat 12", team: "Backend" }); }); it("rejects an empty name", async () => { const res = await POST(postRequest({ name: "" })); expect(res.status).toBe(400); const body = await res.json(); expect(typeof body.error).toBe("string"); }); // See src/lib/areas.ts's createOrGetArea() comment: this is deliberate, // not accidental idempotency — the bot's "pick from list or type new" // flow must be able to re-submit a name it just showed as an existing // option without erroring. it("returns the existing row (200-equivalent 201, same id) instead of erroring on a duplicate name", async () => { const first = await POST(postRequest({ name: "Kho tang 2" })); const firstBody = await first.json(); const second = await POST(postRequest({ name: "Kho tang 2" })); expect(second.status).toBe(201); const secondBody = await second.json(); expect(secondBody.id).toBe(firstBody.id); const all = await prisma.area.findMany(); expect(all).toHaveLength(1); }); it("rejects an invalid JSON body", async () => { const req = new Request("http://localhost/api/areas", { method: "POST", headers: { "content-type": "application/json" }, body: "not json", }); const res = await POST(req); expect(res.status).toBe(400); }); }); describe("GET /api/areas", () => { it("lists areas alphabetically by name", async () => { await POST(postRequest({ name: "Zeta" })); await POST(postRequest({ name: "Alpha" })); const res = await GET(); expect(res.status).toBe(200); const body = await res.json(); expect(body.map((a: { name: string }) => a.name)).toEqual(["Alpha", "Zeta"]); }); it("returns an empty list when there are no areas", async () => { const res = await GET(); expect(res.status).toBe(200); expect(await res.json()).toEqual([]); }); }); describe("auth gating on /api/areas", () => { it("POST /api/areas returns 401 without a session", async () => { mockedAuth.mockResolvedValue(null); const res = await POST(postRequest({ name: "Phong hop A" })); expect(res.status).toBe(401); expect(await prisma.area.findMany()).toHaveLength(0); }); it("GET /api/areas returns 401 without a session", async () => { mockedAuth.mockResolvedValue(null); const res = await GET(); expect(res.status).toBe(401); }); });