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.
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
isDebugMode,
|
||||
setDebugMode,
|
||||
validateTaxId,
|
||||
validateVatId,
|
||||
validateIban,
|
||||
validateBic,
|
||||
validateWebsite,
|
||||
validateCompanyForm,
|
||||
getFieldError,
|
||||
hasFieldError,
|
||||
type ValidationError,
|
||||
type CompanyFormData,
|
||||
} from "@/lib/client-validation";
|
||||
|
||||
describe("client-validation.ts", () => {
|
||||
// Mock localStorage
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => { store[key] = value; },
|
||||
removeItem: (key: string) => { delete store[key]; },
|
||||
clear: () => { store = {}; },
|
||||
};
|
||||
})();
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset localStorage mock
|
||||
localStorageMock.clear();
|
||||
(global as any).localStorage = localStorageMock;
|
||||
(global as any).window = { localStorage: localStorageMock };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(global as any).window = undefined;
|
||||
});
|
||||
|
||||
describe("Debug Mode", () => {
|
||||
it("should be disabled by default (no window)", () => {
|
||||
(global as any).window = undefined;
|
||||
expect(isDebugMode()).toBe(false);
|
||||
});
|
||||
|
||||
it("should detect debug mode from localStorage", () => {
|
||||
setDebugMode(true);
|
||||
expect(isDebugMode()).toBe(true);
|
||||
});
|
||||
|
||||
it("should disable debug mode", () => {
|
||||
setDebugMode(true);
|
||||
expect(isDebugMode()).toBe(true);
|
||||
|
||||
setDebugMode(false);
|
||||
expect(isDebugMode()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateTaxId", () => {
|
||||
it("should return null for empty/null values", () => {
|
||||
expect(validateTaxId("")).toBeNull();
|
||||
expect(validateTaxId(null)).toBeNull();
|
||||
expect(validateTaxId(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it("should validate correct 10-digit tax ID", () => {
|
||||
expect(validateTaxId("1234567890")).toBeNull();
|
||||
});
|
||||
|
||||
it("should reject invalid tax IDs", () => {
|
||||
const result = validateTaxId("123"); // Too short
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.field).toBe("taxId");
|
||||
expect(result?.message).toContain("10 Ziffern");
|
||||
});
|
||||
|
||||
it("should reject tax ID with letters", () => {
|
||||
const result = validateTaxId("abc4567890");
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateVatId", () => {
|
||||
it("should return null for empty/null values", () => {
|
||||
expect(validateVatId("")).toBeNull();
|
||||
expect(validateVatId(null)).toBeNull();
|
||||
});
|
||||
|
||||
it("should validate correct DE VAT ID", () => {
|
||||
expect(validateVatId("DE123456789")).toBeNull();
|
||||
});
|
||||
|
||||
it("should reject VAT ID without DE prefix", () => {
|
||||
const result = validateVatId("1234567890");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.field).toBe("vatId");
|
||||
expect(result?.message).toContain("DE + 9 Ziffern");
|
||||
});
|
||||
|
||||
it("should reject VAT ID with wrong length", () => {
|
||||
const result = validateVatId("DE12345"); // Too short
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateIban", () => {
|
||||
it("should return null for empty/null values", () => {
|
||||
expect(validateIban("")).toBeNull();
|
||||
expect(validateIban(null)).toBeNull();
|
||||
});
|
||||
|
||||
it("should validate correct IBAN", () => {
|
||||
expect(validateIban("DE89370400440532013000")).toBeNull();
|
||||
});
|
||||
|
||||
it("should reject invalid IBAN", () => {
|
||||
const result = validateIban("INVALID");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.field).toBe("bankIban");
|
||||
expect(result?.message).toContain("IBAN");
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateBic", () => {
|
||||
it("should return null for empty/null values", () => {
|
||||
expect(validateBic("")).toBeNull();
|
||||
expect(validateBic(null)).toBeNull();
|
||||
});
|
||||
|
||||
it("should validate correct BIC", () => {
|
||||
expect(validateBic("DEUTDEFF")).toBeNull();
|
||||
});
|
||||
|
||||
it("should reject invalid BIC", () => {
|
||||
const result = validateBic("123"); // Too short
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.field).toBe("bankBic");
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateWebsite", () => {
|
||||
it("should return null for empty/null values", () => {
|
||||
expect(validateWebsite("")).toBeNull();
|
||||
expect(validateWebsite(null)).toBeNull();
|
||||
});
|
||||
|
||||
it("should validate correct website URLs", () => {
|
||||
expect(validateWebsite("https://example.com")).toBeNull();
|
||||
expect(validateWebsite("http://example.com")).toBeNull();
|
||||
});
|
||||
|
||||
it("should reject website without protocol", () => {
|
||||
const result = validateWebsite("example.com");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.field).toBe("website");
|
||||
expect(result?.message).toContain("http:// oder https://");
|
||||
});
|
||||
|
||||
it("should reject too long website URLs", () => {
|
||||
const longUrl = "https://" + "a".repeat(250);
|
||||
const result = validateWebsite(longUrl);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.message).toContain("255");
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateCompanyForm", () => {
|
||||
const validFormData: CompanyFormData = {
|
||||
name: "Test GmbH",
|
||||
address: "Hauptstraße 1",
|
||||
zip: "12345",
|
||||
city: "Berlin",
|
||||
};
|
||||
|
||||
it("should pass validation for valid data", () => {
|
||||
const errors = validateCompanyForm(validFormData);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should require name", () => {
|
||||
const data = { ...validFormData, name: "" };
|
||||
const errors = validateCompanyForm(data);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].field).toBe("name");
|
||||
});
|
||||
|
||||
it("should require address", () => {
|
||||
const data = { ...validFormData, address: "" };
|
||||
const errors = validateCompanyForm(data);
|
||||
const addressError = errors.find(e => e.field === "address");
|
||||
expect(addressError).toBeDefined();
|
||||
});
|
||||
|
||||
it("should require zip", () => {
|
||||
const data = { ...validFormData, zip: "" };
|
||||
const errors = validateCompanyForm(data);
|
||||
const zipError = errors.find(e => e.field === "zip");
|
||||
expect(zipError).toBeDefined();
|
||||
});
|
||||
|
||||
it("should require city", () => {
|
||||
const data = { ...validFormData, city: "" };
|
||||
const errors = validateCompanyForm(data);
|
||||
const cityError = errors.find(e => e.field === "city");
|
||||
expect(cityError).toBeDefined();
|
||||
});
|
||||
|
||||
it("should validate zip format (digits only)", () => {
|
||||
const data = { ...validFormData, zip: "abc" };
|
||||
const errors = validateCompanyForm(data);
|
||||
const zipError = errors.find(e => e.field === "zip");
|
||||
expect(zipError).toBeDefined();
|
||||
expect(zipError?.message).toContain("Zahlen");
|
||||
});
|
||||
|
||||
it("should validate optional fields when provided", () => {
|
||||
const data = {
|
||||
...validFormData,
|
||||
taxId: "123", // Invalid
|
||||
vatId: "INVALID", // Invalid
|
||||
website: "example.com", // Invalid (no protocol)
|
||||
};
|
||||
const errors = validateCompanyForm(data);
|
||||
expect(errors.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
it("should accept valid optional fields", () => {
|
||||
const data = {
|
||||
...validFormData,
|
||||
taxId: "1234567890",
|
||||
vatId: "DE123456789",
|
||||
website: "https://example.com",
|
||||
};
|
||||
const errors = validateCompanyForm(data);
|
||||
const hasTaxIdError = errors.some(e => e.field === "taxId");
|
||||
const hasVatIdError = errors.some(e => e.field === "vatId");
|
||||
const hasWebsiteError = errors.some(e => e.field === "website");
|
||||
expect(hasTaxIdError).toBe(false);
|
||||
expect(hasVatIdError).toBe(false);
|
||||
expect(hasWebsiteError).toBe(false);
|
||||
});
|
||||
|
||||
it("should validate email format", () => {
|
||||
const data = { ...validFormData, email: "invalid-email" };
|
||||
const errors = validateCompanyForm(data);
|
||||
const emailError = errors.find(e => e.field === "email");
|
||||
expect(emailError).toBeDefined();
|
||||
});
|
||||
|
||||
it("should accept valid email", () => {
|
||||
const data = { ...validFormData, email: "test@example.com" };
|
||||
const errors = validateCompanyForm(data);
|
||||
const emailError = errors.find(e => e.field === "email");
|
||||
expect(emailError).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should validate field length limits", () => {
|
||||
const data = {
|
||||
...validFormData,
|
||||
name: "a".repeat(300), // Too long
|
||||
phone: "a".repeat(30), // Too long
|
||||
};
|
||||
const errors = validateCompanyForm(data);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFieldError", () => {
|
||||
it("should return error message for field", () => {
|
||||
const errors: ValidationError[] = [
|
||||
{ field: "name", message: "Name required" },
|
||||
{ field: "email", message: "Invalid email" },
|
||||
];
|
||||
expect(getFieldError("name", errors)).toBe("Name required");
|
||||
expect(getFieldError("email", errors)).toBe("Invalid email");
|
||||
});
|
||||
|
||||
it("should return null for field without error", () => {
|
||||
const errors: ValidationError[] = [];
|
||||
expect(getFieldError("name", errors)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasFieldError", () => {
|
||||
it("should return true if field has error", () => {
|
||||
const errors: ValidationError[] = [
|
||||
{ field: "name", message: "Name required" },
|
||||
];
|
||||
expect(hasFieldError("name", errors)).toBe(true);
|
||||
expect(hasFieldError("email", errors)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for empty errors", () => {
|
||||
expect(hasFieldError("name", [])).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user