- createPendingAuditLog/markAuditCommitted/markAuditFailed implement the pending -> committed/failed lifecycle (D-07, SC-3); markAuditFailed always sanitizes via sanitizeAwsError - insertPulseCrudHistory writes 'pulse_crud' history rows, documented as callable only after a committed write - upsertMirrorRecord/softDeleteMirrorRecord/loadMirrorRecord manage the route53_records mirror; mirror writes are best-effort and soft-delete only (D-08), audit/history writes are not best-effort - log same pre-existing itglue-search.test.ts failures (unrelated, out of scope) in deferred-items.md
180 lines
5.7 KiB
TypeScript
180 lines
5.7 KiB
TypeScript
/**
|
|
* lib/services/route53-write-persistence.ts unit tests — SQL contract checks
|
|
* against a mocked postgresClient. No real Postgres connection is made.
|
|
* Follows the mocking discipline in lib/services/pax8-sync-service.test.ts.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
// Mock postgresClient BEFORE importing the module under test.
|
|
const queryMock = vi.fn();
|
|
vi.mock('@/lib/services/postgres-client', () => ({
|
|
default: {
|
|
query: (...args: unknown[]) => queryMock(...args),
|
|
},
|
|
}));
|
|
|
|
// Import AFTER the mock is declared so vi.mock hoisting takes effect.
|
|
import {
|
|
createPendingAuditLog,
|
|
markAuditCommitted,
|
|
markAuditFailed,
|
|
insertPulseCrudHistory,
|
|
upsertMirrorRecord,
|
|
softDeleteMirrorRecord,
|
|
loadMirrorRecord,
|
|
} from './route53-write-persistence';
|
|
|
|
interface MockCall {
|
|
sql: string;
|
|
params: unknown[];
|
|
}
|
|
|
|
function calls(): MockCall[] {
|
|
return queryMock.mock.calls.map(([sql, params]) => ({
|
|
sql: String(sql),
|
|
params: (params as unknown[]) ?? [],
|
|
}));
|
|
}
|
|
|
|
beforeEach(() => {
|
|
queryMock.mockReset();
|
|
});
|
|
|
|
describe('createPendingAuditLog', () => {
|
|
it("issues an INSERT into route53_audit_log whose SQL contains 'pending'", async () => {
|
|
queryMock.mockResolvedValueOnce({ rows: [{ id: 'audit-1' }] });
|
|
const result = await createPendingAuditLog({
|
|
operation: 'create',
|
|
zoneId: 'Z123',
|
|
recordKey: 'Z123:www.example.com.:A:',
|
|
recordName: 'www.example.com.',
|
|
recordType: 'A',
|
|
beforeValue: null,
|
|
afterValue: { ttl: 300, resourceRecords: [{ value: '1.2.3.4' }] },
|
|
performedByUserId: 'user-1',
|
|
performedByEmail: 'lorentz@wulfconsulting.com',
|
|
});
|
|
expect(result).toEqual({ id: 'audit-1' });
|
|
expect(calls()).toHaveLength(1);
|
|
expect(calls()[0].sql).toContain('INSERT INTO route53_audit_log');
|
|
expect(calls()[0].sql).toContain("'pending'");
|
|
});
|
|
});
|
|
|
|
describe('markAuditCommitted', () => {
|
|
it("issues an UPDATE setting status = 'committed'", async () => {
|
|
queryMock.mockResolvedValueOnce({ rows: [] });
|
|
await markAuditCommitted('audit-1', 'change-1', 'INSYNC', { ResponseMetadata: {} });
|
|
expect(calls()[0].sql).toContain('UPDATE route53_audit_log');
|
|
expect(calls()[0].sql).toContain("'committed'");
|
|
});
|
|
});
|
|
|
|
describe('markAuditFailed', () => {
|
|
it('passes a sanitized error string — a raw AWS key id never reaches the bound parameters', async () => {
|
|
queryMock.mockResolvedValueOnce({ rows: [] });
|
|
const rawError = new Error('AccessDenied for AKIAIOSFODNN7EXAMPLE');
|
|
await markAuditFailed('audit-1', rawError);
|
|
|
|
expect(calls()).toHaveLength(1);
|
|
expect(calls()[0].sql).toContain("'failed'");
|
|
const boundParams = calls()[0].params;
|
|
expect(boundParams).not.toContain(rawError);
|
|
for (const param of boundParams) {
|
|
if (typeof param === 'string') {
|
|
expect(param).not.toContain('AKIAIOSFODNN7EXAMPLE');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('insertPulseCrudHistory', () => {
|
|
it("binds the literal 'pulse_crud' as the history source", async () => {
|
|
queryMock.mockResolvedValueOnce({ rows: [] });
|
|
await insertPulseCrudHistory({
|
|
zoneId: 'Z123',
|
|
recordKey: 'Z123:www.example.com.:A:',
|
|
recordName: 'www.example.com.',
|
|
recordType: 'A',
|
|
changeAction: 'create',
|
|
beforeValue: null,
|
|
afterValue: { ttl: 300 },
|
|
changedByUserId: 'user-1',
|
|
changedByEmail: 'lorentz@wulfconsulting.com',
|
|
auditLogId: 'audit-1',
|
|
});
|
|
expect(calls()[0].sql).toContain('INSERT INTO route53_record_history');
|
|
expect(calls()[0].sql).toContain("'pulse_crud'");
|
|
});
|
|
});
|
|
|
|
describe('upsertMirrorRecord', () => {
|
|
it('issues an INSERT ... ON CONFLICT upsert and never throws even if the query rejects', async () => {
|
|
queryMock.mockRejectedValueOnce(new Error('connection reset'));
|
|
await expect(
|
|
upsertMirrorRecord({
|
|
recordKey: 'Z123:www.example.com.:A:',
|
|
zoneId: 'Z123',
|
|
name: 'www.example.com.',
|
|
type: 'A',
|
|
setIdentifier: null,
|
|
ttl: 300,
|
|
resourceRecords: [{ value: '1.2.3.4' }],
|
|
aliasTarget: null,
|
|
rawPayload: null,
|
|
})
|
|
).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('softDeleteMirrorRecord', () => {
|
|
it('issues an UPDATE and never a DELETE FROM', async () => {
|
|
queryMock.mockResolvedValueOnce({ rows: [] });
|
|
await softDeleteMirrorRecord('Z123:www.example.com.:A:');
|
|
expect(calls()[0].sql).toContain('UPDATE route53_records');
|
|
expect(calls()[0].sql).not.toContain('DELETE FROM');
|
|
});
|
|
|
|
it('never throws even if the query rejects (best-effort)', async () => {
|
|
queryMock.mockRejectedValueOnce(new Error('connection reset'));
|
|
await expect(softDeleteMirrorRecord('Z123:www.example.com.:A:')).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('loadMirrorRecord', () => {
|
|
it('returns a camelCase object when found', async () => {
|
|
queryMock.mockResolvedValueOnce({
|
|
rowCount: 1,
|
|
rows: [
|
|
{
|
|
record_key: 'Z123:www.example.com.:A:',
|
|
zone_id: 'Z123',
|
|
name: 'www.example.com.',
|
|
type: 'A',
|
|
set_identifier: null,
|
|
ttl: 300,
|
|
resource_records: [{ value: '1.2.3.4' }],
|
|
alias_target: null,
|
|
},
|
|
],
|
|
});
|
|
const result = await loadMirrorRecord('Z123:www.example.com.:A:');
|
|
expect(result).toEqual({
|
|
recordKey: 'Z123:www.example.com.:A:',
|
|
zoneId: 'Z123',
|
|
name: 'www.example.com.',
|
|
type: 'A',
|
|
setIdentifier: null,
|
|
ttl: 300,
|
|
resourceRecords: [{ value: '1.2.3.4' }],
|
|
aliasTarget: null,
|
|
});
|
|
});
|
|
|
|
it('returns null when not found', async () => {
|
|
queryMock.mockResolvedValueOnce({ rowCount: 0, rows: [] });
|
|
const result = await loadMirrorRecord('missing-key');
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|