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]>
197 lines
6.8 KiB
TypeScript
197 lines
6.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. 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<Session | null>>;
|
|
|
|
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);
|
|
});
|
|
});
|