Yaltopia-Ticket-Email/tests/unit/validation.test.ts

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');
}
});
});
});
});