diff --git a/tests/maintenance-records.test.ts b/tests/maintenance-records.test.ts new file mode 100644 index 0000000..62230bb --- /dev/null +++ b/tests/maintenance-records.test.ts @@ -0,0 +1,809 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { Mock } from "vitest"; +import type { Session } from "next-auth"; +import { + closeMaintenanceRecord, + createMaintenanceRecord, + listMaintenanceRecords, + validateCloseMaintenanceRecordInput, + validateCreateMaintenanceRecordInput, +} from "@/lib/maintenance-records"; +import { prisma } from "@/lib/prisma"; + +// This file has two layers of coverage, mirroring tests/assignments.test.ts: +// - lib-level tests exercise src/lib/maintenance-records.ts directly +// (validation, transactional business rules). +// - route-level tests exercise src/app/api/maintenance-records/route.ts and +// src/app/api/maintenance-records/[id]/route.ts over HTTP, including the +// auth() gate — mirrors tests/assignments.test.ts's @/auth mocking pattern. +vi.mock("@/auth", () => ({ auth: vi.fn() })); + +import { GET, POST } from "@/app/api/maintenance-records/route"; +import { PATCH } from "@/app/api/maintenance-records/[id]/route"; +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/maintenance-records", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); +} + +function patchRequest(body: unknown): Request { + return new Request("http://localhost/api/maintenance-records/x", { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); +} + +// MaintenanceRecord.assetItemId is a real FK to AssetItem, which itself +// requires a full AssetModel -> AssetCategory chain. Mirrors +// tests/assignments.test.ts's createTestModelId() helper. +async function createTestModelId(): Promise { + const category = await prisma.assetCategory.create({ + data: { name: "Laptop", valueGroup: "high_value" }, + }); + const model = await prisma.assetModel.create({ + data: { categoryId: category.id, manufacturer: "Dell", name: "Latitude 5440" }, + }); + return model.id; +} + +let serialCounter = 0; +function createTestAssetItem(overrides: { + modelId: string; + status?: "in_stock" | "assigned" | "repair" | "retired"; +}) { + serialCounter += 1; + return prisma.assetItem.create({ + data: { + serialNumber: `SN-MAINT-${serialCounter}`, + modelId: overrides.modelId, + purchaseDate: new Date("2026-01-15"), + status: overrides.status ?? "in_stock", + }, + }); +} + +// MaintenanceRecord.employeeId isn't a field on the model, but Assignment is +// used to exercise the "active assignment blocks maintenance" rule, which +// itself needs an Employee. Mirrors tests/assignments.test.ts's +// createTestEmployee() helper. +function createTestEmployee(overrides: { name?: string } = {}) { + return prisma.employee.create({ + data: { + name: overrides.name ?? "Test Employee", + department: "IT", + }, + }); +} + +// MaintenanceRecord.vendorId is a real (nullable) FK to Vendor. Mirrors +// tests/assignments.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", + }, + }); +} + +beforeEach(async () => { + // Clear in FK-safe order: maintenance_records + assignments first + // (reference asset_items/licenses/vendors/employees), then asset_items/ + // licenses (reference purchase_orders/vendors/models), then the rest. + await prisma.maintenanceRecord.deleteMany(); + await prisma.assignment.deleteMany(); + await prisma.assetItem.deleteMany(); + await prisma.license.deleteMany(); + await prisma.assetModel.deleteMany(); + await prisma.assetCategory.deleteMany(); + await prisma.purchaseOrder.deleteMany(); + await prisma.vendor.deleteMany(); + await prisma.employee.deleteMany(); + serialCounter = 0; + // Default to a logged-in session — the lib-level tests don't go through + // the route at all (no-op for them), and the route-level tests exercise + // business logic, not auth gating (covered by the "auth gating" block). + mockedAuth.mockResolvedValue(testSession); +}); + +describe("validateCreateMaintenanceRecordInput", () => { + it("accepts valid input without vendorId", () => { + const result = validateCreateMaintenanceRecordInput({ + assetItemId: "11111111-1111-1111-1111-111111111111", + type: "repair", + issueDescription: "Screen flickers", + startDate: "2026-08-01", + }); + expect(result.ok).toBe(true); + }); + + it("accepts valid input with vendorId set", () => { + const result = validateCreateMaintenanceRecordInput({ + assetItemId: "11111111-1111-1111-1111-111111111111", + type: "warranty", + vendorId: "22222222-2222-2222-2222-222222222222", + issueDescription: "Battery swollen", + startDate: "2026-08-01", + }); + expect(result.ok).toBe(true); + }); + + it("rejects a missing assetItemId", () => { + const result = validateCreateMaintenanceRecordInput({ + type: "repair", + issueDescription: "Screen flickers", + startDate: "2026-08-01", + }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toMatch(/assetItemId/i); + } + }); + + it("rejects an invalid type", () => { + const result = validateCreateMaintenanceRecordInput({ + assetItemId: "11111111-1111-1111-1111-111111111111", + type: "not-a-type", + issueDescription: "Screen flickers", + startDate: "2026-08-01", + }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toMatch(/type/i); + } + }); +}); + +describe("createMaintenanceRecord", () => { + it("opens a maintenance record, flips AssetItem.status to repair", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + + const created = await createMaintenanceRecord({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: new Date("2026-08-01"), + }); + expect(created.ok).toBe(true); + if (!created.ok) return; + expect(created.value.asset_item_id).toBe(assetItem.id); + expect(created.value.vendor_id).toBeNull(); + expect(created.value.end_date).toBeNull(); + + const updatedItem = await prisma.assetItem.findUniqueOrThrow({ where: { id: assetItem.id } }); + expect(updatedItem.status).toBe("repair"); + }); + + it("creates successfully without a vendorId", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + + const created = await createMaintenanceRecord({ + assetItemId: assetItem.id, + type: "preventive", + issueDescription: "Routine cleaning", + startDate: new Date("2026-08-01"), + }); + expect(created.ok).toBe(true); + if (!created.ok) return; + expect(created.value.vendor_id).toBeNull(); + }); + + it("creates successfully with a vendorId", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + const vendor = await createTestVendor(); + + const created = await createMaintenanceRecord({ + assetItemId: assetItem.id, + type: "warranty", + vendorId: vendor.id, + issueDescription: "Battery swollen", + startDate: new Date("2026-08-01"), + }); + expect(created.ok).toBe(true); + if (!created.ok) return; + expect(created.value.vendor_id).toBe(vendor.id); + }); + + it("rejects opening a record for an AssetItem that has an active assignment", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId, status: "assigned" }); + const employee = await createTestEmployee(); + await prisma.assignment.create({ + data: { employeeId: employee.id, assetItemId: assetItem.id, assignedAt: new Date() }, + }); + + const created = await createMaintenanceRecord({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: new Date("2026-08-01"), + }); + expect(created.ok).toBe(false); + if (created.ok) return; + expect(created.error).toMatch(/active assignment/i); + + const remaining = await prisma.maintenanceRecord.findMany(); + expect(remaining).toHaveLength(0); + }); + + it("rejects opening a record for an AssetItem that is already retired", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId, status: "retired" }); + + const created = await createMaintenanceRecord({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: new Date("2026-08-01"), + }); + expect(created.ok).toBe(false); + if (created.ok) return; + expect(created.error).toMatch(/retired/i); + + const remaining = await prisma.maintenanceRecord.findMany(); + expect(remaining).toHaveLength(0); + }); + + it("rejects opening a second record while one is already open for the same AssetItem", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + + const first = await createMaintenanceRecord({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: new Date("2026-08-01"), + }); + expect(first.ok).toBe(true); + + const second = await createMaintenanceRecord({ + assetItemId: assetItem.id, + type: "preventive", + issueDescription: "Another issue", + startDate: new Date("2026-08-02"), + }); + expect(second.ok).toBe(false); + if (second.ok) return; + expect(second.error).toMatch(/already has an open maintenance record/i); + + const records = await prisma.maintenanceRecord.findMany({ where: { assetItemId: assetItem.id } }); + expect(records).toHaveLength(1); + }); +}); + +describe("closeMaintenanceRecord", () => { + it.each(["in_stock", "retired"] as const)( + "closes a record and applies targetStatus %s to the AssetItem", + async (targetStatus) => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + const created = await createMaintenanceRecord({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: new Date("2026-08-01"), + }); + expect(created.ok).toBe(true); + if (!created.ok) return; + + const closed = await closeMaintenanceRecord(created.value.id, { + endDate: new Date("2026-08-05"), + targetStatus, + }); + expect(closed.ok).toBe(true); + if (!closed.ok) return; + expect(closed.value.end_date).not.toBeNull(); + + const item = await prisma.assetItem.findUniqueOrThrow({ where: { id: assetItem.id } }); + expect(item.status).toBe(targetStatus); + } + ); + + it("rejects double-closing a record", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + const created = await createMaintenanceRecord({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: new Date("2026-08-01"), + }); + expect(created.ok).toBe(true); + if (!created.ok) return; + + const firstClose = await closeMaintenanceRecord(created.value.id, { + endDate: new Date("2026-08-05"), + targetStatus: "in_stock", + }); + expect(firstClose.ok).toBe(true); + + const secondClose = await closeMaintenanceRecord(created.value.id, { + endDate: new Date("2026-08-06"), + targetStatus: "in_stock", + }); + expect(secondClose.ok).toBe(false); + if (secondClose.ok) return; + expect(secondClose.status).toBe(400); + expect(secondClose.error).toMatch(/already been closed/i); + }); + + it("rejects an endDate before the record's startDate", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + const created = await createMaintenanceRecord({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: new Date("2026-08-05"), + }); + expect(created.ok).toBe(true); + if (!created.ok) return; + + const closed = await closeMaintenanceRecord(created.value.id, { + endDate: new Date("2026-08-01"), + targetStatus: "in_stock", + }); + expect(closed.ok).toBe(false); + if (closed.ok) return; + expect(closed.status).toBe(400); + expect(closed.error).toMatch(/endDate must be on or after startDate/i); + }); + + it("returns 404 for a nonexistent record id", async () => { + const result = await closeMaintenanceRecord("00000000-0000-0000-0000-000000000000", { + endDate: new Date("2026-08-05"), + targetStatus: "in_stock", + }); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.status).toBe(404); + expect(result.error).toMatch(/not found/i); + }); +}); + +describe("validateCloseMaintenanceRecordInput", () => { + it("accepts valid input", () => { + const result = validateCloseMaintenanceRecordInput({ + endDate: "2026-08-05", + targetStatus: "in_stock", + }); + expect(result.ok).toBe(true); + }); + + it("rejects a missing targetStatus", () => { + const result = validateCloseMaintenanceRecordInput({ endDate: "2026-08-05" }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toMatch(/targetStatus/i); + } + }); + + it("rejects a missing endDate", () => { + const result = validateCloseMaintenanceRecordInput({ targetStatus: "in_stock" }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toMatch(/endDate/i); + } + }); +}); + +describe("listMaintenanceRecords", () => { + it("lists created records, newest first", async () => { + const modelId = await createTestModelId(); + const item1 = await createTestAssetItem({ modelId }); + const item2 = await createTestAssetItem({ modelId }); + + const first = await createMaintenanceRecord({ + assetItemId: item1.id, + type: "repair", + issueDescription: "Issue 1", + startDate: new Date("2026-08-01"), + }); + expect(first.ok).toBe(true); + await new Promise((resolve) => setTimeout(resolve, 5)); + const second = await createMaintenanceRecord({ + assetItemId: item2.id, + type: "repair", + issueDescription: "Issue 2", + startDate: new Date("2026-08-01"), + }); + expect(second.ok).toBe(true); + + const records = await listMaintenanceRecords(); + expect(records).toHaveLength(2); + expect(records[0].asset_item_id).toBe(item2.id); + expect(records[1].asset_item_id).toBe(item1.id); + }); + + it("returns an empty list when there are no records", async () => { + const records = await listMaintenanceRecords(); + expect(records).toEqual([]); + }); +}); + +describe("POST /api/maintenance-records", () => { + it("creates a record on valid input", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + + const res = await POST( + postRequest({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: "2026-08-01", + }) + ); + expect(res.status).toBe(201); + const body = await res.json(); + expect(body).toMatchObject({ + asset_item_id: assetItem.id, + type: "repair", + vendor_id: null, + end_date: null, + }); + + const item = await prisma.assetItem.findUniqueOrThrow({ where: { id: assetItem.id } }); + expect(item.status).toBe("repair"); + }); + + it("returns the full expected snake_case shape", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + const vendor = await createTestVendor(); + + const res = await POST( + postRequest({ + assetItemId: assetItem.id, + type: "warranty", + vendorId: vendor.id, + issueDescription: "Battery swollen", + startDate: "2026-08-01", + }) + ); + expect(res.status).toBe(201); + const body = await res.json(); + expect(body).toMatchObject({ + id: expect.any(String), + asset_item_id: assetItem.id, + type: "warranty", + vendor_id: vendor.id, + issue_description: "Battery swollen", + resolution_notes: null, + start_date: expect.any(String), + end_date: null, + created_at: expect.any(String), + }); + }); + + it("rejects creating a record for an AssetItem with an active assignment", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId, status: "assigned" }); + const employee = await createTestEmployee(); + await prisma.assignment.create({ + data: { employeeId: employee.id, assetItemId: assetItem.id, assignedAt: new Date() }, + }); + + const res = await POST( + postRequest({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: "2026-08-01", + }) + ); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/active assignment/i); + }); + + it("rejects creating a record for a retired AssetItem", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId, status: "retired" }); + + const res = await POST( + postRequest({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: "2026-08-01", + }) + ); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/retired/i); + }); + + it("rejects creating a second open record for the same AssetItem", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + + await POST( + postRequest({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: "2026-08-01", + }) + ); + const res = await POST( + postRequest({ + assetItemId: assetItem.id, + type: "preventive", + issueDescription: "Another issue", + startDate: "2026-08-02", + }) + ); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/already has an open maintenance record/i); + }); + + it("rejects an invalid JSON body", async () => { + const req = new Request("http://localhost/api/maintenance-records", { + 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 (string)", async () => { + 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); + }); + + it("rejects a non-object JSON body (number)", async () => { + const res = await POST(postRequest(123)); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/JSON object/i); + }); +}); + +describe("GET /api/maintenance-records", () => { + it("lists created records, newest first", async () => { + const modelId = await createTestModelId(); + const item1 = await createTestAssetItem({ modelId }); + const item2 = await createTestAssetItem({ modelId }); + + await POST( + postRequest({ + assetItemId: item1.id, + type: "repair", + issueDescription: "Issue 1", + startDate: "2026-08-01", + }) + ); + await new Promise((resolve) => setTimeout(resolve, 5)); + await POST( + postRequest({ + assetItemId: item2.id, + type: "repair", + issueDescription: "Issue 2", + startDate: "2026-08-01", + }) + ); + + const res = await GET(); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toHaveLength(2); + expect(body[0].asset_item_id).toBe(item2.id); + expect(body[1].asset_item_id).toBe(item1.id); + }); + + it("returns an empty list when there are no records", async () => { + const res = await GET(); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toEqual([]); + }); + + it("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" }); + }); +}); + +describe("PATCH /api/maintenance-records/[id]", () => { + it.each(["in_stock", "retired"] as const)( + "closes a record with targetStatus %s", + async (targetStatus) => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + const createRes = await POST( + postRequest({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: "2026-08-01", + }) + ); + const created = await createRes.json(); + + const res = await PATCH( + patchRequest({ endDate: "2026-08-05", targetStatus }), + { params: { id: created.id } } + ); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.end_date).not.toBeNull(); + + const item = await prisma.assetItem.findUniqueOrThrow({ where: { id: assetItem.id } }); + expect(item.status).toBe(targetStatus); + } + ); + + it("returns 400 when targetStatus is missing", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + const createRes = await POST( + postRequest({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: "2026-08-01", + }) + ); + const created = await createRes.json(); + + const res = await PATCH(patchRequest({ endDate: "2026-08-05" }), { + params: { id: created.id }, + }); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/targetStatus/i); + }); + + it("rejects double-closing a record with 400", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + const createRes = await POST( + postRequest({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: "2026-08-01", + }) + ); + const created = await createRes.json(); + + await PATCH(patchRequest({ endDate: "2026-08-05", targetStatus: "in_stock" }), { + params: { id: created.id }, + }); + const res = await PATCH(patchRequest({ endDate: "2026-08-06", targetStatus: "in_stock" }), { + params: { id: created.id }, + }); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/already been closed/i); + }); + + it("rejects an endDate before startDate with 400", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + const createRes = await POST( + postRequest({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: "2026-08-05", + }) + ); + const created = await createRes.json(); + + const res = await PATCH(patchRequest({ endDate: "2026-08-01", targetStatus: "in_stock" }), { + params: { id: created.id }, + }); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/endDate must be on or after startDate/i); + }); + + it("returns 404 for a nonexistent record id", async () => { + const res = await PATCH(patchRequest({ endDate: "2026-08-05", targetStatus: "in_stock" }), { + params: { id: "00000000-0000-0000-0000-000000000000" }, + }); + expect(res.status).toBe(404); + const body = await res.json(); + expect(body.error).toMatch(/not found/i); + }); + + it("returns 400 for a malformed id (not a UUID)", async () => { + const res = await PATCH(patchRequest({ endDate: "2026-08-05", targetStatus: "in_stock" }), { + params: { id: "not-a-uuid" }, + }); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/UUID/i); + }); + + it("rejects an invalid JSON body", async () => { + const req = new Request("http://localhost/api/maintenance-records/x", { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: "not json", + }); + const res = await PATCH(req, { params: { id: "00000000-0000-0000-0000-000000000000" } }); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/JSON/i); + }); + + it("rejects a non-object JSON body (string)", async () => { + const res = await PATCH(patchRequest("not an object"), { + params: { id: "00000000-0000-0000-0000-000000000000" }, + }); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/JSON object/i); + }); + + it("rejects a non-object JSON body (number)", async () => { + const res = await PATCH(patchRequest(123), { + params: { id: "00000000-0000-0000-0000-000000000000" }, + }); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/JSON object/i); + }); + + it("returns 401 without a session", async () => { + mockedAuth.mockResolvedValue(null); + const res = await PATCH(patchRequest({ endDate: "2026-08-05", targetStatus: "in_stock" }), { + params: { id: "00000000-0000-0000-0000-000000000000" }, + }); + expect(res.status).toBe(401); + const body = await res.json(); + expect(body).toEqual({ error: "Unauthorized" }); + }); +}); + +describe("auth gating on /api/maintenance-records", () => { + it("POST /api/maintenance-records returns 401 without a session", async () => { + const modelId = await createTestModelId(); + const assetItem = await createTestAssetItem({ modelId }); + mockedAuth.mockResolvedValue(null); + + const res = await POST( + postRequest({ + assetItemId: assetItem.id, + type: "repair", + issueDescription: "Screen flickers", + startDate: "2026-08-01", + }) + ); + expect(res.status).toBe(401); + const body = await res.json(); + expect(body).toEqual({ error: "Unauthorized" }); + + const remaining = await prisma.maintenanceRecord.findMany(); + expect(remaining).toHaveLength(0); + }); +});