import { describe, it, expect } from 'vitest'; import { generateSecurityTxt } from '../src/generators/security.js'; describe('generateSecurityTxt', () => { const testURL = new URL('https://example.com'); it('generates basic security.txt with required fields', () => { const result = generateSecurityTxt( { contact: 'security@example.com', }, testURL ); expect(result).toContain('Canonical: https://example.com/.well-known/security.txt'); expect(result).toContain('Contact: mailto:security@example.com'); expect(result).toContain('Expires: '); }); it('automatically adds mailto: prefix to email contacts', () => { const result = generateSecurityTxt( { contact: 'security@example.com', }, testURL ); expect(result).toContain('Contact: mailto:security@example.com'); expect(result).not.toContain('Contact: security@example.com'); }); it('preserves mailto: prefix if already present', () => { const result = generateSecurityTxt( { contact: 'mailto:security@example.com', }, testURL ); expect(result).toContain('Contact: mailto:security@example.com'); // Should not double the prefix const mailtoCount = (result.match(/mailto:/g) || []).length; expect(mailtoCount).toBe(1); }); it('handles URL contacts without modification', () => { const result = generateSecurityTxt( { contact: 'https://example.com/security', }, testURL ); expect(result).toContain('Contact: https://example.com/security'); }); it('supports multiple contact methods', () => { const result = generateSecurityTxt( { contact: [ 'security@example.com', 'https://example.com/security', 'tel:+1-555-0100', ], }, testURL ); expect(result).toContain('Contact: mailto:security@example.com'); expect(result).toContain('Contact: https://example.com/security'); expect(result).toContain('Contact: tel:+1-555-0100'); }); it('auto-calculates expiration date 1 year in future', () => { const result = generateSecurityTxt( { contact: 'security@example.com', expires: 'auto', }, testURL ); expect(result).toContain('Expires: '); // Extract the date const match = result.match(/Expires: (.+)/); expect(match).toBeTruthy(); if (match) { const expiresDate = new Date(match[1]); const now = new Date(); const oneYearFromNow = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000); // Should be approximately 1 year from now (within 1 minute) const diff = Math.abs(expiresDate.getTime() - oneYearFromNow.getTime()); expect(diff).toBeLessThan(60 * 1000); } }); it('accepts custom expiration date', () => { const customExpires = '2026-12-31T23:59:59Z'; const result = generateSecurityTxt( { contact: 'security@example.com', expires: customExpires, }, testURL ); expect(result).toContain(`Expires: ${customExpires}`); }); it('includes optional encryption field', () => { const result = generateSecurityTxt( { contact: 'security@example.com', encryption: 'https://example.com/pgp-key.txt', }, testURL ); expect(result).toContain('Encryption: https://example.com/pgp-key.txt'); }); it('supports multiple encryption keys', () => { const result = generateSecurityTxt( { contact: 'security@example.com', encryption: [ 'https://example.com/pgp-key.txt', 'dns:5d2d37ab76d47d36._openpgpkey.example.com', ], }, testURL ); expect(result).toContain('Encryption: https://example.com/pgp-key.txt'); expect(result).toContain('Encryption: dns:5d2d37ab76d47d36._openpgpkey.example.com'); }); it('includes acknowledgments page', () => { const result = generateSecurityTxt( { contact: 'security@example.com', acknowledgments: 'https://example.com/security/hall-of-fame', }, testURL ); expect(result).toContain('Acknowledgments: https://example.com/security/hall-of-fame'); }); it('includes preferred languages', () => { const result = generateSecurityTxt( { contact: 'security@example.com', preferredLanguages: ['en', 'es', 'fr'], }, testURL ); expect(result).toContain('Preferred-Languages: en, es, fr'); }); it('includes custom canonical URL', () => { const result = generateSecurityTxt( { contact: 'security@example.com', canonical: 'https://example.com/custom/.well-known/security.txt', }, testURL ); expect(result).toContain('Canonical: https://example.com/custom/.well-known/security.txt'); }); it('includes policy URL', () => { const result = generateSecurityTxt( { contact: 'security@example.com', policy: 'https://example.com/security/policy', }, testURL ); expect(result).toContain('Policy: https://example.com/security/policy'); }); it('includes hiring URL', () => { const result = generateSecurityTxt( { contact: 'security@example.com', hiring: 'https://example.com/jobs/security', }, testURL ); expect(result).toContain('Hiring: https://example.com/jobs/security'); }); it('generates complete RFC 9116 compliant file with all fields', () => { const result = generateSecurityTxt( { contact: ['security@example.com', 'https://example.com/security'], expires: '2026-12-31T23:59:59Z', encryption: 'https://example.com/pgp-key.txt', acknowledgments: 'https://example.com/security/hall-of-fame', preferredLanguages: ['en', 'es'], canonical: 'https://example.com/.well-known/security.txt', policy: 'https://example.com/security/policy', hiring: 'https://example.com/jobs/security', }, testURL ); // Check field order (Canonical should be first) expect(result.indexOf('Canonical:')).toBeLessThan(result.indexOf('Contact:')); expect(result.indexOf('Contact:')).toBeLessThan(result.indexOf('Expires:')); // Check all fields present expect(result).toContain('Canonical: https://example.com/.well-known/security.txt'); expect(result).toContain('Contact: mailto:security@example.com'); expect(result).toContain('Contact: https://example.com/security'); expect(result).toContain('Expires: 2026-12-31T23:59:59Z'); expect(result).toContain('Encryption: https://example.com/pgp-key.txt'); expect(result).toContain('Acknowledgments: https://example.com/security/hall-of-fame'); expect(result).toContain('Preferred-Languages: en, es'); expect(result).toContain('Policy: https://example.com/security/policy'); expect(result).toContain('Hiring: https://example.com/jobs/security'); }); it('ends with newline', () => { const result = generateSecurityTxt( { contact: 'security@example.com', }, testURL ); expect(result.endsWith('\n')).toBe(true); }); });