Validation: shared NaN-guarded Zod coercion helpers in schemas/common.ts replace the raw number|string transform idiom across every schema (the root-cause NaN bug class); emailOrEmpty + lenient isoDateString/timeString. Security: roles privilege-escalation closed; refresh-token family revocation on reuse; TOTP uses config params; read endpoints permission-guarded; received-invoices gross VAT on all paths; orders-pdf custom-items authz. Concurrency: $queryRaw SELECT...FOR UPDATE locks in ascending-id order (warehouse confirm/cancel, attendance lockUserRow); uniqueness checks moved into create transactions (TOCTOU -> 409); deterministic id tiebreak on second-precision timestamp ordering (plan resolveCell/resolveGrid, warehouse FIFO). Frontend: Rules-of-Hooks fixed across ~14 pages + PlanCellModal; UTC-date persisted fields; dashboard invalidation gaps; stale-closure confirm bugs. Tooling/tests: ESLint flat config (react-hooks/rules-of-hooks = error) + Prettier; tsconfig.test.json so tsc -b type-checks the tests; removed 3 dead deps; npm audit fix (8 -> 3). Suite 195 -> 247 (happy-path auth, FIFO oldest-first, flakiness fixes), isolated on app_test via .env.test with a hard-throw setup guard. Gates: tsc 0 | build 0 | vitest 247/247 | eslint 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
155 lines
5.1 KiB
TypeScript
155 lines
5.1 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
import prisma from "../config/database";
|
|
import {
|
|
listPlanCategories,
|
|
createPlanCategory,
|
|
updatePlanCategory,
|
|
deactivatePlanCategory,
|
|
deletePlanCategory,
|
|
isCategoryKeyActive,
|
|
slugifyCategory,
|
|
} from "../services/planCategory.service";
|
|
|
|
const created: number[] = [];
|
|
|
|
// Every test category's slug starts with "test_" AND contains "_zz" (labels all
|
|
// read "Test … ZZ"). Cleaning by this pattern — not just by in-memory ids —
|
|
// means a previous crashed run can't leave a `test_kategorie_zz` row behind that
|
|
// makes the dedupe test wrongly assert `_3`/`_4` instead of `_2`. The narrow
|
|
// pattern avoids touching real seed categories (work/other/…).
|
|
async function cleanupByPrefix() {
|
|
await prisma.plan_categories.deleteMany({
|
|
where: { key: { startsWith: "test_", contains: "_zz" } },
|
|
});
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
await cleanupByPrefix();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (created.length) {
|
|
await prisma.plan_categories.deleteMany({ where: { id: { in: created } } });
|
|
}
|
|
// Belt-and-suspenders: also remove anything matching the slug prefix in case
|
|
// a test threw before pushing its id into `created`.
|
|
await cleanupByPrefix();
|
|
await prisma.$disconnect();
|
|
});
|
|
|
|
describe("slugifyCategory", () => {
|
|
it("strips Czech diacritics and non-alphanumerics", () => {
|
|
expect(slugifyCategory("Cesta / Montáž")).toBe("cesta_montaz");
|
|
expect(slugifyCategory("Příprava")).toBe("priprava");
|
|
});
|
|
|
|
it("collapses an all-symbol label to an empty slug", () => {
|
|
// No alphanumerics survive → empty string. createPlanCategory's uniqueKey
|
|
// then falls back to "kategorie" so the row is still creatable.
|
|
expect(slugifyCategory("***")).toBe("");
|
|
expect(slugifyCategory(" / ")).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("plan category service", () => {
|
|
it("seeds are present and ordered", async () => {
|
|
const cats = await listPlanCategories(true);
|
|
const keys = cats.map((c) => c.key);
|
|
expect(keys).toContain("work");
|
|
expect(keys).toContain("other");
|
|
});
|
|
|
|
it("creates a category with a unique slug and next sort_order", async () => {
|
|
const res = await createPlanCategory({
|
|
label: "Test Kategorie ZZ",
|
|
color: "#123456",
|
|
});
|
|
if ("error" in res) throw new Error(res.error);
|
|
created.push(res.data.id);
|
|
expect(res.data.key).toBe("test_kategorie_zz");
|
|
expect(res.data.color).toBe("#123456");
|
|
expect(res.data.is_active).toBe(true);
|
|
expect(await isCategoryKeyActive("test_kategorie_zz")).toBe(true);
|
|
});
|
|
|
|
it("dedupes a colliding slug", async () => {
|
|
const res = await createPlanCategory({
|
|
label: "Test Kategorie ZZ",
|
|
color: "#222222",
|
|
});
|
|
if ("error" in res) throw new Error(res.error);
|
|
created.push(res.data.id);
|
|
expect(res.data.key).toBe("test_kategorie_zz_2");
|
|
});
|
|
|
|
it("updates label and color (key immutable)", async () => {
|
|
const res = await createPlanCategory({
|
|
label: "Test Upd ZZ",
|
|
color: "#aaaaaa",
|
|
});
|
|
if ("error" in res) throw new Error(res.error);
|
|
created.push(res.data.id);
|
|
const upd = await updatePlanCategory(res.data.id, {
|
|
label: "Test Upd2 ZZ",
|
|
color: "#bbbbbb",
|
|
});
|
|
if ("error" in upd) throw new Error(upd.error);
|
|
expect(upd.data.label).toBe("Test Upd2 ZZ");
|
|
expect(upd.data.color).toBe("#bbbbbb");
|
|
expect(upd.data.key).toBe("test_upd_zz");
|
|
});
|
|
|
|
it("deactivate hides the key from active check", async () => {
|
|
const res = await createPlanCategory({
|
|
label: "Test Deact ZZ",
|
|
color: "#cccccc",
|
|
});
|
|
if ("error" in res) throw new Error(res.error);
|
|
created.push(res.data.id);
|
|
await deactivatePlanCategory(res.data.id);
|
|
expect(await isCategoryKeyActive("test_deact_zz")).toBe(false);
|
|
});
|
|
|
|
it("hard-deletes an unused category", async () => {
|
|
const res = await createPlanCategory({
|
|
label: "Test Del ZZ",
|
|
color: "#dddddd",
|
|
});
|
|
if ("error" in res) throw new Error(res.error);
|
|
const del = await deletePlanCategory(res.data.id);
|
|
expect("data" in del).toBe(true);
|
|
const stillThere = await prisma.plan_categories.findUnique({
|
|
where: { id: res.data.id },
|
|
});
|
|
expect(stillThere).toBeNull();
|
|
});
|
|
|
|
it("refuses to hard-delete a category in use", async () => {
|
|
const res = await createPlanCategory({
|
|
label: "Test InUse ZZ",
|
|
color: "#eeeeee",
|
|
});
|
|
if ("error" in res) throw new Error(res.error);
|
|
created.push(res.data.id);
|
|
const user = await prisma.users.findFirst({ select: { id: true } });
|
|
if (!user) throw new Error("no user in test DB");
|
|
const entry = await prisma.work_plan_entries.create({
|
|
data: {
|
|
user_id: user.id,
|
|
date_from: new Date("2026-06-10T00:00:00.000Z"),
|
|
date_to: new Date("2026-06-10T00:00:00.000Z"),
|
|
category: res.data.key,
|
|
note: "test in-use",
|
|
created_by: user.id,
|
|
},
|
|
});
|
|
try {
|
|
const del = await deletePlanCategory(res.data.id);
|
|
expect("error" in del).toBe(true);
|
|
if ("error" in del) expect(del.status).toBe(409);
|
|
} finally {
|
|
await prisma.work_plan_entries.delete({ where: { id: entry.id } });
|
|
}
|
|
});
|
|
});
|