From 087e613a695a4c487d9fec3fd37a5754a7a56724 Mon Sep 17 00:00:00 2001 From: Toan Hoang Minh Date: Fri, 31 Jul 2026 16:04:58 +0700 Subject: [PATCH] test: add AssetItem API tests --- tests/assets.test.ts | 184 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 tests/assets.test.ts diff --git a/tests/assets.test.ts b/tests/assets.test.ts new file mode 100644 index 0000000..951ebe5 --- /dev/null +++ b/tests/assets.test.ts @@ -0,0 +1,184 @@ +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), + }); +} + +beforeEach(async () => { + // Each test starts from an empty table so listing/order assertions are + // deterministic. + await prisma.assetItem.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 res = await POST( + postRequest({ serialNumber: "SN-001", model: "Dell Latitude 5440", purchaseDate: "2026-01-15" }) + ); + + expect(res.status).toBe(201); + const body = await res.json(); + + expect(body).toMatchObject({ + serial_number: "SN-001", + model: "Dell Latitude 5440", + 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 res = await POST( + postRequest({ + serialNumber: "SN-002", + model: "Dell Latitude 5440", + 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 res = await POST(postRequest({ serialNumber: "", model: "Dell", purchaseDate: "2026-01-15" })); + + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/serialNumber/i); + }); + + it("rejects an empty model", async () => { + const res = await POST(postRequest({ serialNumber: "SN-003", model: "", purchaseDate: "2026-01-15" })); + + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/model/i); + }); + + it("rejects an invalid purchaseDate", async () => { + const res = await POST( + postRequest({ serialNumber: "SN-004", model: "Dell", 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 res = await POST( + postRequest({ + serialNumber: "SN-005", + model: "Dell", + 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("rejects a duplicate serialNumber", async () => { + await POST(postRequest({ serialNumber: "SN-006", model: "Dell", purchaseDate: "2026-01-15" })); + const res = await POST(postRequest({ serialNumber: "SN-006", model: "HP", 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); + }); +}); + +describe("GET /api/assets", () => { + it("lists created asset items, newest first", async () => { + await POST(postRequest({ serialNumber: "SN-010", model: "Dell", 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", model: "HP", purchaseDate: "2026-02-01" })); + + 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({ serial_number: "SN-011", model: "HP" }); + expect(body[1]).toMatchObject({ serial_number: "SN-010", model: "Dell" }); + }); + + it("returns an empty list when there are no asset items", async () => { + const res = await GET(); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toEqual([]); + }); +}); + +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 () => { + mockedAuth.mockResolvedValue(null); + + const res = await POST( + postRequest({ serialNumber: "SN-020", model: "Dell", 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(); + + expect(res.status).toBe(401); + const body = await res.json(); + expect(body).toEqual({ error: "Unauthorized" }); + }); +});