import { randomUUID } from "node:crypto"; import { NextRequest } from "next/server"; import { beforeEach, expect, it, vi } from "vitest"; import { PERMS } from "@/lib/permission-slugs"; const mocks = vi.hoisted(() => ({ guard: vi.fn(), create: vi.fn(), list: vi.fn(), cancel: vi.fn(), after: vi.fn(), ping: vi.fn(), source: vi.fn(), })); vi.mock("@/lib/api-handler", () => ({ withAdmin: (options: unknown, handler: unknown) => { mocks.guard(options); return handler; }, })); vi.mock("next/server", async (original) => ({ ...(await original()), after: mocks.after, })); vi.mock("@/lib/redis", () => ({ redis: { ping: mocks.ping } })); vi.mock("@/lib/services/furni-job-worker", () => ({ drainFurnitureImports: vi.fn(), })); vi.mock("@/lib/services/furni-job-store", async (original) => ({ ...(await original()), ImportJobStore: class { create = mocks.create; list = mocks.list; requestCancellation = mocks.cancel; }, })); vi.mock("@/lib/services/clone-sources", () => ({ getSource: mocks.source })); import { GET, PATCH, POST } from "./route"; const item = { id: 1, classname: "nft_china_light", name: "Lamp", description: "", type: "wallitem", revision: 70317, category: "other", }; const ctx = { session: { user: { id: 7 } } } as never; const request = (body: unknown) => new NextRequest("http://localhost/api/admin/studio/import-jobs", { method: "POST", body: JSON.stringify(body), }); beforeEach(() => { mocks.create.mockClear(); mocks.list.mockClear(); mocks.after.mockClear(); mocks.ping.mockResolvedValue("PONG"); mocks.source.mockResolvedValue(null); mocks.create.mockImplementation((job) => job); mocks.list.mockResolvedValue([]); }); it("requires asset import permission", () => { expect(mocks.guard).toHaveBeenCalledWith({ permission: PERMS.ASSETS_IMPORT }); }); it("queues a durable owner-scoped job and schedules execution after responding", async () => { const response = await POST( request({ id: randomUUID(), items: [item, item] }), ctx, ); expect(response.status).toBe(200); expect(mocks.create.mock.calls[0][0]).toMatchObject({ userId: 7, state: "queued", items: [{ ...item, state: "pending" }], }); expect(mocks.after).toHaveBeenCalledOnce(); }); it("rejects traversal before saving work", async () => { expect( ( await POST( request({ id: randomUUID(), items: [{ ...item, classname: "../bad" }], }), ctx, ) ).status, ).toBe(400); expect(mocks.create).not.toHaveBeenCalled(); }); it("does not accept work when the worker lease service is unavailable", async () => { mocks.ping.mockRejectedValue(Error("offline")); expect( (await POST(request({ id: randomUUID(), items: [item] }), ctx)).status, ).toBe(503); expect(mocks.create).not.toHaveBeenCalled(); }); it("rejects a missing configured source", async () => { expect( ( await POST( request({ id: randomUUID(), sourceId: "gone", items: [item] }), ctx, ) ).status, ).toBe(404); }); it("requests bounded owner-scoped history from the store", async () => { mocks.list.mockResolvedValue([{ id: "a", userId: 7 }]); const response = await GET(request({}), ctx); expect(mocks.list).toHaveBeenCalledWith({ userId: 7, limit: 30 }); expect((await response.json()).jobs).toEqual([{ id: "a", userId: 7 }]); }); it("requests cancellation with the authenticated owner", async () => { const id = randomUUID(); mocks.cancel.mockResolvedValue({ id, cancelRequested: true }); const response = await PATCH(request({ id }), ctx); expect(response.status).toBe(200); expect(mocks.cancel).toHaveBeenCalledWith(id, 7); }); it("does not reveal other owners' jobs", async () => { mocks.cancel.mockResolvedValue(null); expect((await PATCH(request({ id: randomUUID() }), ctx)).status).toBe(404); }); it("rejects unsafe cancellation IDs", async () => { expect((await PATCH(request({ id: "../other" }), ctx)).status).toBe(400); });