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/assets/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/assets", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); } // GET /api/assets takes a Request now (2026-08-25, see route.ts) so it can // read pagination/filter query params -- query is an optional raw query // string (e.g. "status=in_use&page=2"), empty by default. function getRequest(query = ""): Request { return new Request(`http://localhost/api/assets${query ? `?${query}` : ""}`); } // AssetItem.purchaseOrderId is a real FK to PurchaseOrder (see // schema.prisma / src/lib/assets.ts), and PurchaseOrder.vendorId is a real // FK to Vendor — tests that need a valid purchaseOrderId create real rows // first and use their ids. Mirrors tests/purchase-orders.test.ts's // createTestVendor() helper. function createTestVendor(overrides: { name?: string } = {}) { return prisma.vendor.create({ data: { name: overrides.name ?? "Test Vendor", contactEmail: "sales@testvendor.example", contactPhone: "0281234567", address: "123 Test Street", }, }); } function createTestPurchaseOrder(overrides: { vendorId?: string } = {}) { return prisma.purchaseOrder.create({ data: { vendorId: overrides.vendorId as string, orderDate: new Date("2026-08-01"), expectedDate: new Date("2026-08-10"), }, }); } // AssetItem.modelId is a real FK to AssetModel (see schema.prisma / // src/lib/assets.ts), and AssetModel.categoryId is a real FK to // AssetCategory — tests that need a valid modelId create the full chain // first and use the model's id. Mirrors tests/asset-models.test.ts's // createTestAssetCategory() helper. Category is always high_value here — // AssetModel only ever references high_value categories per // src/lib/asset-models.ts's business rule. function createTestAssetCategory(overrides: { name?: string } = {}) { return prisma.assetCategory.create({ data: { name: overrides.name ?? "Laptop", valueGroup: "high_value", }, }); } function createTestAssetModel(overrides: { categoryId?: string } = {}) { return prisma.assetModel.create({ data: { categoryId: overrides.categoryId as string, manufacturer: "Dell", name: "Latitude 5440", }, }); } async function createTestModelId(): Promise { const category = await createTestAssetCategory(); const model = await createTestAssetModel({ categoryId: category.id }); return model.id; } beforeEach(async () => { // Each test starts from empty tables so listing/order assertions are // deterministic. asset_items must be cleared before purchase_orders/ // vendors/asset_models/asset_categories since asset_items has FKs to all // of them — deleteMany order avoids relying on cascade behavior in tests. // purchase_orders must be cleared before vendors (ON DELETE RESTRICT to // vendors, mirrors tests/purchase-orders.test.ts). asset_models must be // cleared before asset_categories (ON DELETE RESTRICT, mirrors // tests/asset-models.test.ts's cleanup order). await prisma.assetItem.deleteMany(); await prisma.purchaseOrder.deleteMany(); await prisma.vendor.deleteMany(); await prisma.assetModel.deleteMany(); await prisma.assetCategory.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/assets", () => { it("creates an asset item in in_stock status on valid input, with no warrantyEndDate", async () => { const modelId = await createTestModelId(); const res = await POST( postRequest({ serialNumber: "SN-001", modelId, purchaseDate: "2026-01-15" }) ); expect(res.status).toBe(201); const body = await res.json(); expect(body).toMatchObject({ serial_number: "SN-001", model_id: modelId, status: "in_stock", warranty_end_date: null, }); expect(typeof body.id).toBe("string"); expect(body.id.length).toBeGreaterThan(0); expect(Number.isNaN(Date.parse(body.purchase_date))).toBe(false); expect(Number.isNaN(Date.parse(body.created_at))).toBe(false); }); it("creates an asset item with a warrantyEndDate", async () => { const modelId = await createTestModelId(); const res = await POST( postRequest({ serialNumber: "SN-002", modelId, purchaseDate: "2026-01-15", warrantyEndDate: "2029-01-15", }) ); expect(res.status).toBe(201); const body = await res.json(); expect(Number.isNaN(Date.parse(body.warranty_end_date))).toBe(false); }); it("rejects an empty serialNumber", async () => { const modelId = await createTestModelId(); const res = await POST(postRequest({ serialNumber: "", modelId, purchaseDate: "2026-01-15" })); expect(res.status).toBe(400); const body = await res.json(); expect(body.error).toMatch(/serialNumber/i); }); it("rejects a missing modelId", async () => { const res = await POST(postRequest({ serialNumber: "SN-003", purchaseDate: "2026-01-15" })); expect(res.status).toBe(400); const body = await res.json(); expect(body.error).toMatch(/modelId/i); }); it("rejects an invalid purchaseDate", async () => { const modelId = await createTestModelId(); const res = await POST( postRequest({ serialNumber: "SN-004", modelId, purchaseDate: "not-a-date" }) ); expect(res.status).toBe(400); const body = await res.json(); expect(body.error).toMatch(/purchaseDate/i); }); it("rejects an invalid warrantyEndDate", async () => { const modelId = await createTestModelId(); const res = await POST( postRequest({ serialNumber: "SN-005", modelId, purchaseDate: "2026-01-15", warrantyEndDate: "not-a-date", }) ); expect(res.status).toBe(400); const body = await res.json(); expect(body.error).toMatch(/warrantyEndDate/i); }); it("creates an asset item with a purchaseOrderId that references a real purchase order", async () => { const modelId = await createTestModelId(); const vendor = await createTestVendor(); const purchaseOrder = await createTestPurchaseOrder({ vendorId: vendor.id }); const res = await POST( postRequest({ serialNumber: "SN-007", modelId, purchaseDate: "2026-01-15", purchaseOrderId: purchaseOrder.id, }) ); expect(res.status).toBe(201); const body = await res.json(); expect(body.purchase_order_id).toBe(purchaseOrder.id); }); it("creates an asset item with no purchaseOrderId (stays null)", async () => { const modelId = await createTestModelId(); const res = await POST( postRequest({ serialNumber: "SN-008", modelId, purchaseDate: "2026-01-15" }) ); expect(res.status).toBe(201); const body = await res.json(); expect(body.purchase_order_id).toBeNull(); }); it("rejects a purchaseOrderId that is a well-formed UUID but doesn't reference an existing purchase order", async () => { const modelId = await createTestModelId(); const res = await POST( postRequest({ serialNumber: "SN-009", modelId, purchaseDate: "2026-01-15", purchaseOrderId: "00000000-0000-0000-0000-000000000000", }) ); expect(res.status).toBe(400); const body = await res.json(); expect(body.error).toMatch(/purchaseOrderId/i); expect(body.error).toMatch(/does not reference an existing purchase order/i); // And it must not have created anything. const remaining = await prisma.assetItem.findMany(); expect(remaining).toHaveLength(0); }); it("rejects a purchaseOrderId that is not a valid UUID", async () => { const modelId = await createTestModelId(); const res = await POST( postRequest({ serialNumber: "SN-012", modelId, purchaseDate: "2026-01-15", purchaseOrderId: "not-a-uuid", }) ); expect(res.status).toBe(400); const body = await res.json(); expect(body.error).toMatch(/purchaseOrderId/i); }); it("rejects a duplicate serialNumber", async () => { const modelId = await createTestModelId(); await POST(postRequest({ serialNumber: "SN-006", modelId, purchaseDate: "2026-01-15" })); const res = await POST(postRequest({ serialNumber: "SN-006", modelId, purchaseDate: "2026-02-01" })); expect(res.status).toBe(400); const body = await res.json(); expect(body.error).toMatch(/serialNumber already exists/i); // And it must not have created a second row. const remaining = await prisma.assetItem.findMany(); expect(remaining).toHaveLength(1); }); it("rejects a modelId that is not a valid UUID", async () => { const res = await POST( postRequest({ serialNumber: "SN-013", modelId: "not-a-uuid", purchaseDate: "2026-01-15" }) ); expect(res.status).toBe(400); const body = await res.json(); expect(body.error).toMatch(/modelId/i); // And it must not have created anything. const remaining = await prisma.assetItem.findMany(); expect(remaining).toHaveLength(0); }); it("rejects a modelId that is a well-formed UUID but doesn't reference an existing asset model", async () => { const res = await POST( postRequest({ serialNumber: "SN-014", modelId: "00000000-0000-0000-0000-000000000000", purchaseDate: "2026-01-15", }) ); expect(res.status).toBe(400); const body = await res.json(); expect(body.error).toMatch(/modelId/i); expect(body.error).toMatch(/does not reference an existing asset model/i); // And it must not have created anything. const remaining = await prisma.assetItem.findMany(); expect(remaining).toHaveLength(0); }); it("rejects an invalid JSON body", async () => { const req = new Request("http://localhost/api/assets", { method: "POST", headers: { "content-type": "application/json" }, body: "not json", }); const res = await POST(req); expect(res.status).toBe(400); const body = await res.json(); expect(body.error).toMatch(/JSON/i); }); it("rejects a non-object JSON body", async () => { // Note: arrays are `typeof "object"` in JS, so they pass the route's // object check — use a JSON string primitive here to actually exercise // the "not an object" branch (mirrors tests/licenses.test.ts). const res = await POST(postRequest("not an object")); expect(res.status).toBe(400); const body = await res.json(); expect(body.error).toMatch(/JSON object/i); }); }); describe("GET /api/assets", () => { it("lists created asset items, newest first", async () => { const modelId = await createTestModelId(); await POST(postRequest({ serialNumber: "SN-010", modelId, purchaseDate: "2026-01-15" })); // Ensure a distinct created_at ordering even on fast filesystems/clocks. await new Promise((resolve) => setTimeout(resolve, 5)); await POST(postRequest({ serialNumber: "SN-011", modelId, purchaseDate: "2026-02-01" })); const res = await GET(getRequest()); expect(res.status).toBe(200); // {items, total} now (2026-08-25, see listAssetItems()'s doc comment // in src/lib/assets.ts) -- was a bare array before pagination. const body = await res.json(); expect(Array.isArray(body.items)).toBe(true); expect(body.items).toHaveLength(2); expect(body.total).toBe(2); // newest first expect(body.items[0]).toMatchObject({ serial_number: "SN-011", model_id: modelId }); expect(body.items[1]).toMatchObject({ serial_number: "SN-010", model_id: modelId }); }); it("returns an empty list when there are no asset items", async () => { const res = await GET(getRequest()); expect(res.status).toBe(200); const body = await res.json(); expect(body).toEqual({ items: [], total: 0 }); }); }); describe("auth gating on /api/assets", () => { // Mirrors tests/employees.test.ts's "auth gating" block — only checks // session presence, per the flat permission model (no role/permission // logic). it("POST /api/assets returns 401 without a session", async () => { const modelId = await createTestModelId(); mockedAuth.mockResolvedValue(null); const res = await POST( postRequest({ serialNumber: "SN-020", modelId, purchaseDate: "2026-01-15" }) ); 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.assetItem.findMany(); expect(remaining).toHaveLength(0); }); it("GET /api/assets returns 401 without a session", async () => { mockedAuth.mockResolvedValue(null); const res = await GET(getRequest()); expect(res.status).toBe(401); const body = await res.json(); expect(body).toEqual({ error: "Unauthorized" }); }); });