334 lines
11 KiB
TypeScript
334 lines
11 KiB
TypeScript
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('test<script>alert(1)</script>test')).toBe('testscriptalert(1)/scripttest');
|
|
expect(sanitizeString('Hello <b>world</b>!')).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');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}); |