diff --git a/lib/services/route53-sync-service.test.ts b/lib/services/route53-sync-service.test.ts new file mode 100644 index 0000000..c8347fa --- /dev/null +++ b/lib/services/route53-sync-service.test.ts @@ -0,0 +1,126 @@ +/** + * lib/services/route53-sync-service.ts unit tests. + * + * Only buildDriftHistoryRows is exercised here — a pure function taking + * hand-constructed NormalizedRecordSet maps, no AWS or Postgres mocking + * required (matches the plan's stated test scope for this file). + */ + +import { describe, it, expect } from 'vitest'; +import { buildDriftHistoryRows } from './route53-sync-service'; +import type { NormalizedRecordSet } from './route53-record-key'; + +function makeRecordSet(overrides: Partial = {}): NormalizedRecordSet { + return { + recordKey: 'Z123:www.example.com.:A:', + zoneId: 'Z123', + name: 'www.example.com.', + type: 'A', + setIdentifier: null, + ttl: 300, + resourceRecords: [{ value: '10.0.0.1' }], + aliasTarget: null, + ...overrides, + }; +} + +describe('buildDriftHistoryRows', () => { + it('produces one update row with non-null before/after when TTL changes', () => { + const key = 'Z123:www.example.com.:A:'; + const prev = makeRecordSet({ ttl: 300 }); + const next = makeRecordSet({ ttl: 600 }); + + const rows = buildDriftHistoryRows( + new Map([[key, prev]]), + new Map([[key, next]]), + 'Z123' + ); + + expect(rows).toHaveLength(1); + expect(rows[0].changeAction).toBe('update'); + expect(rows[0].zoneId).toBe('Z123'); + expect(rows[0].recordKey).toBe(key); + expect(rows[0].beforeValue).not.toBeNull(); + expect(rows[0].afterValue).not.toBeNull(); + }); + + it('produces a delete row with non-null before and null after when the mirror row has no AWS match', () => { + const key = 'Z123:gone.example.com.:A:'; + const prev = makeRecordSet({ recordKey: key, name: 'gone.example.com.' }); + + const rows = buildDriftHistoryRows(new Map([[key, prev]]), new Map(), 'Z123'); + + expect(rows).toHaveLength(1); + expect(rows[0].changeAction).toBe('delete'); + expect(rows[0].beforeValue).not.toBeNull(); + expect(rows[0].afterValue).toBeNull(); + }); + + it('produces a create row with null before and non-null after when AWS has no matching mirror row', () => { + const key = 'Z123:new.example.com.:A:'; + const next = makeRecordSet({ recordKey: key, name: 'new.example.com.' }); + + const rows = buildDriftHistoryRows(new Map(), new Map([[key, next]]), 'Z123'); + + expect(rows).toHaveLength(1); + expect(rows[0].changeAction).toBe('create'); + expect(rows[0].beforeValue).toBeNull(); + expect(rows[0].afterValue).not.toBeNull(); + }); + + it('produces zero rows when mirror and AWS recordsets are identical', () => { + const key = 'Z123:www.example.com.:A:'; + const prev = makeRecordSet(); + const next = makeRecordSet(); + + const rows = buildDriftHistoryRows( + new Map([[key, prev]]), + new Map([[key, next]]), + 'Z123' + ); + + expect(rows).toHaveLength(0); + }); + + it('stores the whole recordset (name, type, setIdentifier, ttl, resourceRecords, aliasTarget) in before/after, not a per-field delta', () => { + const key = 'Z123:www.example.com.:A:'; + const prev = makeRecordSet({ ttl: 300 }); + const next = makeRecordSet({ ttl: 600 }); + + const rows = buildDriftHistoryRows( + new Map([[key, prev]]), + new Map([[key, next]]), + 'Z123' + ); + + expect(rows[0].afterValue).toEqual({ + name: 'www.example.com.', + type: 'A', + setIdentifier: null, + ttl: 600, + resourceRecords: [{ value: '10.0.0.1' }], + aliasTarget: null, + }); + }); + + it('a record changed by Pulse CRUD in the same window is still tagged as drift by the sync (sync cannot distinguish actor)', () => { + // The sync has no notion of "who changed this" — it only compares + // mirror vs. AWS-live state. A CRUD-originated change looks identical + // to an out-of-band AWS console edit from the sync's perspective, so it + // still produces a drift row here. The CRUD route's own pulse_crud row + // (written elsewhere, not by this function) is the authoritative one; + // both rows are expected to coexist in the ledger. + const key = 'Z123:www.example.com.:A:'; + const prev = makeRecordSet({ ttl: 300 }); + const next = makeRecordSet({ ttl: 900 }); // as if Pulse CRUD had just updated this + + const rows = buildDriftHistoryRows( + new Map([[key, prev]]), + new Map([[key, next]]), + 'Z123' + ); + + expect(rows).toHaveLength(1); + expect(rows[0].changeAction).toBe('update'); + }); +});