import { describe, expect, it } from "vitest"; import { createMemoryDb, repos, runMigrations } from "@cfdm/db"; import { SYNC_SYNCED } from "@cfdm/shared"; import type { CloudflareClient } from "../src/lib/cf-client.js"; import { updateConfig } from "../src/services/service-config-service.js"; type CfRec = { id: string; type: string; name: string; content: string; ttl: number; proxied: boolean; }; function setupDb() { const { db, sqlite } = createMemoryDb(); runMigrations(sqlite); return db; } describe("DNS reconcile for extra FQDN", () => { it("publishes extra A even when local/CF already have CNAME on same name", async () => { const db = setupDb(); const remote: CfRec[] = [ { id: "cf-cname-nsgt", type: "CNAME", name: "nsgt.example.com", content: "legacy.example.com", ttl: 1, proxied: false, }, ]; const created: Array<{ type: string; name: string; content: string }> = []; const deleted: string[] = []; const cf = { listDnsRecords: async () => remote, createDnsRecord: async ( _zoneId: string, payload: { type: string; name: string; content: string }, ) => { created.push(payload); const rec: CfRec = { id: `cf-new-${created.length}`, type: payload.type, name: payload.name, content: payload.content, ttl: 1, proxied: false, }; remote.push(rec); return rec; }, updateDnsRecord: async ( _zoneId: string, id: string, payload: { type: string; name: string; content: string }, ) => { const idx = remote.findIndex((r) => r.id === id); const rec: CfRec = { id, type: payload.type, name: payload.name, content: payload.content, ttl: 1, proxied: false, }; if (idx >= 0) remote[idx] = rec; else remote.push(rec); return rec; }, deleteDnsRecord: async (_zoneId: string, id: string) => { deleted.push(id); const idx = remote.findIndex((r) => r.id === id); if (idx >= 0) remote.splice(idx, 1); }, verifyToken: async () => true, listZones: async () => [ { id: "zone-1", name: "example.com", status: "active" }, ], } as unknown as CloudflareClient; const domain = repos.createDomain(db, null, "example.com", "zone-1"); // Leftover local CNAME (previous service / manual import). repos.insertDnsRecord( db, domain.id, "CNAME", "nsgt.example.com", "legacy.example.com", 1, false, null, SYNC_SYNCED, "cloudflare", "cf-cname-nsgt", ); const service = repos.createService(db, "MSK Hip", "tg-msk-hip"); repos.setServiceEnabled(db, service.id, true); const view = await updateConfig(db, cf, service.id, { ips: ["130.49.213.176"], domains: [ { fqdn: "gt.example.com", target_ips: ["130.49.213.176"], }, { fqdn: "nsgt.example.com", target_ips: ["130.49.213.176"], }, ], }); const nsgt = view.domains.find((d) => d.fqdn === "nsgt.example.com"); expect(nsgt?.record_type).toBe("A"); expect(nsgt?.target_ips).toEqual(["130.49.213.176"]); expect(nsgt?.target_cname).toBeFalsy(); const binding = repos .listBindingsByService(db, service.id) .find((b) => b.hostname === "nsgt")!; expect(repos.listBindingIps(db, binding.id)).toEqual(["130.49.213.176"]); expect(deleted).toContain("cf-cname-nsgt"); expect( created.some( (r) => r.type === "A" && (r.name === "nsgt" || r.name === "nsgt.example.com") && r.content === "130.49.213.176", ), ).toBe(true); expect(remote.some((r) => r.type === "CNAME" && r.name.includes("nsgt"))).toBe( false, ); expect( remote.some( (r) => r.type === "A" && r.name.includes("nsgt") && r.content === "130.49.213.176", ), ).toBe(true); }); it("replaces stale A content for extra FQDN on the same hostname", async () => { const db = setupDb(); const remote: CfRec[] = [ { id: "cf-stale-a", type: "A", name: "nsgt.example.com", content: "1.1.1.1", ttl: 1, proxied: false, }, ]; const deleted: string[] = []; const created: Array<{ type: string; name: string; content: string }> = []; const cf = { listDnsRecords: async () => [...remote], createDnsRecord: async ( _zoneId: string, payload: { type: string; name: string; content: string }, ) => { created.push(payload); const rec: CfRec = { id: `cf-new-${created.length}`, type: payload.type, name: payload.name, content: payload.content, ttl: 1, proxied: false, }; remote.push(rec); return rec; }, updateDnsRecord: async ( _zoneId: string, id: string, payload: { type: string; name: string; content: string }, ) => ({ id, type: payload.type, name: payload.name, content: payload.content, ttl: 1, proxied: false, }), deleteDnsRecord: async (_zoneId: string, id: string) => { deleted.push(id); const idx = remote.findIndex((r) => r.id === id); if (idx >= 0) remote.splice(idx, 1); }, verifyToken: async () => true, listZones: async () => [ { id: "zone-1", name: "example.com", status: "active" }, ], } as unknown as CloudflareClient; repos.createDomain(db, null, "example.com", "zone-1"); const service = repos.createService(db, "MSK", "msk"); repos.setServiceEnabled(db, service.id, true); await updateConfig(db, cf, service.id, { ips: ["130.49.213.176"], domains: [ { fqdn: "nsgt.example.com", target_ips: ["130.49.213.176"] }, ], }); expect(deleted).toContain("cf-stale-a"); expect( created.some( (r) => r.type === "A" && r.content === "130.49.213.176", ), ).toBe(true); expect(remote.map((r) => r.content)).toEqual(["130.49.213.176"]); }); });