feat(catalog): queue furniture imports with history and matching file attachments
This commit is contained in:
1 parent
775d14f861
commit
2619bec165
16 files changed
+1016
-232
No files matched your search
@@ -0,0 +1,114 @@
|
||||
import { beforeEach, expect, it, vi } from "vitest";
|
||||
import type { ImportJob } from "@/lib/furni/import-job";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
set: vi.fn(),
|
||||
eval: vi.fn(),
|
||||
list: vi.fn(),
|
||||
save: vi.fn(),
|
||||
import: vi.fn(),
|
||||
attachment: vi.fn(),
|
||||
export: vi.fn(),
|
||||
translate: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/redis", () => ({ redis: { set: mocks.set, eval: mocks.eval } }));
|
||||
vi.mock("@/lib/server-log", () => ({ logServerError: vi.fn() }));
|
||||
vi.mock("./audit", () => ({ logAudit: vi.fn() }));
|
||||
vi.mock("./catalog-git-queue", () => ({ withCatalogExport: mocks.export }));
|
||||
vi.mock("./clone-sources", () => ({ getSource: async () => null }));
|
||||
vi.mock("./furni-job-store", () => ({
|
||||
ImportJobStore: class {
|
||||
list = mocks.list;
|
||||
save = mocks.save;
|
||||
},
|
||||
}));
|
||||
vi.mock("./furni-import", () => ({
|
||||
ensureDirectories: async () => {},
|
||||
importSingleFurni: mocks.import,
|
||||
}));
|
||||
vi.mock("./furni-data-i18n", () => ({
|
||||
patchLocalizedFurniDataEntries: mocks.translate,
|
||||
}));
|
||||
vi.mock("./furni-attachment", () => ({
|
||||
readFurnitureAttachment: mocks.attachment,
|
||||
}));
|
||||
vi.mock("./rcon", () => ({
|
||||
rcon: { updateCatalog: async () => {}, updateItems: async () => {} },
|
||||
}));
|
||||
|
||||
vi.mock("./catalog-git-export", () => ({ runCatalogExport: vi.fn() }));
|
||||
|
||||
import { drainFurnitureImports } from "./furni-job-worker";
|
||||
|
||||
let job: ImportJob;
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.set.mockResolvedValue("OK");
|
||||
mocks.eval.mockResolvedValue(1);
|
||||
mocks.export.mockImplementation((fn) => fn());
|
||||
mocks.import.mockResolvedValue({ ok: true, itemId: 900, warnings: [] });
|
||||
mocks.save.mockResolvedValue(undefined);
|
||||
job = {
|
||||
id: "job",
|
||||
userId: 5,
|
||||
createdAt: "now",
|
||||
updatedAt: "now",
|
||||
state: "queued",
|
||||
translate: false,
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
classname: "chair",
|
||||
name: "Chair",
|
||||
description: "",
|
||||
type: "flooritem",
|
||||
revision: 1,
|
||||
category: "other",
|
||||
state: "pending",
|
||||
},
|
||||
],
|
||||
};
|
||||
mocks.list.mockResolvedValue([job]);
|
||||
});
|
||||
it("imports with repair enabled and saves the allocated local ID", async () => {
|
||||
await drainFurnitureImports();
|
||||
expect(mocks.import).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ classname: "chair", repairExisting: true }),
|
||||
);
|
||||
expect(job.state).toBe("completed");
|
||||
expect(job.items[0]).toMatchObject({ state: "done", itemId: 900 });
|
||||
expect(mocks.export).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("does not run a second worker while another holds the lease", async () => {
|
||||
mocks.set.mockResolvedValue(null);
|
||||
await drainFurnitureImports();
|
||||
expect(mocks.import).not.toHaveBeenCalled();
|
||||
});
|
||||
it("marks abandoned work interrupted without repeating an uncertain import", async () => {
|
||||
job.state = "running";
|
||||
job.items[0].state = "running";
|
||||
await drainFurnitureImports();
|
||||
expect(job.state).toBe("interrupted");
|
||||
expect(job.items[0].state).toBe("failed");
|
||||
expect(mocks.import).not.toHaveBeenCalled();
|
||||
});
|
||||
it("keeps errors for failed items and continues the batch", async () => {
|
||||
job.items.push({ ...job.items[0], classname: "table" });
|
||||
mocks.import.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
error: "Missing asset",
|
||||
warnings: [],
|
||||
});
|
||||
await drainFurnitureImports();
|
||||
expect(job.items.map((i) => i.state)).toEqual(["failed", "done"]);
|
||||
expect(job.items[0].error).toBe("Missing asset");
|
||||
});
|
||||
it("loads attachments using the job owner and exact classname", async () => {
|
||||
job.items[0].attachmentId = "upload";
|
||||
mocks.attachment.mockResolvedValue(Buffer.from("verified"));
|
||||
await drainFurnitureImports();
|
||||
expect(mocks.attachment).toHaveBeenCalledWith("upload", "chair", 5);
|
||||
expect(mocks.import).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ providedNitro: Buffer.from("verified") }),
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user