Files
EpicNext-Cms/src/features/catalog/server/duplicate-commands.test.ts
T
Simo 55a47949f1
CI / check (push) Successful in 1m14s
CI / deploy (push) Successful in 1m11s
feat(catalog): add reviewed bulk edits and complete category duplication
2026-09-06 20:25:19 +02:00

180 lines
5.2 KiB
TypeScript

import type { SQL } from "drizzle-orm";
import { MySqlDialect } from "drizzle-orm/mysql-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
const state = vi.hoisted(() => ({
pages: [
{ id: 1, parent_id: -1, caption: "Source", enabled: "1", includes: "2" },
{ id: 2, parent_id: 1, caption: "Child", enabled: "1", includes: "" },
],
offers: [
{
id: 10,
page_id: 2,
item_ids: "8;8",
cost_credits: 23,
limited_stack: 10,
offer_id: 10,
},
],
bases: [{ id: 8 }],
queries: [] as { sql: string; params: unknown[] }[],
writes: 0,
failAt: 0,
commits: 0,
rollbacks: 0,
}));
vi.mock("@/lib/services/furni-import", () => ({
allocateCatalogItemId: async (fn: (id: number) => unknown) => fn(500),
}));
vi.mock("@/lib/db", () => ({
db: {
transaction: async (fn: (tx: unknown) => Promise<unknown>) => {
const saved = state.writes;
try {
const result = await fn({
execute: async (query: SQL) => {
const compiled = new MySqlDialect().sqlToQuery(query);
state.queries.push(compiled);
if (compiled.sql.startsWith("SELECT"))
return [
compiled.sql.includes("catalog_pages")
? state.pages
: compiled.sql.includes("items_base")
? state.bases
: state.offers,
[],
];
state.writes++;
if (state.writes === state.failAt) throw Error("Insert failed");
return [
{
insertId: compiled.sql.includes("`catalog_items`")
? 0
: 100 + state.writes,
},
[],
];
},
});
state.commits++;
return result;
} catch (error) {
state.writes = saved;
state.rollbacks++;
throw error;
}
},
},
}));
import {
duplicateCategoryCommand,
previewDuplicateCommand,
} from "./duplicate-commands";
const input = {
kind: "normal" as const,
sourceId: 1,
parentId: -1,
name: "Copy",
};
beforeEach(() => {
state.pages = [
{ id: 1, parent_id: -1, caption: "Source", enabled: "1", includes: "2" },
{ id: 2, parent_id: 1, caption: "Child", enabled: "1", includes: "" },
];
state.offers = [
{
id: 10,
page_id: 2,
item_ids: "8;8",
cost_credits: 23,
limited_stack: 10,
offer_id: 10,
},
];
state.bases = [{ id: 8 }];
state.queries = [];
state.writes = 0;
state.failAt = 0;
state.commits = 0;
state.rollbacks = 0;
});
describe("atomic subtree duplication", () => {
it("previews without writes and copies descendants, offers and internal references", async () => {
const preview = await previewDuplicateCommand(input);
expect(preview).toMatchObject({ pages: 2, offers: 1 });
expect(state.writes).toBe(0);
state.queries = [];
await duplicateCategoryCommand(preview);
const queries = state.queries;
expect(queries.slice(0, 3).map((q) => q.sql)).toEqual([
expect.stringContaining("catalog_pages"),
expect.stringContaining("catalog_items"),
expect.stringContaining("items_base"),
]);
expect(queries.slice(0, 3).every((q) => q.sql.endsWith("FOR UPDATE"))).toBe(
true,
);
const inserts = queries.filter((q) => q.sql.startsWith("INSERT"));
expect(inserts).toHaveLength(3);
expect(inserts[0].params).toContain("0");
expect(inserts[1].params).toContain(101);
expect(inserts[1].params).toContain("1");
expect(inserts[2].params.filter((p) => p === 500)).toHaveLength(2);
expect(inserts[2].params).toEqual(
expect.arrayContaining(["102", "8;8", 23, 10, 500]),
);
expect(queries.find((q) => q.sql.startsWith("UPDATE"))?.params).toEqual([
"102",
101,
]);
expect(state.pages[0].caption).toBe("Source");
});
it("detects stale source fields before any insert", async () => {
const preview = await previewDuplicateCommand(input);
state.offers[0].cost_credits = 99;
await expect(duplicateCategoryCommand(preview)).rejects.toThrow("changed");
expect(state.writes).toBe(0);
});
it("rolls back pages if an offer insert fails", async () => {
const preview = await previewDuplicateCommand(input);
state.failAt = 4;
await expect(duplicateCategoryCommand(preview)).rejects.toThrow(
"Insert failed",
);
expect(state.writes).toBe(0);
expect(state.rollbacks).toBe(1);
});
it("rejects missing furniture without writes", async () => {
state.bases = [];
await expect(previewDuplicateCommand(input)).rejects.toThrow("Furniture");
expect(state.writes).toBe(0);
});
it("copies BC with database-generated IDs and no added currency columns", async () => {
const preview = await previewDuplicateCommand({ ...input, kind: "bc" });
state.queries = [];
await duplicateCategoryCommand(preview);
const inserts = state.queries.filter((q) => q.sql.startsWith("INSERT"));
expect(inserts[2].sql).toContain("catalog_items_bc");
expect(inserts[2].sql).not.toMatch(/`id`=/);
expect(inserts[0].sql).not.toContain("caption_save");
});
});
it("invalidates the preview after the new root appears among destination siblings", async () => {
const preview = await previewDuplicateCommand(input);
const result = await duplicateCategoryCommand(preview);
state.pages.push({
id: result.id,
parent_id: 0,
caption: "Copy",
enabled: "0",
includes: "",
});
const writes = state.writes;
await expect(duplicateCategoryCommand(preview)).rejects.toThrow("changed");
expect(state.writes).toBe(writes);
});