import { describe, it, expect } from "vitest"; import { CreateOrderSchema } from "../schemas/orders.schema"; import { CreateQuotationSchema } from "../schemas/offers.schema"; import { CreateTripSchema, UpdateTripSchema } from "../schemas/trips.schema"; import { CreateReceivedInvoiceSchema } from "../schemas/received-invoices.schema"; import { ReceiptItemSchema, CreateSupplierSchema, } from "../schemas/warehouse.schema"; import { UpdateCompanySettingsSchema } from "../schemas/settings.schema"; // These schemas now build on the shared form-coercion helpers in // `src/schemas/common.ts` (numberFromForm / numberInRange / intIdFromForm / …). // HTTP bodies arrive as strings (multipart) or numbers (JSON), so the helpers // must BOTH (a) coerce a valid numeric STRING → number and (b) reject a NaN // string. The original suite only asserted (b); these tests pin (a) as well. describe("Zod form coercion + NaN rejection", () => { describe("CreateOrderSchema", () => { it("rejects NaN string in quantity", () => { const result = CreateOrderSchema.safeParse({ customer_id: 1, items: [{ quantity: "not-a-number" }], }); expect(result.success).toBe(false); }); it("rejects NaN string in unit_price", () => { const result = CreateOrderSchema.safeParse({ customer_id: 1, items: [{ unit_price: "abc" }], }); expect(result.success).toBe(false); }); it("accepts valid numbers", () => { const result = CreateOrderSchema.safeParse({ customer_id: 1, items: [{ quantity: 2, unit_price: 100 }], }); expect(result.success).toBe(true); }); it("coerces a valid numeric STRING quantity/unit_price to a number", () => { const result = CreateOrderSchema.safeParse({ customer_id: 1, items: [{ quantity: "2", unit_price: "100.5" }], }); expect(result.success).toBe(true); if (result.success) { const item = result.data.items![0]; expect(item.quantity).toBe(2); expect(typeof item.quantity).toBe("number"); expect(item.unit_price).toBe(100.5); expect(typeof item.unit_price).toBe("number"); } }); }); describe("CreateQuotationSchema", () => { it("rejects NaN string in item quantity", () => { const result = CreateQuotationSchema.safeParse({ customer_id: 1, items: [{ quantity: "bad" }], }); expect(result.success).toBe(false); }); it("coerces a valid numeric STRING item quantity/unit_price", () => { const result = CreateQuotationSchema.safeParse({ customer_id: 1, items: [{ quantity: "3", unit_price: "250" }], }); expect(result.success).toBe(true); if (result.success) { const item = result.data.items![0]; expect(item.quantity).toBe(3); expect(item.unit_price).toBe(250); } }); }); // The remaining schemas were the ones flagged as accepting NaN before the // shared helpers landed. Assert both the coercion and the rejection paths. describe("CreateTripSchema", () => { it("coerces numeric STRING vehicle_id / start_km / end_km to numbers", () => { // km fields are Int columns — integer strings coerce, decimals are // rejected (see trips-vehicles-km-int.test.ts for the rejection pins). const result = CreateTripSchema.safeParse({ vehicle_id: "5", trip_date: "2026-01-15", start_km: "100", end_km: "150", route_from: "Praha", route_to: "Brno", }); expect(result.success).toBe(true); if (result.success) { expect(result.data.vehicle_id).toBe(5); expect(result.data.start_km).toBe(100); expect(result.data.end_km).toBe(150); expect(typeof result.data.start_km).toBe("number"); } }); it("rejects a NaN string in start_km", () => { const result = CreateTripSchema.safeParse({ vehicle_id: 5, trip_date: "2026-01-15", start_km: "not-a-number", end_km: 150, route_from: "Praha", route_to: "Brno", }); expect(result.success).toBe(false); }); it("rejects a NaN / non-positive string in vehicle_id (FK)", () => { const result = CreateTripSchema.safeParse({ vehicle_id: "abc", trip_date: "2026-01-15", start_km: 100, end_km: 150, route_from: "Praha", route_to: "Brno", }); expect(result.success).toBe(false); }); }); describe("CreateReceivedInvoiceSchema", () => { it("coerces numeric STRING month / year / amount to numbers", () => { const result = CreateReceivedInvoiceSchema.safeParse({ supplier_name: "ACME s.r.o.", month: "6", year: "2026", amount: "1210.50", }); expect(result.success).toBe(true); if (result.success) { expect(result.data.month).toBe(6); expect(result.data.year).toBe(2026); expect(result.data.amount).toBe(1210.5); expect(typeof result.data.amount).toBe("number"); } }); it("rejects a NaN string in amount", () => { const result = CreateReceivedInvoiceSchema.safeParse({ supplier_name: "ACME s.r.o.", month: 6, year: 2026, amount: "not-money", }); expect(result.success).toBe(false); }); it("rejects an out-of-range month (numberInRange 1-12)", () => { const result = CreateReceivedInvoiceSchema.safeParse({ supplier_name: "ACME s.r.o.", month: 13, year: 2026, }); expect(result.success).toBe(false); }); it("rejects a NaN string in month", () => { const result = CreateReceivedInvoiceSchema.safeParse({ supplier_name: "ACME s.r.o.", month: "bad", year: 2026, }); expect(result.success).toBe(false); }); }); describe("warehouse ReceiptItemSchema", () => { it("coerces numeric STRING item_id / quantity / unit_price to numbers", () => { const result = ReceiptItemSchema.safeParse({ item_id: "7", quantity: "12.5", unit_price: "99", }); expect(result.success).toBe(true); if (result.success) { expect(result.data.item_id).toBe(7); expect(result.data.quantity).toBe(12.5); expect(result.data.unit_price).toBe(99); expect(typeof result.data.quantity).toBe("number"); } }); it("rejects a NaN string in quantity", () => { const result = ReceiptItemSchema.safeParse({ item_id: 7, quantity: "lots", unit_price: 99, }); expect(result.success).toBe(false); }); it("rejects a non-positive quantity (positiveNumberFromForm)", () => { const result = ReceiptItemSchema.safeParse({ item_id: 7, quantity: 0, unit_price: 99, }); expect(result.success).toBe(false); }); it("rejects a NaN / non-positive string in item_id (FK)", () => { const result = ReceiptItemSchema.safeParse({ item_id: "abc", quantity: 5, unit_price: 99, }); expect(result.success).toBe(false); }); }); }); // Regression tests for the 2026-06-09 audit fix pass: the schema hardening // must NOT reject input the app legitimately sends. describe("schema hardening — does not reject previously-valid input", () => { it("settings: optional email fields accept an empty string (un-filled)", () => { const r = UpdateCompanySettingsSchema.safeParse({ invoice_alert_email: "", leave_notify_email: "", smtp_from: "", }); expect(r.success).toBe(true); }); it("settings: a valid email is accepted, a malformed one rejected", () => { expect( UpdateCompanySettingsSchema.safeParse({ smtp_from: "a@b.cz" }).success, ).toBe(true); expect( UpdateCompanySettingsSchema.safeParse({ smtp_from: "not-an-email" }) .success, ).toBe(false); }); it("warehouse supplier: dropped legacy contact/address keys are silently stripped", () => { // email/phone/contact_person/notes/address columns were replaced by the // customers-model custom_fields blob (2026-06); legacy clients still // sending them must not 400 — z.object strips unknown keys. const parsed = CreateSupplierSchema.safeParse({ name: "Dodavatel", email: "x@y.cz", phone: "123", contact_person: "Jan", notes: "n", address: "Ulice 1", custom_fields: [{ name: "Kontakt", value: "Jan", showLabel: true }], }); expect(parsed.success).toBe(true); if (parsed.success) { expect("email" in parsed.data).toBe(false); expect("address" in parsed.data).toBe(false); expect(parsed.data.custom_fields).toHaveLength(1); } }); it("trips: trip_date accepts a date-only AND a datetime round-trip from @db.Date", () => { const base = { vehicle_id: 1, start_km: 100, end_km: 120, route_from: "A", route_to: "B", }; // Plain date-only (from the date picker). const a = CreateTripSchema.safeParse({ ...base, trip_date: "2026-06-09" }); expect(a.success).toBe(true); // The edit form re-submits what the API serialized for a @db.Date column // via the Date.toJSON override ("YYYY-MM-DDT00:00:00") — must still pass and // normalize to the date-only value. const b = UpdateTripSchema.safeParse({ trip_date: "2026-06-09T00:00:00" }); expect(b.success).toBe(true); if (b.success) expect(b.data.trip_date).toBe("2026-06-09"); // Genuinely malformed dates are still rejected. expect( CreateTripSchema.safeParse({ ...base, trip_date: "09/06/2026" }).success, ).toBe(false); }); });