Added 38 new tests (16 + 22) covering all features of the new generators: ## security.txt Tests (16 tests) - RFC 9116 field validation (Canonical, Contact, Expires) - Automatic mailto: prefix handling for email contacts - Auto-expiration calculation (1 year from generation) - Multiple contact methods support - Multiple encryption keys - All optional fields: acknowledgments, preferredLanguages, policy, hiring - Proper field ordering compliance ## canary.txt Tests (22 tests) - Compact field: value format validation - Frequency-based expiration (daily: 2d, weekly: 10d, monthly: 35d, quarterly: 100d, yearly: 380d) - Statement filtering (only non-received statements appear) - Default statements vs custom statements - Function-based dynamic statements - Blockchain proof formatting (Network:Address:TxHash) - Personnel duress statement - Verification field - Previous canary references - Contact with mailto: prefix - Organization and frequency fields Test suite now at 72 total tests (up from 34), all passing.
346 lines
10 KiB
TypeScript
346 lines
10 KiB
TypeScript
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);
|
|
});
|
|
});
|