test: add AssetItem API tests
This commit is contained in:
@@ -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<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/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" });
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user