180 lines
5.5 KiB
TypeScript
180 lines
5.5 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { MySqlDialect } from "drizzle-orm/mysql-core";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
official: vi.fn(),
|
|
download: vi.fn(),
|
|
execute: vi.fn(),
|
|
select: vi.fn(),
|
|
stat: vi.fn(),
|
|
conflict: vi.fn(),
|
|
build: vi.fn(),
|
|
append: vi.fn(),
|
|
}));
|
|
vi.mock("@/lib/db", () => ({
|
|
db: { select: mocks.select, execute: mocks.execute },
|
|
ItemsBase: {
|
|
id: "id",
|
|
itemName: "item_name",
|
|
spriteId: "sprite_id",
|
|
type: "type",
|
|
},
|
|
CatalogPages: {},
|
|
}));
|
|
vi.mock("node:fs", () => ({
|
|
existsSync: () => true,
|
|
promises: {
|
|
stat: mocks.stat,
|
|
readFile: vi.fn().mockResolvedValue(Buffer.alloc(256)),
|
|
},
|
|
}));
|
|
vi.mock("@/lib/services/furni-asset-dirs", () => ({
|
|
DEFAULT_FURNI_NITRO_DIR: "/nitro",
|
|
getFurniAssetWriteTargets: async () => ({
|
|
swfDir: "/swf",
|
|
nitroDir: "/nitro",
|
|
iconDir: "/icons",
|
|
mirrorDirs: [],
|
|
}),
|
|
}));
|
|
vi.mock("@/lib/services/furni-data", () => ({
|
|
findFurniDataIdConflict: mocks.conflict,
|
|
buildFurniEntry: mocks.build,
|
|
appendFurniEntry: mocks.append,
|
|
}));
|
|
vi.mock("@/lib/services/habbo-furnidata-cache", () => ({
|
|
lookupOfficialHabboFurni: mocks.official,
|
|
}));
|
|
vi.mock("@/lib/services/habbo-gamedata-hotel", () => ({
|
|
getHabboGamedataHotel: async () => "it",
|
|
}));
|
|
vi.mock("@/lib/services/import/core/download", () => ({
|
|
downloadFile: vi.fn(),
|
|
tryDownloadCandidates: mocks.download,
|
|
}));
|
|
vi.mock("@/lib/services/swf-to-nitro", () => ({
|
|
parseNitroBundle: () => ({ json: {} }),
|
|
extractIconFromSwfAsync: async () => null,
|
|
}));
|
|
|
|
import { importSingleFurni } from "./furni-import";
|
|
|
|
const input = {
|
|
id: 42,
|
|
classname: "recycler_scrapbookA",
|
|
name: "Scrapbook",
|
|
description: "Book",
|
|
type: "flooritem",
|
|
revision: 1,
|
|
category: "unknown",
|
|
repairExisting: true,
|
|
};
|
|
const dialect = new MySqlDialect();
|
|
function queries() {
|
|
return mocks.execute.mock.calls.map(([query]) => dialect.sqlToQuery(query));
|
|
}
|
|
describe("completion of existing furniture", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mocks.official.mockResolvedValue(null);
|
|
mocks.download.mockResolvedValue(false);
|
|
const rows = [
|
|
[{ id: 2000037257, spriteId: 9001, type: "s" }],
|
|
[{ itemName: input.classname }],
|
|
];
|
|
mocks.select.mockImplementation(() => ({
|
|
from: () => ({ where: () => ({ limit: async () => rows.shift() }) }),
|
|
}));
|
|
mocks.stat.mockResolvedValue({ size: 256 });
|
|
mocks.conflict.mockResolvedValue(null);
|
|
mocks.execute.mockResolvedValue([[{ id: 123, page_id: "15" }], []]);
|
|
mocks.build.mockImplementation((value) => value);
|
|
});
|
|
it("reuses local item and sprite IDs without rewriting SQL settings or offers", async () => {
|
|
const result = await importSingleFurni(input);
|
|
expect(result.ok).toBe(true);
|
|
expect(result.itemId).toBe(2000037257);
|
|
expect(mocks.conflict).toHaveBeenCalledWith(9001, input.classname);
|
|
expect(mocks.build).toHaveBeenCalledWith(
|
|
expect.objectContaining({ id: 9001, spriteId: 9001 }),
|
|
);
|
|
expect(mocks.append).toHaveBeenCalled();
|
|
expect(
|
|
queries().every((query) => query.sql.trim().startsWith("SELECT")),
|
|
).toBe(true);
|
|
expect(queries()[0].params).toContain("2000037257");
|
|
});
|
|
it("never deletes existing SQL when the nitro cannot be recovered", async () => {
|
|
mocks.stat.mockRejectedValue(new Error("missing"));
|
|
const result = await importSingleFurni(input);
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toContain("preserved");
|
|
expect(mocks.execute).not.toHaveBeenCalled();
|
|
expect(mocks.append).not.toHaveBeenCalled();
|
|
});
|
|
it("rejects an occupied furnidata sprite before any mutation", async () => {
|
|
mocks.conflict.mockResolvedValue("other_furniture");
|
|
const result = await importSingleFurni(input);
|
|
expect(result.ok).toBe(false);
|
|
expect(mocks.execute).not.toHaveBeenCalled();
|
|
expect(mocks.append).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it("imports recycler_kintsugiB with an allocated ID throughout SQL and furnidata", async () => {
|
|
vi.clearAllMocks();
|
|
const selections = [[], [{ itemName: "recycler_kintsugi" }]];
|
|
mocks.select.mockImplementation(() => ({
|
|
from: () => ({ where: () => ({ limit: async () => selections.shift() }) }),
|
|
}));
|
|
mocks.conflict.mockResolvedValue(null);
|
|
mocks.stat.mockResolvedValue({ size: 256 });
|
|
mocks.build.mockImplementation((value) => value);
|
|
mocks.execute.mockImplementation(async (query) => {
|
|
const { sql, params } = dialect.sqlToQuery(query);
|
|
if (sql.includes("MAX(")) return [[{ maximum: 2000037257 }], []];
|
|
if (sql.includes("SELECT item_name"))
|
|
return [
|
|
params[0] === 16940 ? [{ item_name: "recycler_kintsugi" }] : [],
|
|
[],
|
|
];
|
|
if (sql.includes("SELECT id, page_id"))
|
|
return [[{ id: 123, page_id: "15" }], []];
|
|
return [[], []];
|
|
});
|
|
mocks.official.mockResolvedValue({ revision: 73706, xdim: 1, ydim: 1 });
|
|
const result = await importSingleFurni({
|
|
...input,
|
|
revision: 0,
|
|
id: 16940,
|
|
classname: "recycler_kintsugiB",
|
|
});
|
|
expect(result.ok).toBe(true);
|
|
expect(result.itemId).toBe(2000037258);
|
|
expect(mocks.download).toHaveBeenCalledWith(
|
|
expect.arrayContaining([
|
|
"https://images.habbo.com/dcr/hof_furni/73706/recycler_kintsugiB.swf",
|
|
]),
|
|
expect.any(String),
|
|
"swf",
|
|
);
|
|
expect(mocks.build).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
id: 2000037258,
|
|
spriteId: 2000037258,
|
|
classname: "recycler_kintsugiB",
|
|
}),
|
|
);
|
|
expect(
|
|
queries()
|
|
.find((q) => q.sql.includes("INSERT INTO items_base"))
|
|
?.params.slice(0, 2),
|
|
).toEqual([2000037258, 2000037258]);
|
|
expect(
|
|
queries().find((q) => q.sql.includes("SELECT id, page_id"))?.params,
|
|
).toContain("2000037258");
|
|
expect(result.warnings.join(" ")).toContain("16940");
|
|
});
|