wulf-pulse/lib/services/route53-record-validation.test.ts
lorentz 4be4a191a5 feat(24-03): add D-01 record-write validator and AWS error sanitizer
- validateRecordWrite enforces closed allowlist (A/AAAA/CNAME/MX/TXT/SRV), rejects NS/SOA case-insensitively with a delegation-specific reason
- sanitizeAwsError redacts AWS access key ids, ARNs, and 12-digit account ids, truncates to 500 chars (T-24-03)
- no AWS SDK or Postgres dependency; fully unit-tested (23 assertions)
2026-08-05 20:20:46 -04:00

163 lines
6 KiB
TypeScript

/**
* lib/services/route53-record-validation.ts unit tests — D-01 allowlist
* enforcement and AWS error sanitization (T-24-03). Pure logic, no AWS SDK
* or Postgres dependency to mock.
*/
import { describe, it, expect } from 'vitest';
import {
WRITABLE_RECORD_TYPES,
validateRecordWrite,
sanitizeAwsError,
} from './route53-record-validation';
describe('WRITABLE_RECORD_TYPES', () => {
it('is exactly the six D-01 writable types, never NS or SOA', () => {
expect([...WRITABLE_RECORD_TYPES].sort()).toEqual(
['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT'].sort()
);
expect(WRITABLE_RECORD_TYPES).not.toContain('NS');
expect(WRITABLE_RECORD_TYPES).not.toContain('SOA');
});
});
describe('validateRecordWrite', () => {
const basePayload = {
name: 'www.example.com',
resourceRecords: [{ value: '1.2.3.4' }],
};
it('rejects NS with a reason naming it as a zone-delegation record', () => {
const result = validateRecordWrite({ ...basePayload, type: 'NS' });
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.status).toBe(400);
expect(result.reason).toMatch(/NS/);
expect(result.reason).toMatch(/delegation/i);
}
});
it('rejects SOA', () => {
const result = validateRecordWrite({ ...basePayload, type: 'SOA' });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.status).toBe(400);
});
it('rejects lowercase "ns" case-insensitively', () => {
const result = validateRecordWrite({ ...basePayload, type: 'ns' });
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.status).toBe(400);
expect(result.reason).toMatch(/delegation/i);
}
});
it.each(['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'SRV'])(
'accepts well-formed %s payload',
(type) => {
const result = validateRecordWrite({ ...basePayload, type });
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.value.type).toBe(type);
expect(result.value.name).toBe('www.example.com.');
expect(result.value.ttl).toBe(300);
}
}
);
it('rejects an unknown type such as CAA (closed allowlist, not a blocklist)', () => {
const result = validateRecordWrite({ ...basePayload, type: 'CAA' });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.status).toBe(400);
});
it('rejects an unknown type such as DS', () => {
const result = validateRecordWrite({ ...basePayload, type: 'DS' });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.status).toBe(400);
});
it('rejects a missing name', () => {
const result = validateRecordWrite({ type: 'A', resourceRecords: [{ value: '1.2.3.4' }] });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.status).toBe(400);
});
it('rejects an empty-string name', () => {
const result = validateRecordWrite({ ...basePayload, name: ' ', type: 'A' });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.status).toBe(400);
});
it('rejects a non-integer ttl', () => {
const result = validateRecordWrite({ ...basePayload, type: 'A', ttl: 3.5 });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.status).toBe(400);
});
it('rejects a ttl outside 0..2147483647', () => {
const tooHigh = validateRecordWrite({ ...basePayload, type: 'A', ttl: 2147483648 });
expect(tooHigh.ok).toBe(false);
const tooLow = validateRecordWrite({ ...basePayload, type: 'A', ttl: -1 });
expect(tooLow.ok).toBe(false);
});
it('accepts ttl of exactly 0 and exactly 2147483647', () => {
const min = validateRecordWrite({ ...basePayload, type: 'A', ttl: 0 });
expect(min.ok).toBe(true);
const max = validateRecordWrite({ ...basePayload, type: 'A', ttl: 2147483647 });
expect(max.ok).toBe(true);
});
it('rejects an empty resourceRecords array (Route 53 rejects an empty value set)', () => {
const result = validateRecordWrite({ name: 'www.example.com', type: 'A', resourceRecords: [] });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.status).toBe(400);
});
it('rejects a resourceRecords entry with an empty-string value', () => {
const result = validateRecordWrite({
name: 'www.example.com',
type: 'A',
resourceRecords: [{ value: '' }],
});
expect(result.ok).toBe(false);
if (!result.ok) expect(result.status).toBe(400);
});
it('caps resourceRecords at 100 entries', () => {
const tooMany = Array.from({ length: 101 }, (_, i) => ({ value: `10.0.0.${i % 256}` }));
const result = validateRecordWrite({ name: 'www.example.com', type: 'A', resourceRecords: tooMany });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.status).toBe(400);
});
});
describe('sanitizeAwsError', () => {
it('strips AWS access key ids, ARNs, and account ids, then truncates to 500 chars', () => {
const err = new Error(
'AccessDenied for AKIAIOSFODNN7EXAMPLE on arn:aws:route53:::hostedzone/Z123 account 123456789012'
);
const sanitized = sanitizeAwsError(err);
expect(sanitized).not.toContain('AKIAIOSFODNN7EXAMPLE');
expect(sanitized).not.toContain('arn:aws:route53:::hostedzone/Z123');
expect(sanitized).not.toContain('123456789012');
expect(sanitized.length).toBeLessThanOrEqual(504);
});
it('truncates messages longer than 500 characters', () => {
const longMessage = 'x'.repeat(1000);
const sanitized = sanitizeAwsError(new Error(longMessage));
expect(sanitized.length).toBeLessThanOrEqual(504);
expect(sanitized.endsWith('...')).toBe(true);
});
it('never throws on a non-Error input and returns a string', () => {
expect(() => sanitizeAwsError('a plain string error')).not.toThrow();
expect(typeof sanitizeAwsError('a plain string error')).toBe('string');
expect(() => sanitizeAwsError(undefined)).not.toThrow();
expect(typeof sanitizeAwsError(undefined)).toBe('string');
expect(() => sanitizeAwsError({ weird: 'object' })).not.toThrow();
expect(typeof sanitizeAwsError(null)).toBe('string');
});
});