Files
EpicNext-Cms/src/lib/services/upload-import.test.ts
T
openhands 306e209e29
Gitea Actions Runner Test / test-job (push) Successful in 2s
CI / check (push) Successful in 39s
CI / tests-unit (push) Successful in 2m3s
CI / tests-integration (push) Successful in 2m7s
CI / tests-ui (push) Successful in 2m50s
CI / preflight (push) Skipped
CI / deploy (push) Failing after 1m45s
fix(nitro): normalise uploaded bundles so meta.image matches the texture
An uploaded .nitro was written to disk byte-for-byte, so a bundle from a
third-party tool that ships a WebP member while still pointing
spritesheet.meta.image at a .png was accepted and stored as-is. The client
resolves the spritesheet through that pointer, so the result was a file
that validates fine and then renders nothing.

Re-write the bundle through createNitroBundle on import, which labels the
member from the actual bytes and repairs the pointer. No texture is
re-encoded, so the bytes stay identical, and the member keeps the base
name it arrived with so `chair*2` colour variants that share the `chair`
library are not renamed.
2026-09-27 15:47:44 +02:00

169 lines
4.6 KiB
TypeScript

// @ts-nocheck
// @vitest-environment node
import { promises as fs } from "node:fs";
import os from "node:os";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getRuntimePath } from "@/lib/utils/runtime-path";
const {
appendFurniEntryMock,
assetTargets,
dbExecuteMock,
getOrCreateCategoryPageMock,
} = vi.hoisted(() => ({
appendFurniEntryMock: vi.fn(),
assetTargets: {
swfDir: "",
iconDir: "",
nitroDir: "",
mirrorDirs: [] as Array<{
swfDir: string;
iconDir: string;
nitroDir: string;
}>,
},
dbExecuteMock: vi.fn(),
getOrCreateCategoryPageMock: vi.fn(),
}));
vi.mock("drizzle-orm", () => ({
eq: vi.fn(() => ({})),
sql: (strings: TemplateStringsArray, ...values: unknown[]) => ({
strings,
values,
}),
}));
vi.mock("@/lib/db", async () => ({
...(await import("@/test/db-helpers")).createDbHelpers(dbExecuteMock),
ItemsBase: { id: "id", itemName: "item_name" },
db: {
execute: dbExecuteMock,
select: () => ({
from: () => ({
where: () => ({
limit: async () => [],
}),
}),
}),
},
}));
vi.mock("@/lib/services/furni-asset-dirs", () => ({
getFurniAssetDirs: vi.fn(async () => assetTargets),
getFurniAssetWriteTargets: vi.fn(async () => assetTargets),
}));
vi.mock("@/lib/services/furni-data", () => ({
appendFurniEntry: appendFurniEntryMock,
buildFurniEntry: vi.fn(() => ({ id: 501 })),
}));
vi.mock("@/lib/services/furni-import", () => ({
allocateCatalogItemId: vi.fn(
async (insertFn: (nextId: number) => Promise<unknown>) => insertFn(701),
),
autoPriceFurni: vi.fn(() => ({ credits: 3, points: 0, pointsType: 0 })),
CATEGORY_PAGE: {},
classifyFurni: vi.fn(() => "other"),
ensureDirectories: vi.fn(async () => {
const dirs = [
assetTargets.swfDir,
assetTargets.iconDir,
assetTargets.nitroDir,
...assetTargets.mirrorDirs.flatMap((target) => [
target.swfDir,
target.iconDir,
target.nitroDir,
]),
];
await Promise.all(dirs.map((dir) => fs.mkdir(dir, { recursive: true })));
}),
getOrCreateCategoryPage: getOrCreateCategoryPageMock,
IMPORTED_PAGE_CAPTION: "Imported Furniture",
IMPORTED_PAGE_CAPTION_SAVE: "imported_furni",
}));
const nitroBuilderMock = vi.hoisted(() => ({
createNitroBundle: vi.fn((json: object) =>
Buffer.from(`rebuilt:${JSON.stringify(json)}`, "utf-8"),
),
}));
vi.mock("@/lib/services/swf/nitro-builder", () => ({
parseNitroBundle: vi.fn(() => ({
json: { xdim: 1, ydim: 1 },
texture: Buffer.from("texture-bytes"),
textureFileName: "chair.png",
})),
createNitroBundle: nitroBuilderMock.createNitroBundle,
}));
import {
__resetUploadItemsBaseAllocatorForTests,
uploadSingleFurni,
} from "./upload-import";
describe("uploadSingleFurni live asset mirrors", () => {
let tempDir: string;
beforeEach(async () => {
__resetUploadItemsBaseAllocatorForTests();
tempDir = await fs.mkdtemp(getRuntimePath(os.tmpdir(), "upload-furni-"));
assetTargets.swfDir = getRuntimePath(tempDir, "cms", "swf");
assetTargets.iconDir = getRuntimePath(tempDir, "cms", "icons");
assetTargets.nitroDir = getRuntimePath(tempDir, "cms", "furniture");
assetTargets.mirrorDirs = [
{
swfDir: assetTargets.swfDir,
iconDir: getRuntimePath(tempDir, "Gamedata", "icons"),
nitroDir: getRuntimePath(tempDir, "Gamedata", "bundled", "furniture"),
},
];
dbExecuteMock.mockReset();
dbExecuteMock
.mockResolvedValueOnce([[{ next: 501 }], undefined])
.mockResolvedValue(undefined);
getOrCreateCategoryPageMock.mockResolvedValue("imp_other");
appendFurniEntryMock.mockResolvedValue(undefined);
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
vi.clearAllMocks();
});
it("copies the Nitro bundle and icon into the directories served by production", async () => {
const nitroBuffer = Buffer.from("nitro-bundle");
const iconBuffer = Buffer.from("png-icon");
const result = await uploadSingleFurni({
classname: "chair",
name: "Chair",
itemType: "s",
nitroBuffer,
iconBuffer,
});
expect(result.ok).toBe(true);
// The bundle is re-written through the builder so `meta.image` can never
// disagree with the texture member, while the member keeps the name it
// arrived with (colour variants share a library name).
expect(nitroBuilderMock.createNitroBundle).toHaveBeenCalledWith(
{ xdim: 1, ydim: 1 },
Buffer.from("texture-bytes"),
"chair",
);
await expect(
fs.readFile(
getRuntimePath(assetTargets.mirrorDirs[0].nitroDir, "chair.nitro"),
),
).resolves.toEqual(Buffer.from('rebuilt:{"xdim":1,"ydim":1}', "utf-8"));
await expect(
fs.readFile(
getRuntimePath(assetTargets.mirrorDirs[0].iconDir, "chair_icon.png"),
),
).resolves.toEqual(iconBuffer);
});
});