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. Creation of // InventoryScan rows only happens via the bot-secret-authed // /api/bot/inventory-scans route (see tests/bot-api.test.ts) — this file // covers the session-authed human routes only (list/edit/delete), so test // data is seeded directly via prisma, not through POST. vi.mock("@/auth", () => ({ auth: vi.fn() })); import { GET as listScans } from "@/app/api/inventory-scans/route"; import { PATCH, DELETE } from "@/app/api/inventory-scans/[id]/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(), }; async function seedCategoryAndModel() { const category = await prisma.assetCategory.create({ data: { name: "Controller", valueGroup: "high_value" }, }); const model = await prisma.assetModel.create({ data: { categoryId: category.id, manufacturer: "Sony", name: "PS5", specs: {} }, }); const model2 = await prisma.assetModel.create({ data: { categoryId: category.id, manufacturer: "Microsoft", name: "Xbox", specs: {} }, }); return { category, model, model2 }; } function patchRequest(body: unknown): Request { return new Request("http://localhost/api/inventory-scans/x", { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); } beforeEach(async () => { await prisma.inventoryScan.deleteMany(); await prisma.assetModel.deleteMany(); await prisma.assetCategory.deleteMany(); await prisma.area.deleteMany(); await prisma.employee.deleteMany(); mockedAuth.mockResolvedValue(testSession); }); describe("GET /api/inventory-scans", () => { it("lists scans newest first, with joined category/model names", async () => { const { category, model } = await seedCategoryAndModel(); const scan = await prisma.inventoryScan.create({ data: { categoryId: category.id, modelId: model.id, assetTag: "TAG-1" }, }); const res = await listScans(); expect(res.status).toBe(200); const body = await res.json(); expect(body).toHaveLength(1); expect(body[0]).toMatchObject({ id: scan.id, category_name: "Controller", model_name: "PS5", model_manufacturer: "Sony", asset_tag: "TAG-1", area_name: null, assigned_employee_name: null, }); }); it("returns an empty list when there are no scans", async () => { const res = await listScans(); expect(res.status).toBe(200); expect(await res.json()).toEqual([]); }); }); describe("PATCH /api/inventory-scans/[id]", () => { it("updates area, assignee, asset tag, serial number and specs", async () => { const { category, model } = await seedCategoryAndModel(); const scan = await prisma.inventoryScan.create({ data: { categoryId: category.id, modelId: model.id }, }); const area = await prisma.area.create({ data: { name: "Phong hop A" } }); const employee = await prisma.employee.create({ data: { name: "Toan", department: "IT" } }); const res = await PATCH( patchRequest({ areaId: area.id, assignedEmployeeId: employee.id, assetTag: "TAG-2", serialNumber: "SN-123", specs: { cpu: "i7" }, }), { params: { id: scan.id } } ); expect(res.status).toBe(200); const body = await res.json(); expect(body).toMatchObject({ area_name: "Phong hop A", assigned_employee_name: "Toan", asset_tag: "TAG-2", serial_number: "SN-123", specs: { cpu: "i7" }, }); }); it("allows switching to a different model within the same category", async () => { const { category, model, model2 } = await seedCategoryAndModel(); const scan = await prisma.inventoryScan.create({ data: { categoryId: category.id, modelId: model.id }, }); const res = await PATCH(patchRequest({ modelId: model2.id }), { params: { id: scan.id } }); expect(res.status).toBe(200); const body = await res.json(); expect(body.model_name).toBe("Xbox"); }); it("clears area/assignee when explicitly set to null", async () => { const { category, model } = await seedCategoryAndModel(); const area = await prisma.area.create({ data: { name: "Phong hop A" } }); const scan = await prisma.inventoryScan.create({ data: { categoryId: category.id, modelId: model.id, areaId: area.id }, }); const res = await PATCH(patchRequest({ areaId: null }), { params: { id: scan.id } }); expect(res.status).toBe(200); const body = await res.json(); expect(body.area_id).toBeNull(); }); it("returns 404 for an id that doesn't exist", async () => { const res = await PATCH(patchRequest({ assetTag: "x" }), { params: { id: "00000000-0000-0000-0000-000000000000" }, }); expect(res.status).toBe(404); }); it("returns 400 for a malformed id", async () => { const res = await PATCH(patchRequest({ assetTag: "x" }), { params: { id: "not-a-uuid" } }); expect(res.status).toBe(400); }); }); describe("DELETE /api/inventory-scans/[id]", () => { it("deletes an existing scan", async () => { const { category, model } = await seedCategoryAndModel(); const scan = await prisma.inventoryScan.create({ data: { categoryId: category.id, modelId: model.id }, }); const res = await DELETE(new Request("http://localhost/api/inventory-scans/x", { method: "DELETE" }), { params: { id: scan.id }, }); expect(res.status).toBe(204); expect(await prisma.inventoryScan.findUnique({ where: { id: scan.id } })).toBeNull(); }); it("returns 404 for an id that doesn't exist", async () => { const res = await DELETE(new Request("http://localhost/api/inventory-scans/x", { method: "DELETE" }), { params: { id: "00000000-0000-0000-0000-000000000000" }, }); expect(res.status).toBe(404); }); }); describe("auth gating on /api/inventory-scans", () => { it("GET returns 401 without a session", async () => { mockedAuth.mockResolvedValue(null); const res = await listScans(); expect(res.status).toBe(401); }); it("PATCH returns 401 without a session", async () => { mockedAuth.mockResolvedValue(null); const res = await PATCH(patchRequest({ assetTag: "x" }), { params: { id: "00000000-0000-0000-0000-000000000000" }, }); expect(res.status).toBe(401); }); it("DELETE returns 401 without a session", async () => { mockedAuth.mockResolvedValue(null); const res = await DELETE(new Request("http://localhost/api/inventory-scans/x", { method: "DELETE" }), { params: { id: "00000000-0000-0000-0000-000000000000" }, }); expect(res.status).toBe(401); }); });