Files
AnnasRechnungsManager/tests/lib/schemas.test.ts
T
hwinkel db953b1e28 Add comprehensive tests for client validation, revenue and expense categories, invoice generation, and schemas
- Implement tests for client validation functions including tax ID, VAT ID, IBAN, BIC, website, and company form validation.
- Create tests for revenue and expense categories ensuring all expected categories and labels are present.
- Add tests for invoice number generation with various scenarios including prefix handling and sequence padding.
- Introduce tests for default categories and their integration, ensuring no overlaps and consistent naming conventions.
- Implement Zod schema validation tests for currency, tax rates, IBAN, tax ID, VAT ID, invoices, companies, and customers.
- Add utility tests for tax calculations, including item amounts and invoice totals, ensuring correct handling of tax rates and formatting.
- Set up Vitest configuration and global test setup for consistent testing environment.
2026-05-08 16:03:05 +02:00

244 lines
7.7 KiB
TypeScript

import {
currencySchema,
taxRateSchema,
ibanSchema,
taxIdSchema,
vatIdSchema,
invoiceSchema,
companySchema,
customerSchema,
} from "@/lib/schemas";
describe("schemas.ts - Zod Validation", () => {
describe("currencySchema", () => {
it("should accept valid currency values", () => {
expect(currencySchema.parse(0)).toBe(0);
expect(currencySchema.parse(100)).toBe(100);
expect(currencySchema.parse(99.99)).toBe(99.99);
expect(currencySchema.parse(1000.5)).toBe(1000.5);
});
it("should reject negative values", () => {
expect(() => currencySchema.parse(-1)).toThrow("Geldbeträge dürfen nicht negativ sein");
});
it("should reject more than 2 decimal places", () => {
expect(() => currencySchema.parse(1.999)).toThrow(
"Geldbeträge dürfen maximal 2 Dezimalstellen haben"
);
});
it("should accept exactly 2 decimal places", () => {
expect(currencySchema.parse(1.99)).toBe(1.99);
});
});
describe("taxRateSchema", () => {
it("should accept valid German tax rates", () => {
expect(taxRateSchema.parse(0)).toBe(0);
expect(taxRateSchema.parse(7)).toBe(7);
expect(taxRateSchema.parse(19)).toBe(19);
});
it("should reject invalid tax rates", () => {
expect(() => taxRateSchema.parse(5)).toThrow(
"Steuersatz muss 0 (steuerfrei), 7 (ermäßigt) oder 19 (Standard) sein"
);
expect(() => taxRateSchema.parse(20)).toThrow();
expect(() => taxRateSchema.parse(15)).toThrow();
});
it("should reject non-integer values", () => {
expect(() => taxRateSchema.parse(7.5)).toThrow("Steuersatz muss eine ganze Zahl sein");
});
});
describe("ibanSchema", () => {
it("should accept valid IBANs", () => {
expect(ibanSchema.parse("DE89370400440532013000")).toBe("DE89370400440532013000");
expect(ibanSchema.parse("AT611904300234573201")).toBe("AT611904300234573201");
expect(ibanSchema.parse("CH9300762011623852957")).toBe("CH9300762011623852957");
});
it("should accept empty string", () => {
expect(ibanSchema.parse("")).toBe("");
});
it("should reject invalid IBAN format", () => {
// No country code - starts with numbers
expect(() => ibanSchema.parse("1234567890")).toThrow();
// Too short - only 4 chars (need at least 5: 2 letters + 2 digits + 1)
expect(() => ibanSchema.parse("DE1")).toThrow();
});
});
describe("taxIdSchema", () => {
it("should accept valid 10-digit tax IDs", () => {
expect(taxIdSchema.parse("1234567890")).toBe("1234567890");
});
it("should accept empty string", () => {
expect(taxIdSchema.parse("")).toBe("");
});
it("should reject invalid tax IDs", () => {
expect(() => taxIdSchema.parse("123")).toThrow("Steuernummer muss 10 Ziffern haben");
expect(() => taxIdSchema.parse("12345678901")).toThrow();
expect(() => taxIdSchema.parse("abcdefghij")).toThrow();
});
});
describe("vatIdSchema", () => {
it("should accept valid DE VAT IDs", () => {
expect(vatIdSchema.parse("DE123456789")).toBe("DE123456789");
});
it("should accept empty string", () => {
expect(vatIdSchema.parse("")).toBe("");
});
it("should reject invalid VAT IDs", () => {
expect(() => vatIdSchema.parse("DE123")).toThrow(
"USt-IdNr. muss im Format DE + 9 Ziffern sein"
);
expect(() => vatIdSchema.parse("1234567890")).toThrow();
expect(() => vatIdSchema.parse("DE12345678")).toThrow();
});
});
describe("invoiceSchema", () => {
const validInvoice = {
companyId: "cm123abc",
customerId: "cm456def",
issueDate: "2026-05-08",
dueDate: "2026-06-08",
kleinunternehmer: false,
items: [
{
position: 1,
description: "Web Development",
quantity: 10,
unit: "Stunden",
unitPrice: 100,
taxRate: 19,
netAmount: 1000,
taxAmount: 190,
grossAmount: 1190,
},
],
netTotal: 1000,
taxTotal: 190,
grossTotal: 1190,
};
it("should accept valid invoice", () => {
const result = invoiceSchema.parse(validInvoice);
expect(result.companyId).toBe("cm123abc");
expect(result.items).toHaveLength(1);
});
it("should require at least one item", () => {
const invalid = { ...validInvoice, items: [] };
expect(() => invoiceSchema.parse(invalid)).toThrow(
"Mindestens ein Rechnungsposition erforderlich"
);
});
it("should accept optional deliveryDate", () => {
const withDelivery = { ...validInvoice, deliveryDate: "2026-05-07" };
expect(() => invoiceSchema.parse(withDelivery)).not.toThrow();
});
it("should reject invalid dates", () => {
const invalid = { ...validInvoice, issueDate: "not-a-date" };
expect(() => invoiceSchema.parse(invalid)).toThrow("Ungültiges Datum");
});
it("should accept optional notes with max length", () => {
const withNotes = { ...validInvoice, notes: "Test notes" };
expect(() => invoiceSchema.parse(withNotes)).not.toThrow();
});
});
describe("companySchema", () => {
const validCompany = {
name: "Test GmbH",
taxId: null,
vatId: null,
address: "Hauptstraße 1",
zip: "12345",
city: "Berlin",
country: "DE",
invoicePrefix: "RE",
kleinunternehmer: false,
bankIban: null,
// bankBic is optional, can be omitted or empty string
};
it("should accept valid company", () => {
const result = companySchema.parse(validCompany);
expect(result.name).toBe("Test GmbH");
});
it("should require name and address", () => {
const invalid = { ...validCompany, name: "" };
expect(() => companySchema.parse(invalid)).toThrow("Firmenname erforderlich");
});
it("should validate email format", () => {
const withEmail = { ...validCompany, email: "test@example.com" };
expect(() => companySchema.parse(withEmail)).not.toThrow();
});
it("should reject invalid email", () => {
const invalid = { ...validCompany, email: "not-an-email" };
expect(() => companySchema.parse(invalid)).toThrow("Ungültige E-Mail-Adresse");
});
it("should validate website starts with http/https", () => {
const withWebsite = { ...validCompany, website: "https://example.com" };
expect(() => companySchema.parse(withWebsite)).not.toThrow();
});
it("should reject website without protocol", () => {
const invalid = { ...validCompany, website: "example.com" };
expect(() => companySchema.parse(invalid)).toThrow(
"Website muss mit http:// oder https:// beginnen"
);
});
it("should accept valid IBAN", () => {
const withIban = { ...validCompany, bankIban: "DE89370400440532013000" };
expect(() => companySchema.parse(withIban)).not.toThrow();
});
});
describe("customerSchema", () => {
const validCustomer = {
companyId: "cm123abc",
name: "Max Mustermann",
address: "Musterstraße 1",
zip: "12345",
city: "Berlin",
country: "DE",
};
it("should accept valid customer", () => {
const result = customerSchema.parse(validCustomer);
expect(result.name).toBe("Max Mustermann");
});
it("should require companyId", () => {
const invalid = { ...validCustomer, companyId: "" };
expect(() => customerSchema.parse(invalid)).toThrow("Mandant erforderlich");
});
it("should validate zip code format", () => {
const invalid = { ...validCustomer, zip: "abc" };
expect(() => customerSchema.parse(invalid)).toThrow(
"PLZ darf nur Zahlen, Bindestriche und Leerzeichen enthalten"
);
});
});
});