import { getPayload, Payload } from "payload"; import config from "@/payload.config"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { mkdirSync, rmSync, writeFileSync } from "fs"; import { join } from "path"; import { tmpdir } from "os"; import { ASSET_TYPES, enrichPlanMedia, linkWeaponCompatibility, planRecord, upsertPlan, type CatalogRecord, type MediaEnv, } from "@/lib/arma-import/catalog"; let payload: Payload; const RUN = `armacat-${Date.now().toString(36)}`; let fuelId: number; let createdFuel = false; const assetIds: number[] = []; const vehicleIds: number[] = []; const mediaIds: number[] = []; const SHOT_DIR = join(tmpdir(), `ptf-shots-${RUN}`); // 1x1 PNG: enough for the upload path, tiny enough to inline. const PNG_1PX = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", "base64", ); function mediaEnv(screenshots = ""): MediaEnv { return { paaRoot: "", screenshots, outDir: SHOT_DIR, hemtt: "" }; } describe("Arma catalog import", () => { beforeAll(async () => { const payloadConfig = await config; payload = await getPayload({ config: payloadConfig }); // Vehicles need a fuel resource for the fuel group; reuse one if a prior // fixture left it, otherwise create it (and only then clean it up). const existingFuel = await payload.find({ collection: "resources", where: { name: { equals: "Fuel" } }, limit: 1, depth: 0, overrideAccess: true, }); if (existingFuel.docs[0]) { fuelId = existingFuel.docs[0].id; } else { const fuel = await payload.create({ collection: "resources", data: { name: "Fuel", codeName: `res_fuel_${RUN}`, unitOfMeasure: "liter", approvalStatus: "in_progress", type: "fluid", baseValue: 1, rarity: "common", massPerUnit: 0, gridWidth: 1, gridHeight: 1, tradeable: false, }, depth: 0, overrideAccess: true, }); fuelId = fuel.id; createdFuel = true; } }); afterAll(async () => { for (const id of vehicleIds) { await payload.delete({ collection: "vehicles", id, overrideAccess: true }).catch(() => {}); } for (const id of assetIds) { await payload.delete({ collection: "assets", id, overrideAccess: true }).catch(() => {}); } for (const id of mediaIds) { await payload.delete({ collection: "media", id, overrideAccess: true }).catch(() => {}); } rmSync(SHOT_DIR, { recursive: true, force: true }); if (createdFuel && fuelId) { await payload.delete({ collection: "resources", id: fuelId, overrideAccess: true }).catch(() => {}); } }); describe("planRecord classification", () => { it("trusts type for every contract asset type", () => { for (const t of ASSET_TYPES) { const rec: CatalogRecord = { classname: `PTF_Test_Thing_${t}`, type: t, displayName: "Thing", picture: "", mod: "CBA_3den", description: "", mass: 1, extra: "", }; const plan = planRecord(rec); expect(plan).toBeDefined(); expect(plan!.kind).toBe("asset"); expect(plan!.assetType).toBe(t); } }); it("routes vehicle records to the vehicles collection with transportMode from extra", () => { const air = planRecord({ classname: "B_Heli_Light_01_F", type: "vehicle", displayName: "Ghost Hawk", picture: "", mod: "Arma 3", description: "", mass: 3000, extra: "air", }); expect(air!.kind).toBe("vehicle"); expect(air!.transportMode).toBe("air"); const sea = planRecord({ classname: "C_Boat_Civil_01_F", type: "vehicle", extra: "sea" }); expect(sea!.transportMode).toBe("sea"); }); it("defaults transportMode to ground when extra is not a valid mode", () => { const plan = planRecord({ classname: "B_MRAP_01_F", type: "vehicle", extra: "submarine" }); expect(plan!.kind).toBe("vehicle"); expect(plan!.transportMode).toBe("ground"); }); it("falls back to heuristics for unknown types (miscellaneous, not item)", () => { const plan = planRecord({ classname: "PTF_Test_Odd_Gadget", type: "qrd_mysterious_device" }); expect(plan!.kind).toBe("asset"); expect(plan!.assetType).toBe("miscellaneous"); }); it("falls back to the legacy vehicle heuristic for unknown types", () => { const plan = planRecord({ classname: "B_Truck_01_transport_F", type: "who_knows" }); expect(plan!.kind).toBe("vehicle"); expect(plan!.transportMode).toBe("ground"); }); it("captures magazine round counts from extra", () => { const plan = planRecord({ classname: "30rnd_556x45_Mag", type: "magazine", extra: 30 }); expect(plan!.kind).toBe("asset"); expect(plan!.roundCapacity).toBe(30); expect(plan!.assetType).toBe("magazine"); }); it("ignores numeric extra for non-magazine assets", () => { const plan = planRecord({ classname: "PTF_Test_Rifle", type: "weapon", extra: 30 }); expect(plan!.assetType).toBe("weapon"); expect(plan!.roundCapacity).toBeUndefined(); }); it("supports legacy class/category/name aliases and icon-map overrides", () => { const plan = planRecord( { class: "Legacy_Thing", category: "optic", name: "Legacy Optic" }, { icons: { legacy_thing: "\\a3\\data\\ui\\icon.paa" } }, ); expect(plan!.className).toBe("Legacy_Thing"); expect(plan!.assetType).toBe("optic"); expect(plan!.displayName).toBe("Legacy Optic"); expect(plan!.pictureRef).toBe("\\a3\\data\\ui\\icon.paa"); }); it("defaults mod to Arma 3 and rejects records without a classname", () => { const plan = planRecord({ classname: "PTF_Test_Thing", type: "weapon" }); expect(plan).toBeDefined(); expect(plan!.mod).toBe("Arma 3"); expect(planRecord({ type: "weapon" })).toBeUndefined(); }); }); describe("upsertPlan assets", () => { const assetClassName = `${RUN}_asset_rifle`; it("creates an asset with correct className/assetType and Asset Metadata.mod", async () => { const plan = planRecord({ classname: assetClassName, type: "weapon", displayName: `${RUN} Rifle`, picture: "", mod: "PTF_Test_Mod", description: "An import test rifle.", mass: 2, extra: "", })!; const res = await upsertPlan(payload, plan); expect(res.action).toBe("created"); expect(res.collection).toBe("assets"); assetIds.push(res.id); const doc = await payload.findByID({ collection: "assets", id: res.id, depth: 0, overrideAccess: true }); expect(doc.className).toBe(assetClassName); expect(doc.assetType).toBe("weapon"); expect(doc.name).toBe(`${RUN} Rifle`); expect(doc["Asset Metadata"]?.mod).toBe("PTF_Test_Mod"); expect(doc["Asset Metadata"]?.gameDescription).toBe("An import test rifle."); expect(doc.approvalStatus).toBe("in_progress"); }); it("updates on re-run without duplicating and preserves approvalStatus and Virtual Data", async () => { const assetId = assetIds[0]; // Simulate human edits before the second import run. await payload.update({ collection: "assets", id: assetId, data: { approvalStatus: "approved", tradeable: true }, overrideAccess: true, }); const res = await upsertPlan( payload, planRecord({ classname: assetClassName, type: "weapon", displayName: `${RUN} Rifle MkII`, picture: "", mod: "PTF_Test_Mod_2", description: "Updated in-game description.", mass: 2, extra: "", })!, undefined, // assets never need fuel ); expect(res.action).toBe("updated"); expect(res.collection).toBe("assets"); expect(res.id).toBe(assetId); const count = await payload.count({ collection: "assets", where: { className: { equals: assetClassName } }, }); expect(count.totalDocs).toBe(1); const doc = await payload.findByID({ collection: "assets", id: assetId, depth: 0, overrideAccess: true }); expect(doc.name).toBe(`${RUN} Rifle MkII`); expect(doc["Asset Metadata"]?.mod).toBe("PTF_Test_Mod_2"); expect(doc["Asset Metadata"]?.gameDescription).toBe("Updated in-game description."); expect(doc.approvalStatus).toBe("approved"); // never reset by re-import expect(doc.tradeable).toBe(true); // Virtual Data untouched }); }); describe("adding images in a later import pass", () => { const lateClassName = `${RUN}_late_image_rifle`; function planFor(screenshot: string) { const plan = planRecord({ classname: lateClassName, type: "weapon", displayName: `${RUN} Late Image Rifle`, picture: "", mod: "PTF_Test_Mod", description: "", mass: 1, extra: "", })!; enrichPlanMedia(plan, mediaEnv(screenshot)); return plan; } it("imports the catalog with no image, then attaches one on a later run", async () => { // Pass 1: data only (no --screenshots). const first = await upsertPlan(payload, planFor("")); expect(first.action).toBe("created"); assetIds.push(first.id); let doc = await payload.findByID({ collection: "assets", id: first.id, depth: 0, overrideAccess: true }); expect(doc["Asset Metadata"]?.thumbnail).toBeFalsy(); // Pass 2: the screenshot now exists on disk. mkdirSync(SHOT_DIR, { recursive: true }); writeFileSync(join(SHOT_DIR, `${lateClassName}.png`), PNG_1PX); const second = await upsertPlan(payload, planFor(SHOT_DIR)); expect(second.action).toBe("updated"); expect(second.id).toBe(first.id); // upsert, not a duplicate doc = await payload.findByID({ collection: "assets", id: first.id, depth: 0, overrideAccess: true }); const thumbnail = doc["Asset Metadata"]?.thumbnail; expect(thumbnail).toBeTruthy(); if (typeof thumbnail === "number") mediaIds.push(thumbnail); else if (thumbnail && typeof thumbnail === "object") mediaIds.push(thumbnail.id); const count = await payload.count({ collection: "assets", where: { className: { equals: lateClassName } }, }); expect(count.totalDocs).toBe(1); }); it("keeps the image when re-run, and repairs a cleared one", async () => { const assetId = assetIds[assetIds.length - 1]; const before = await payload.findByID({ collection: "assets", id: assetId, depth: 0, overrideAccess: true }); const originalId = typeof before["Asset Metadata"]?.thumbnail === "number" ? (before["Asset Metadata"]!.thumbnail as number) : (before["Asset Metadata"]!.thumbnail as { id: number }).id; // Re-run: the media doc already exists, so it must not be duplicated. await upsertPlan(payload, planFor(SHOT_DIR)); const after = await payload.findByID({ collection: "assets", id: assetId, depth: 0, overrideAccess: true }); const afterId = typeof after["Asset Metadata"]?.thumbnail === "number" ? (after["Asset Metadata"]!.thumbnail as number) : (after["Asset Metadata"]!.thumbnail as { id: number }).id; expect(afterId).toBe(originalId); // A cleared thumbnail is repaired by the next run (media already exists). await payload.update({ collection: "assets", id: assetId, data: { "Asset Metadata": { thumbnail: null } }, overrideAccess: true, }); await upsertPlan(payload, planFor(SHOT_DIR)); const repaired = await payload.findByID({ collection: "assets", id: assetId, depth: 0, overrideAccess: true }); expect(repaired["Asset Metadata"]?.thumbnail).toBeTruthy(); }); }); describe("weapon compatibility", () => { const rifle = `${RUN}_compat_rifle`; const mag = `${RUN}_compat_mag`; const optic = `${RUN}_compat_optic`; const light = `${RUN}_compat_light`; function catalog() { return [ { classname: rifle, type: "weapon", displayName: "Compat Rifle", compat: { slot: "primary", // duplicate + different case + a ref that was never imported magazines: [mag, mag.toUpperCase(), `${RUN}_not_in_app`], attachments: [optic, light], } }, { classname: mag, type: "magazine", displayName: "Compat Mag", extra: 30 }, { classname: optic, type: "optic", displayName: "Compat Optic" }, { classname: light, type: "miscellaneous", displayName: "Compat Light" }, ].map((r) => planRecord(r as CatalogRecord)!); } it("parses compat on weapons only, deduplicating classnames", () => { const plans = catalog(); expect(plans[0].compat).toEqual({ slot: "primary", magazines: [mag, `${RUN}_not_in_app`], attachments: [optic, light], }); expect(plans[1].compat).toBeUndefined(); expect(planRecord({ classname: "x", type: "magazine", compat: { magazines: ["y"] } })!.compat).toBeUndefined(); }); it("links magazines/attachments as references, merges on re-run, never duplicates", async () => { const plans = catalog(); const ids: Record = {}; for (const plan of plans) { const res = await upsertPlan(payload, plan); assetIds.push(res.id); ids[plan.className] = res.id; } const first = await linkWeaponCompatibility(payload, plans); expect(first.updated).toBe(1); expect(first.unresolved).toBe(1); // the never-imported magazine expect(first.skippedType).toBe(1); // rail light is "miscellaneous" let doc = await payload.findByID({ collection: "assets", id: ids[rifle], depth: 0, overrideAccess: true }); const wc = () => (doc as unknown as { "Weapon Configuration": Record })["Weapon Configuration"]; expect(wc().slotType).toBe("primary"); expect(wc().compatibleMagazines).toEqual([ids[mag]]); expect(wc().compatibleAttachments).toEqual([ids[optic]]); // A link added by hand survives, and re-running adds nothing twice. const extraOptic = await upsertPlan(payload, planRecord({ classname: `${RUN}_hand_optic`, type: "optic" })!); assetIds.push(extraOptic.id); await payload.update({ collection: "assets", id: ids[rifle], data: { "Weapon Configuration": { compatibleAttachments: [ids[optic], extraOptic.id] } } as never, overrideAccess: true, }); const second = await linkWeaponCompatibility(payload, plans); expect(second.updated).toBe(0); expect(second.unchanged).toBe(1); doc = await payload.findByID({ collection: "assets", id: ids[rifle], depth: 0, overrideAccess: true }); expect(wc().compatibleMagazines).toEqual([ids[mag]]); expect(wc().compatibleAttachments).toEqual([ids[optic], extraOptic.id]); // Each referenced item still exists exactly once. for (const name of [mag, optic]) { const count = await payload.count({ collection: "assets", where: { className: { equals: name } } }); expect(count.totalDocs).toBe(1); } }); }); describe("upsertPlan vehicles", () => { const vehicleClassname = `${RUN}_heli_ghost`; it("creates a vehicle with classname and transportMode air", async () => { const plan = planRecord({ classname: vehicleClassname, type: "vehicle", displayName: `${RUN} Ghost Hawk`, picture: "", mod: "PTF_Test_Mod", description: "An import test helicopter.", mass: 3000, extra: "air", })!; const res = await upsertPlan(payload, plan, fuelId); expect(res.action).toBe("created"); expect(res.collection).toBe("vehicles"); vehicleIds.push(res.id); const doc = await payload.findByID({ collection: "vehicles", id: res.id, depth: 0, overrideAccess: true }); expect(doc.classname).toBe(vehicleClassname); expect(doc.name).toBe(`${RUN} Ghost Hawk`); expect(doc.transportMode).toBe("air"); expect(doc.fuel.fuelType).toBe(fuelId); expect(doc.fuel.fuelCapacity).toBe(100); expect(doc.approvalStatus).toBe("in_progress"); }); it("updates an existing vehicle without duplicating", async () => { const vehicleId = vehicleIds[0]; const res = await upsertPlan( payload, planRecord({ classname: vehicleClassname, type: "vehicle", displayName: `${RUN} Ghost Hawk II`, extra: "air", })!, fuelId, ); expect(res.action).toBe("updated"); expect(res.id).toBe(vehicleId); const count = await payload.count({ collection: "vehicles", where: { classname: { equals: vehicleClassname } }, }); expect(count.totalDocs).toBe(1); const doc = await payload.findByID({ collection: "vehicles", id: vehicleId, depth: 0, overrideAccess: true }); expect(doc.name).toBe(`${RUN} Ghost Hawk II`); expect(doc.transportMode).toBe("air"); }); }); });