import { describe, it, expect } from 'vitest'; import { isValidEmail, validateEmailList, getEmailValidationError, isValidUnsubscribeUrl, parseUnsubscribeUrls, parseMailtoUrl, } from '../validation'; describe('validation', () => { describe('isValidEmail', () => { it('should accept valid basic emails', () => { expect(isValidEmail('user@example.com')).toBe(true); expect(isValidEmail('john.doe@company.co.uk')).toBe(true); expect(isValidEmail('test_user@subdomain.example.com')).toBe(true); }); it('should accept emails with plus addressing', () => { expect(isValidEmail('user+tag@example.com')).toBe(true); expect(isValidEmail('user+shopping@example.com')).toBe(true); }); it('should accept various valid formats', () => { expect(isValidEmail('a@b.co')).toBe(true); expect(isValidEmail('user123@test-domain.com')).toBe(true); expect(isValidEmail('first.last+tag@example.co.uk')).toBe(true); }); it('should reject emails without @ symbol', () => { expect(isValidEmail('userexample.com')).toBe(false); expect(isValidEmail('user')).toBe(false); }); it('should reject emails without domain', () => { expect(isValidEmail('user@')).toBe(false); expect(isValidEmail('@example.com')).toBe(false); }); it('should reject header injection attempts', () => { expect(isValidEmail('test\r\nBcc:evil@example.com')).toBe(false); expect(isValidEmail('test\rBcc:evil@example.com')).toBe(false); expect(isValidEmail('test\nBcc:evil@example.com')).toBe(false); }); it('should reject emails with dangerous characters', () => { expect(isValidEmail('test')).toBe(false); expect(isValidUnsubscribeUrl('data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==')).toBe(false); }); it('should reject file: protocol', () => { expect(isValidUnsubscribeUrl('file:///etc/passwd')).toBe(false); expect(isValidUnsubscribeUrl('file://C:/Windows/System32/config')).toBe(false); }); it('should reject vbscript: protocol', () => { expect(isValidUnsubscribeUrl('vbscript:msgbox(1)')).toBe(false); }); it('should reject about: protocol', () => { expect(isValidUnsubscribeUrl('about:blank')).toBe(false); }); it('should reject ftp: protocol', () => { expect(isValidUnsubscribeUrl('ftp://example.com/file')).toBe(false); }); }); describe('edge cases', () => { it('should reject empty or null input', () => { expect(isValidUnsubscribeUrl('')).toBe(false); expect(isValidUnsubscribeUrl(' ')).toBe(false); }); it('should reject malformed URLs', () => { expect(isValidUnsubscribeUrl('not-a-url')).toBe(false); expect(isValidUnsubscribeUrl('example.com/unsub')).toBe(false); expect(isValidUnsubscribeUrl('//example.com')).toBe(false); }); it('should reject relative URLs', () => { expect(isValidUnsubscribeUrl('/unsubscribe')).toBe(false); expect(isValidUnsubscribeUrl('../unsub')).toBe(false); }); }); }); describe('parseUnsubscribeUrls', () => { it('should parse single HTTP URL', () => { const result = parseUnsubscribeUrls(''); expect(result.http).toBe('https://example.com/unsub'); expect(result.mailto).toBeUndefined(); expect(result.preferred).toBe('http'); }); it('should parse single mailto URL', () => { const result = parseUnsubscribeUrls(''); expect(result.http).toBeUndefined(); expect(result.mailto).toBe('mailto:unsub@example.com'); expect(result.preferred).toBe('mailto'); }); it('should parse multiple URLs and prefer HTTP', () => { const result = parseUnsubscribeUrls(', '); expect(result.http).toBe('https://example.com/unsub'); expect(result.mailto).toBe('mailto:unsub@example.com'); expect(result.preferred).toBe('http'); }); it('should prefer HTTP over mailto when both present', () => { const result = parseUnsubscribeUrls(', '); expect(result.http).toBe('https://example.com/unsub'); expect(result.mailto).toBe('mailto:unsub@example.com'); expect(result.preferred).toBe('http'); }); it('should handle URLs with query parameters', () => { const result = parseUnsubscribeUrls(''); expect(result.http).toBe('https://example.com/unsub?token=abc123&id=456'); expect(result.preferred).toBe('http'); }); it('should handle mailto with query parameters', () => { const result = parseUnsubscribeUrls(''); expect(result.mailto).toBe('mailto:unsub@example.com?subject=Unsubscribe&body=Remove'); expect(result.preferred).toBe('mailto'); }); it('should filter out invalid URLs', () => { const result = parseUnsubscribeUrls(', '); expect(result.http).toBe('https://example.com/unsub'); expect(result.preferred).toBe('http'); }); it('should return empty object for all invalid URLs', () => { const result = parseUnsubscribeUrls(', >'); expect(result.http).toBeUndefined(); expect(result.mailto).toBeUndefined(); expect(result.preferred).toBeUndefined(); }); it('should handle empty or null input', () => { expect(parseUnsubscribeUrls('')).toEqual({}); expect(parseUnsubscribeUrls(' ')).toEqual({}); }); it('should handle malformed headers without angle brackets', () => { const result = parseUnsubscribeUrls('https://example.com/unsub'); expect(result).toEqual({}); }); it('should handle whitespace in headers', () => { const result = parseUnsubscribeUrls(' , '); expect(result.http).toBe('https://example.com/unsub'); expect(result.mailto).toBe('mailto:unsub@example.com'); expect(result.preferred).toBe('http'); }); it('should handle three or more URLs', () => { const result = parseUnsubscribeUrls( ', , ' ); expect(result.http).toBeDefined(); expect(result.mailto).toBe('mailto:unsub@example.com'); expect(result.preferred).toBe('http'); }); it('should validate email addresses in mailto URLs', () => { const result = parseUnsubscribeUrls(', '); expect(result.mailto).toBeUndefined(); expect(result.http).toBe('https://example.com/unsub'); expect(result.preferred).toBe('http'); }); }); }); describe('parseMailtoUrl', () => { it('parses address, subject and body', () => { const r = parseMailtoUrl('mailto:list@example.com?subject=Unsubscribe%20123&body=Please%20remove'); expect(r).toEqual({ to: ['list@example.com'], subject: 'Unsubscribe 123', body: 'Please remove' }); }); it('keeps a literal plus (RFC 6068 uses percent-encoding only)', () => { const r = parseMailtoUrl('mailto:owner+unsub@example.com?subject=a+b'); expect(r?.to).toEqual(['owner+unsub@example.com']); expect(r?.subject).toBe('a+b'); }); it('supports multiple recipients and the to param', () => { const r = parseMailtoUrl('mailto:a@example.com,b@example.com?to=c@example.com'); expect(r?.to).toEqual(['a@example.com', 'b@example.com', 'c@example.com']); }); it('returns null without a valid recipient', () => { expect(parseMailtoUrl('mailto:?subject=x')).toBeNull(); expect(parseMailtoUrl('mailto:not-an-address')).toBeNull(); expect(parseMailtoUrl('https://example.com/unsub')).toBeNull(); }); it('survives malformed percent-encoding', () => { const r = parseMailtoUrl('mailto:list@example.com?subject=%E0%A4%A'); expect(r?.to).toEqual(['list@example.com']); expect(r?.subject).toBe('%E0%A4%A'); }); });