Files
ubi-dan-itam/tests/areas.test.ts
T
toanhoangandClaude Sonnet 5 84e6db7406 test: add Area, InventoryScan and bot API tests
369/369 tests pass (35 new), tsc --noEmit clean, next lint clean. Not yet run through the repos code-reviewer subagent gate (this session operated via SSH, cwd was not the project dir so .claude/agents/ subagents were not recognized) -- see CLAUDE.md note.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:07:32 -04:00

119 lines
3.8 KiB
TypeScript

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<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/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);
});
});