import { emailSchema, urlSchema, currencySchema, companyConfigSchema, invitationEmailSchema, paymentRequestSchema, passwordResetSchema, invoiceSchema, reportSchema, validateEmail, validateUrl, sanitizeString, formatValidationError } from '../../src/lib/validation'; import { z } from 'zod'; describe('Validation Module', () => { describe('Basic Schema Validation', () => { describe('emailSchema', () => { it('should validate correct email addresses', () => { expect(emailSchema.safeParse('user@example.com').success).toBe(true); expect(emailSchema.safeParse('test.email@domain.co.uk').success).toBe(true); expect(emailSchema.safeParse('user+tag@example.org').success).toBe(true); }); it('should reject invalid email addresses', () => { expect(emailSchema.safeParse('invalid-email').success).toBe(false); expect(emailSchema.safeParse('@example.com').success).toBe(false); expect(emailSchema.safeParse('user@').success).toBe(false); expect(emailSchema.safeParse('').success).toBe(false); }); }); describe('urlSchema', () => { it('should validate correct URLs', () => { expect(urlSchema.safeParse('https://example.com').success).toBe(true); expect(urlSchema.safeParse('http://localhost:3000').success).toBe(true); expect(urlSchema.safeParse('https://sub.domain.com/path?query=1').success).toBe(true); }); it('should reject invalid URLs', () => { expect(urlSchema.safeParse('not-a-url').success).toBe(false); expect(urlSchema.safeParse('').success).toBe(false); // Note: ftp:// is actually a valid URL scheme, so we test with invalid format instead expect(urlSchema.safeParse('://invalid').success).toBe(false); }); }); describe('currencySchema', () => { it('should validate supported currencies', () => { expect(currencySchema.safeParse('USD').success).toBe(true); expect(currencySchema.safeParse('EUR').success).toBe(true); expect(currencySchema.safeParse('GBP').success).toBe(true); expect(currencySchema.safeParse('CAD').success).toBe(true); expect(currencySchema.safeParse('AUD').success).toBe(true); }); it('should reject unsupported currencies', () => { expect(currencySchema.safeParse('JPY').success).toBe(false); expect(currencySchema.safeParse('CHF').success).toBe(false); expect(currencySchema.safeParse('').success).toBe(false); }); }); }); describe('Company Configuration Schema', () => { const validCompany = { name: 'Test Company', logoUrl: 'https://example.com/logo.png', primaryColor: '#ff0000', paymentLink: 'https://pay.example.com', bankDetails: { bankName: 'Test Bank', accountName: 'Test Account', accountNumber: '123456789', branch: 'Main Branch', iban: 'GB82WEST12345698765432', referenceNote: 'Payment reference' } }; it('should validate complete company configuration', () => { const result = companyConfigSchema.safeParse(validCompany); expect(result.success).toBe(true); }); it('should validate minimal company configuration', () => { const minimal = { name: 'Test Company' }; const result = companyConfigSchema.safeParse(minimal); expect(result.success).toBe(true); }); it('should reject invalid company name', () => { const invalid = { ...validCompany, name: '' }; const result = companyConfigSchema.safeParse(invalid); expect(result.success).toBe(false); }); it('should reject invalid logo URL', () => { const invalid = { ...validCompany, logoUrl: 'not-a-url' }; const result = companyConfigSchema.safeParse(invalid); expect(result.success).toBe(false); }); it('should reject invalid color format', () => { const invalid = { ...validCompany, primaryColor: 'red' }; const result = companyConfigSchema.safeParse(invalid); expect(result.success).toBe(false); }); it('should allow empty optional fields', () => { const withEmpty = { ...validCompany, logoUrl: '', paymentLink: '' }; const result = companyConfigSchema.safeParse(withEmpty); expect(result.success).toBe(true); }); }); describe('Email Template Schemas', () => { const validCompany = { name: 'Test Company' }; describe('invitationEmailSchema', () => { const validInvitation = { to: 'user@example.com', eventName: 'Test Event', dateTime: '2024-12-25 18:00', location: 'Test Location', ctaUrl: 'https://example.com/rsvp', company: validCompany }; it('should validate complete invitation data', () => { const result = invitationEmailSchema.safeParse(validInvitation); expect(result.success).toBe(true); }); it('should apply default CTA label', () => { const result = invitationEmailSchema.safeParse(validInvitation); if (result.success) { expect(result.data.ctaLabel).toBe('RSVP Now'); } }); it('should reject missing required fields', () => { const invalid = { ...validInvitation }; delete invalid.eventName; const result = invitationEmailSchema.safeParse(invalid); expect(result.success).toBe(false); }); }); describe('paymentRequestSchema', () => { const validPayment = { to: 'user@example.com', amount: 100.50, currency: 'USD' as const, description: 'Test payment', dueDate: '2024-12-31', company: validCompany }; it('should validate complete payment request', () => { const result = paymentRequestSchema.safeParse(validPayment); expect(result.success).toBe(true); }); it('should reject negative amounts', () => { const invalid = { ...validPayment, amount: -10 }; const result = paymentRequestSchema.safeParse(invalid); expect(result.success).toBe(false); }); it('should reject excessive amounts', () => { const invalid = { ...validPayment, amount: 2000000 }; const result = paymentRequestSchema.safeParse(invalid); expect(result.success).toBe(false); }); }); describe('passwordResetSchema', () => { const validReset = { to: 'user@example.com', resetLink: 'https://example.com/reset?token=abc123', company: validCompany }; it('should validate password reset data', () => { const result = passwordResetSchema.safeParse(validReset); expect(result.success).toBe(true); }); it('should reject invalid reset link', () => { const invalid = { ...validReset, resetLink: 'not-a-url' }; const result = passwordResetSchema.safeParse(invalid); expect(result.success).toBe(false); }); }); describe('invoiceSchema', () => { const validInvoice = { to: 'user@example.com', invoiceNumber: 'INV-001', issueDate: '2024-01-01', currency: 'USD' as const, items: [ { description: 'Test item', quantity: 2, unitPrice: 50.00 } ], company: validCompany }; it('should validate complete invoice', () => { const result = invoiceSchema.safeParse(validInvoice); expect(result.success).toBe(true); }); it('should reject empty items array', () => { const invalid = { ...validInvoice, items: [] }; const result = invoiceSchema.safeParse(invalid); expect(result.success).toBe(false); }); it('should reject too many items', () => { const items = Array(60).fill({ description: 'Item', quantity: 1, unitPrice: 10 }); const invalid = { ...validInvoice, items }; const result = invoiceSchema.safeParse(invalid); expect(result.success).toBe(false); }); }); describe('reportSchema', () => { const validReport = { to: 'user@example.com', periodLabel: 'Q1 2024', totalAmount: 1000.00, taxAmount: 200.00, currency: 'USD' as const, company: validCompany }; it('should validate report data', () => { const result = reportSchema.safeParse(validReport); expect(result.success).toBe(true); }); it('should reject negative amounts', () => { const invalid = { ...validReport, totalAmount: -100 }; const result = reportSchema.safeParse(invalid); expect(result.success).toBe(false); }); }); }); describe('Utility Functions', () => { describe('validateEmail', () => { it('should validate correct emails', () => { expect(validateEmail('user@example.com')).toBe(true); expect(validateEmail('test@domain.org')).toBe(true); }); it('should reject invalid emails', () => { expect(validateEmail('invalid-email')).toBe(false); expect(validateEmail('')).toBe(false); }); }); describe('validateUrl', () => { it('should validate correct URLs', () => { expect(validateUrl('https://example.com')).toBe(true); expect(validateUrl('http://localhost:3000')).toBe(true); }); it('should reject invalid URLs', () => { expect(validateUrl('not-a-url')).toBe(false); expect(validateUrl('')).toBe(false); }); }); describe('sanitizeString', () => { it('should trim whitespace', () => { expect(sanitizeString(' test ')).toBe('test'); }); it('should remove HTML tags', () => { expect(sanitizeString('testtest')).toBe('testscriptalert(1)/scripttest'); expect(sanitizeString('Hello world!')).toBe('Hello bworld/b!'); }); it('should limit string length', () => { const longString = 'a'.repeat(2000); expect(sanitizeString(longString, 100)).toHaveLength(100); }); it('should use default max length', () => { const longString = 'a'.repeat(2000); expect(sanitizeString(longString)).toHaveLength(1000); }); }); describe('formatValidationError', () => { it('should format single validation error', () => { const schema = z.object({ email: z.string().email() }); const result = schema.safeParse({ email: 'invalid' }); if (!result.success) { const formatted = formatValidationError(result.error); expect(formatted).toContain('email'); expect(formatted).toContain('Invalid email'); } }); it('should format multiple validation errors', () => { const schema = z.object({ email: z.string().email(), age: z.number().min(18) }); const result = schema.safeParse({ email: 'invalid', age: 10 }); if (!result.success) { const formatted = formatValidationError(result.error); expect(formatted).toContain('email'); expect(formatted).toContain('age'); } }); }); }); });