/** * 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'); }); });