test: add comprehensive tests for security.txt and canary.txt
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.
This commit is contained in:
parent
6de34f55a9
commit
2063d81e60
2 changed files with 586 additions and 0 deletions
241
tests/security.test.ts
Normal file
241
tests/security.test.ts
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue