// @vitest-environment node import { describe, expect, it, vi } from "vitest"; // biome-ignore lint/suspicious/noExplicitAny: test helper for mocking with arbitrary signatures type AnyFn = (...args: any[]) => any; const { downloadFile, appendFurniEntry, parseNitroBundle, execRaw, queryRaw, fsUnlink, fsReadFile, } = vi.hoisted(() => ({ downloadFile: vi.fn(async () => ({ ok: true, size: 200 })), appendFurniEntry: vi.fn(async () => {}), parseNitroBundle: vi.fn(() => ({ json: {}, png: Buffer.alloc(0) })), execRaw: vi.fn(async () => 1), queryRaw: vi.fn(async () => [] as unknown[]), fsUnlink: vi.fn(async () => {}), fsReadFile: vi.fn(async () => Buffer.from("NITRO")), })); vi.mock("@/lib/services/import/core/download", () => ({ downloadFile })); vi.mock("@/lib/services/furni-data", () => ({ appendFurniEntry })); vi.mock("@/lib/services/furni-import", () => ({ ensureDirectories: vi.fn(async () => {}), getOrCreateCategoryPage: vi.fn(async () => 99), autoPriceFurni: vi.fn(() => ({ credits: 5, points: 0, pointsType: 0, })), allocateCatalogItemId: vi.fn( async (fn: (n: number) => Promise) => fn(1000), ), })); vi.mock("@/lib/services/furni-asset-dirs", () => ({ getFurniAssetDirs: vi.fn(async () => ({ nitroDir: "/tmp/nitro", iconDir: "/tmp/icons", swfDir: "/tmp/swf", })), })); vi.mock("@/lib/services/swf/nitro-builder", () => ({ parseNitroBundle })); vi.mock("node:fs", async (orig) => { const real = (await orig()) as typeof import("node:fs"); return { ...real, promises: { ...real.promises, readFile: fsReadFile as AnyFn, unlink: fsUnlink as AnyFn, }, }; }); vi.mock("@/lib/prisma", () => ({ prisma: { $executeRaw: execRaw as AnyFn, $queryRaw: queryRaw as AnyFn, $executeRawUnsafe: execRaw as AnyFn, $queryRawUnsafe: queryRaw as AnyFn, }, })); import { cloneSingleFurni, parseFurnidata } from "./clone-import"; const SOURCE = { id: "s", name: "X", furnidataUrl: "https://x/fd.json", nitroBaseUrl: "https://x/furni", iconBaseUrl: "https://x/icons", }; const ENTRY = { id: 5, classname: "bc_sofa", name: "BC Sofa", description: "d", xdim: 2, ydim: 1, canstandon: false, cansiton: true, canlayon: false, customparams: "", }; describe("clone-import", () => { it("parseFurnidata normalizes room + wall items with itemType", () => { const list = parseFurnidata({ roomitemtypes: { furnitype: [ENTRY] }, wallitemtypes: { furnitype: [{ ...ENTRY, classname: "wall_x" }] }, }); expect(list.find((e) => e.classname === "bc_sofa")?.itemType).toBe("s"); expect(list.find((e) => e.classname === "wall_x")?.itemType).toBe("i"); }); it("cloneSingleFurni downloads .nitro+icon, inserts items_base, appends FurnitureData, makes catalog item", async () => { // Reset mocks to clear state from other tests. downloadFile.mockReset(); downloadFile.mockResolvedValue({ ok: true, size: 200 }); execRaw.mockReset(); execRaw.mockResolvedValue(1); queryRaw.mockReset(); // First call: dedup check (no existing row); second call: allocateItemsBaseId MAX(id)+1. queryRaw.mockResolvedValueOnce([]).mockResolvedValueOnce([{ next: 42 }]); appendFurniEntry.mockReset(); appendFurniEntry.mockResolvedValue(undefined); const r = await cloneSingleFurni({ source: SOURCE, entry: { ...ENTRY, itemType: "s" }, }); expect(r.ok).toBe(true); expect(downloadFile).toHaveBeenCalledWith( "https://x/furni/bc_sofa.nitro", expect.any(String), expect.any(Object), ); expect(downloadFile).toHaveBeenCalledWith( "https://x/icons/bc_sofa_icon.png", expect.any(String), expect.objectContaining({ validate: "png" }), ); expect(appendFurniEntry).toHaveBeenCalled(); expect(execRaw).toHaveBeenCalled(); // items_base insert + catalog }); it("skips when classname already exists (dedup)", async () => { queryRaw.mockReset(); queryRaw.mockResolvedValueOnce([{ id: 5 }]); // existing items_base row const r = await cloneSingleFurni({ source: SOURCE, entry: { ...ENTRY, itemType: "s" }, }); expect(r.ok).toBe(false); expect(r.skipped).toBe(true); }); it("rolls back: unlinks .nitro and icon, skips FurnitureData when items_base INSERT rejects", async () => { const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); downloadFile.mockReset(); downloadFile.mockResolvedValue({ ok: true, size: 200 }); fsUnlink.mockReset(); fsUnlink.mockResolvedValue(undefined); appendFurniEntry.mockReset(); queryRaw.mockReset(); execRaw.mockReset(); // Dedup query: no existing row. queryRaw.mockResolvedValueOnce([]); // allocateItemsBaseId: MAX(id)+1 query returns next id. queryRaw.mockResolvedValueOnce([{ next: 7 }]); // items_base INSERT rejects. execRaw.mockRejectedValueOnce(new Error("Duplicate entry")); try { const r = await cloneSingleFurni({ source: SOURCE, entry: { ...ENTRY, itemType: "s" }, }); // (a) .nitro must be unlinked. expect(fsUnlink).toHaveBeenCalledWith( expect.stringContaining("bc_sofa.nitro"), ); // (b) icon must also be unlinked. expect(fsUnlink).toHaveBeenCalledWith( expect.stringContaining("bc_sofa_icon.png"), ); // (c) appendFurniEntry must NOT have been called. expect(appendFurniEntry).not.toHaveBeenCalled(); // (d) result must be ok === false. expect(r.ok).toBe(false); expect(r.error).toMatch(/items_base insert failed/); } finally { warn.mockRestore(); } }); });