Files
app/src/__tests__/planCategory.test.ts
BOHA 519edce373 fix: 2026-06-09 full-codebase audit hardening
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>
2026-06-09 06:45:26 +02:00

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 } });
}
});
});