// @vitest-environment node import { describe, expect, it, vi } from 'vitest' 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 () => { 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')) 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/) }) })