test(24-01): add failing test for Route 53 credential factory
- lib/types/route53.ts: camelCase interfaces for zones/records/history/audit-log/sync-result - lib/services/route53-factory.test.ts: isRoute53Configured() + getRoute53Client() behavior cases — fails RED, factory module does not exist yet
This commit is contained in:
parent
b9df27b656
commit
4dd9d5dab8
2 changed files with 163 additions and 0 deletions
63
lib/services/route53-factory.test.ts
Normal file
63
lib/services/route53-factory.test.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { isRoute53Configured, getRoute53Client, resetRoute53Client } from './route53-factory';
|
||||||
|
|
||||||
|
const ORIGINAL_ENV = { ...process.env };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
delete process.env.AWS_ACCESS_KEY_ID;
|
||||||
|
delete process.env.AWS_SECRET_ACCESS_KEY;
|
||||||
|
delete process.env.AWS_REGION;
|
||||||
|
resetRoute53Client();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = { ...ORIGINAL_ENV };
|
||||||
|
resetRoute53Client();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isRoute53Configured', () => {
|
||||||
|
it('returns false when AWS_ACCESS_KEY_ID is unset', () => {
|
||||||
|
process.env.AWS_SECRET_ACCESS_KEY = 'secret1';
|
||||||
|
expect(isRoute53Configured()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when AWS_SECRET_ACCESS_KEY is unset', () => {
|
||||||
|
process.env.AWS_ACCESS_KEY_ID = 'id1';
|
||||||
|
expect(isRoute53Configured()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when both are set to empty strings', () => {
|
||||||
|
process.env.AWS_ACCESS_KEY_ID = '';
|
||||||
|
process.env.AWS_SECRET_ACCESS_KEY = '';
|
||||||
|
expect(isRoute53Configured()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when both are set to non-empty values', () => {
|
||||||
|
process.env.AWS_ACCESS_KEY_ID = 'id1';
|
||||||
|
process.env.AWS_SECRET_ACCESS_KEY = 'secret1';
|
||||||
|
expect(isRoute53Configured()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRoute53Client', () => {
|
||||||
|
it('throws an Error mentioning AWS_ACCESS_KEY_ID when credentials are absent', () => {
|
||||||
|
expect(() => getRoute53Client()).toThrow(/AWS_ACCESS_KEY_ID/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the same cached instance on a second call', () => {
|
||||||
|
process.env.AWS_ACCESS_KEY_ID = 'id1';
|
||||||
|
process.env.AWS_SECRET_ACCESS_KEY = 'secret1';
|
||||||
|
const first = getRoute53Client();
|
||||||
|
const second = getRoute53Client();
|
||||||
|
expect(second).toBe(first);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a different instance after resetRoute53Client()', () => {
|
||||||
|
process.env.AWS_ACCESS_KEY_ID = 'id1';
|
||||||
|
process.env.AWS_SECRET_ACCESS_KEY = 'secret1';
|
||||||
|
const first = getRoute53Client();
|
||||||
|
resetRoute53Client();
|
||||||
|
const second = getRoute53Client();
|
||||||
|
expect(second).not.toBe(first);
|
||||||
|
});
|
||||||
|
});
|
||||||
100
lib/types/route53.ts
Normal file
100
lib/types/route53.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
/**
|
||||||
|
* AWS Route 53 DNS sync — shared type definitions.
|
||||||
|
* API-response shapes (camelCase) — route handlers transform snake_case
|
||||||
|
* Postgres rows into these manually (no ORM, per CLAUDE.md).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Zone / Record mirror types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface Route53Zone {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
comment: string | null;
|
||||||
|
privateZone: boolean;
|
||||||
|
recordCount: number;
|
||||||
|
authoritativeNameServers: string[] | null;
|
||||||
|
syncedAt: string;
|
||||||
|
isDeleted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Route53RecordValue {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Route53Record {
|
||||||
|
recordKey: string;
|
||||||
|
zoneId: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
setIdentifier: string | null;
|
||||||
|
ttl: number | null;
|
||||||
|
resourceRecords: Route53RecordValue[] | null;
|
||||||
|
aliasTarget: Record<string, unknown> | null;
|
||||||
|
syncedAt: string;
|
||||||
|
isDeleted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Change ledger / audit log types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type Route53HistorySource = 'pulse_crud' | 'sync_detected_drift';
|
||||||
|
|
||||||
|
export interface Route53RecordHistory {
|
||||||
|
id: string;
|
||||||
|
zoneId: string;
|
||||||
|
recordKey: string;
|
||||||
|
recordName: string;
|
||||||
|
recordType: string;
|
||||||
|
changeAction: 'create' | 'update' | 'delete';
|
||||||
|
beforeValue: Record<string, unknown> | null;
|
||||||
|
afterValue: Record<string, unknown> | null;
|
||||||
|
source: Route53HistorySource;
|
||||||
|
changedByUserId: string | null;
|
||||||
|
changedByEmail: string | null;
|
||||||
|
changedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Route53AuditStatus = 'pending' | 'committed' | 'failed';
|
||||||
|
|
||||||
|
export interface Route53AuditLog {
|
||||||
|
id: string;
|
||||||
|
operation: 'create' | 'update' | 'delete' | 'sync';
|
||||||
|
zoneId: string | null;
|
||||||
|
recordKey: string | null;
|
||||||
|
recordName: string | null;
|
||||||
|
recordType: string | null;
|
||||||
|
beforeValue: Record<string, unknown> | null;
|
||||||
|
afterValue: Record<string, unknown> | null;
|
||||||
|
performedByUserId: string | null;
|
||||||
|
performedByEmail: string | null;
|
||||||
|
performedAt: string;
|
||||||
|
completedAt: string | null;
|
||||||
|
status: Route53AuditStatus;
|
||||||
|
awsChangeId: string | null;
|
||||||
|
awsChangeStatus: string | null;
|
||||||
|
errorMessage: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Writable record types (D-01)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type Route53WritableType = 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'SRV';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Sync result
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface Route53SyncResult {
|
||||||
|
syncId: string;
|
||||||
|
syncType: string;
|
||||||
|
status: string;
|
||||||
|
startedAt: string;
|
||||||
|
completedAt: string | null;
|
||||||
|
duration: number | null;
|
||||||
|
entities: Record<string, unknown>;
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue