import { describe, it, expect } from 'vitest'; import { generateCanaryTxt } from '../src/generators/canary.js'; describe('generateCanaryTxt', () => { const testURL = new URL('https://example.com'); it('generates basic canary with defaults', () => { const result = generateCanaryTxt({}, testURL); expect(result).toContain('Canonical-URL:'); expect(result).toContain('Issued:'); expect(result).toContain('Expires:'); expect(result).toContain('Statement:'); }); it('includes organization name', () => { const result = generateCanaryTxt( { organization: 'Test Corp', }, testURL ); expect(result).toContain('Organization: Test Corp'); }); it('includes contact information with mailto prefix', () => { const result = generateCanaryTxt( { contact: 'canary@example.com', }, testURL ); expect(result).toContain('Contact: mailto:canary@example.com'); }); it('automatically calculates expiration for daily frequency', () => { const result = generateCanaryTxt( { frequency: 'daily', expires: 'auto', }, testURL ); expect(result).toContain('Frequency: daily'); 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 twoDaysFromNow = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000); // Should be approximately 2 days from now (within 1 minute) const diff = Math.abs(expiresDate.getTime() - twoDaysFromNow.getTime()); expect(diff).toBeLessThan(60 * 1000); } }); it('automatically calculates expiration for weekly frequency', () => { const result = generateCanaryTxt( { frequency: 'weekly', expires: 'auto', }, testURL ); const match = result.match(/Expires: (.+)/); if (match) { const expiresDate = new Date(match[1]); const now = new Date(); const tenDaysFromNow = new Date(now.getTime() + 10 * 24 * 60 * 60 * 1000); const diff = Math.abs(expiresDate.getTime() - tenDaysFromNow.getTime()); expect(diff).toBeLessThan(60 * 1000); } }); it('automatically calculates expiration for monthly frequency', () => { const result = generateCanaryTxt( { frequency: 'monthly', expires: 'auto', }, testURL ); const match = result.match(/Expires: (.+)/); if (match) { const expiresDate = new Date(match[1]); const now = new Date(); const thirtyFiveDaysFromNow = new Date(now.getTime() + 35 * 24 * 60 * 60 * 1000); const diff = Math.abs(expiresDate.getTime() - thirtyFiveDaysFromNow.getTime()); expect(diff).toBeLessThan(60 * 1000); } }); it('automatically calculates expiration for quarterly frequency', () => { const result = generateCanaryTxt( { frequency: 'quarterly', expires: 'auto', }, testURL ); const match = result.match(/Expires: (.+)/); if (match) { const expiresDate = new Date(match[1]); const now = new Date(); const oneHundredDaysFromNow = new Date(now.getTime() + 100 * 24 * 60 * 60 * 1000); const diff = Math.abs(expiresDate.getTime() - oneHundredDaysFromNow.getTime()); expect(diff).toBeLessThan(60 * 1000); } }); it('automatically calculates expiration for yearly frequency', () => { const result = generateCanaryTxt( { frequency: 'yearly', expires: 'auto', }, testURL ); const match = result.match(/Expires: (.+)/); if (match) { const expiresDate = new Date(match[1]); const now = new Date(); const threeHundredEightyDaysFromNow = new Date(now.getTime() + 380 * 24 * 60 * 60 * 1000); const diff = Math.abs(expiresDate.getTime() - threeHundredEightyDaysFromNow.getTime()); expect(diff).toBeLessThan(60 * 1000); } }); it('accepts custom expiration date', () => { const customExpires = '2026-01-15T00:00:00Z'; const result = generateCanaryTxt( { expires: customExpires, }, testURL ); expect(result).toContain(`Expires: ${customExpires}`); }); it('includes default statements when none provided', () => { const result = generateCanaryTxt({}, testURL); expect(result).toContain(' - National Security Letters (NSLs)'); expect(result).toContain(' - FISA court orders'); expect(result).toContain(' - Gag orders preventing disclosure'); expect(result).toContain(' - Secret government requests for user data'); expect(result).toContain(' - Requests to install surveillance capabilities'); }); it('supports custom statements as array', () => { const result = generateCanaryTxt( { statements: [ { type: 'custom1', description: 'Custom order type 1', received: false }, { type: 'custom2', description: 'Custom order type 2', received: true }, ], }, testURL ); // Only non-received statements are listed expect(result).toContain(' - Custom order type 1'); // Received statements are NOT included in the output expect(result).not.toContain('Custom order type 2'); }); it('supports statements as function', () => { const result = generateCanaryTxt( { statements: () => [ { type: 'dynamic', description: 'Dynamically generated statement', received: false }, ], }, testURL ); expect(result).toContain(' - Dynamically generated statement'); }); it('filters out received statements from output', () => { const result = generateCanaryTxt( { statements: [ { type: 'test1', description: 'Not received', received: false }, { type: 'test2', description: 'Received', received: true }, ], }, testURL ); // Only non-received statements appear expect(result).toContain(' - Not received'); // Received statements are filtered out expect(result).not.toContain('Received'); }); it('includes additional statement', () => { const result = generateCanaryTxt( { additionalStatement: 'We are committed to transparency and user privacy.', }, testURL ); expect(result).toContain('We are committed to transparency and user privacy.'); }); it('includes verification information', () => { const result = generateCanaryTxt( { verification: 'PGP Signature: https://example.com/canary.txt.asc', }, testURL ); expect(result).toContain('Verification: PGP Signature: https://example.com/canary.txt.asc'); }); it('includes previous canary reference', () => { const result = generateCanaryTxt( { previousCanary: 'https://example.com/canary-2024-11.txt', }, testURL ); expect(result).toContain('Previous-Canary: https://example.com/canary-2024-11.txt'); }); it('includes blockchain proof', () => { const result = generateCanaryTxt( { blockchainProof: { network: 'Ethereum', address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', txHash: '0xabc123...', timestamp: '2024-11-03T12:00:00Z', }, }, testURL ); expect(result).toContain('Blockchain-Proof: Ethereum:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb:0xabc123...'); expect(result).toContain('Blockchain-Timestamp: 2024-11-03T12:00:00Z'); }); it('includes personnel duress statement when enabled', () => { const result = generateCanaryTxt( { personnelStatement: true, }, testURL ); expect(result).toContain('Key Personnel Statement: All key personnel with access to'); expect(result).toContain('infrastructure remain free and under no duress'); }); it('excludes personnel statement when disabled', () => { const result = generateCanaryTxt( { personnelStatement: false, }, testURL ); expect(result).not.toContain('Key Personnel Statement'); }); it('generates complete canary with all fields', () => { const result = generateCanaryTxt( { organization: 'Example Corp', contact: 'canary@example.com', frequency: 'monthly', expires: '2026-01-15T00:00:00Z', statements: [ { type: 'nsl', description: 'National Security Letters', received: false }, { type: 'gag', description: 'Gag orders', received: false }, ], additionalStatement: 'We value transparency.', verification: 'PGP Signature: https://example.com/canary.txt.asc', previousCanary: 'https://example.com/canary-2024-10.txt', blockchainProof: { network: 'Bitcoin', address: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh', }, personnelStatement: true, }, testURL ); expect(result).toContain('Organization: Example Corp'); expect(result).toContain('Contact: mailto:canary@example.com'); expect(result).toContain('Frequency: monthly'); expect(result).toContain('Expires: 2026-01-15T00:00:00Z'); expect(result).toContain('Previous-Canary: https://example.com/canary-2024-10.txt'); expect(result).toContain(' - National Security Letters'); expect(result).toContain(' - Gag orders'); expect(result).toContain('We value transparency.'); expect(result).toContain('Verification: PGP Signature'); expect(result).toContain('Blockchain-Proof: Bitcoin:bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh'); expect(result).toContain('Key Personnel Statement'); }); it('formats Issued date as ISO date', () => { const result = generateCanaryTxt({}, testURL); const match = result.match(/Issued: (.+)/); expect(match).toBeTruthy(); if (match) { // Should be valid ISO date const date = new Date(match[1]); expect(date.toString()).not.toBe('Invalid Date'); // Should be recent (within 1 minute) const now = new Date(); const diff = Math.abs(date.getTime() - now.getTime()); expect(diff).toBeLessThan(60 * 1000); } }); it('ends with newline', () => { const result = generateCanaryTxt({}, testURL); expect(result.endsWith('\n')).toBe(true); }); });