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>
106 lines
3.0 KiB
TypeScript
106 lines
3.0 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
CreateCustomerSchema,
|
|
UpdateCustomerSchema,
|
|
} from "../schemas/customers.schema";
|
|
|
|
describe("UpdateCustomerSchema", () => {
|
|
it("rejects empty name", () => {
|
|
const result = UpdateCustomerSchema.safeParse({ name: "" });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("accepts valid name", () => {
|
|
const result = UpdateCustomerSchema.safeParse({ name: "Acme Corp" });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("accepts partial updates without name", () => {
|
|
const result = UpdateCustomerSchema.safeParse({ street: "Main St" });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("rejects a name longer than 255 chars", () => {
|
|
const result = UpdateCustomerSchema.safeParse({ name: "x".repeat(256) });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("accepts nullable address/identity fields set to null", () => {
|
|
const result = UpdateCustomerSchema.safeParse({
|
|
street: null,
|
|
city: null,
|
|
postal_code: null,
|
|
country: null,
|
|
company_id: null,
|
|
vat_id: null,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("CreateCustomerSchema", () => {
|
|
it("requires a name", () => {
|
|
const result = CreateCustomerSchema.safeParse({ street: "Main St" });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("accepts a full valid payload with all optional fields", () => {
|
|
const result = CreateCustomerSchema.safeParse({
|
|
name: "Acme Corp",
|
|
street: "Main St 1",
|
|
city: "Praha",
|
|
postal_code: "11000",
|
|
country: "CZ",
|
|
company_id: "12345678",
|
|
vat_id: "CZ12345678",
|
|
custom_fields: [{ label: "Note", value: "VIP" }],
|
|
customer_field_order: ["name", "street"],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("accepts custom_fields as an array of arbitrary objects", () => {
|
|
const result = CreateCustomerSchema.safeParse({
|
|
name: "Acme Corp",
|
|
custom_fields: [
|
|
{ label: "a", value: 1 },
|
|
{ nested: { deep: true } },
|
|
"raw string",
|
|
],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("rejects custom_fields that is not an array", () => {
|
|
const result = CreateCustomerSchema.safeParse({
|
|
name: "Acme Corp",
|
|
custom_fields: { not: "an array" },
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects custom_fields longer than the 100-item cap", () => {
|
|
const result = CreateCustomerSchema.safeParse({
|
|
name: "Acme Corp",
|
|
custom_fields: Array.from({ length: 101 }, (_, i) => ({ i })),
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects customer_field_order entries that are not strings", () => {
|
|
const result = CreateCustomerSchema.safeParse({
|
|
name: "Acme Corp",
|
|
customer_field_order: ["name", 5],
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects an over-length nullable field (vat_id > 255)", () => {
|
|
const result = CreateCustomerSchema.safeParse({
|
|
name: "Acme Corp",
|
|
vat_id: "x".repeat(256),
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|