import { describe, expect, it } from "vitest"; import { createMemoryDb, repos, runMigrations } from "@cfdm/db"; import type { CloudflareClient } from "../src/lib/cf-client.js"; import { updateConfig } from "../src/services/service-config-service.js"; function mockCf(): CloudflareClient { return { listDnsRecords: async () => [], createDnsRecord: async () => ({ id: "cf-new", type: "A", name: "api.example.com", content: "10.0.0.1", ttl: 1, proxied: false, }), updateDnsRecord: async () => ({ id: "cf-upd", type: "A", name: "api.example.com", content: "10.0.0.1", ttl: 1, proxied: false, }), deleteDnsRecord: async () => undefined, verifyToken: async () => true, listZones: async () => [], } as unknown as CloudflareClient; } function setupDb() { const { db, sqlite } = createMemoryDb(); runMigrations(sqlite); return db; } describe("service bindings prune", () => { it("updateConfig with domains: [] removes all bindings", async () => { const db = setupDb(); const cf = mockCf(); const domain = repos.createDomain(db, null, "example.com", "cf-zone-example"); const service = repos.createService(db, "Web", "web"); repos.replaceServiceIps(db, service.id, ["10.0.0.1"]); const binding = repos.insertBinding(db, domain.id, service.id, "api", null); repos.replaceBindingIps(db, binding.id, ["10.0.0.1"]); expect(repos.listBindingsByService(db, service.id)).toHaveLength(1); const view = await updateConfig(db, cf, service.id, { ips: ["10.0.0.1"], domains: [], }); expect(repos.listBindingsByService(db, service.id)).toHaveLength(0); expect(view.domains).toEqual([]); }); it("updateConfig keeps multiple FQDN bindings", async () => { const db = setupDb(); const cf = mockCf(); repos.createDomain(db, null, "a.example", "cf-zone-a"); repos.createDomain(db, null, "b.example", "cf-zone-b"); const service = repos.createService(db, "Edge", "edge"); repos.replaceServiceIps(db, service.id, ["10.0.0.2"]); const view = await updateConfig(db, cf, service.id, { ips: ["10.0.0.2"], domains: [ { fqdn: "api.a.example", target_ips: ["10.0.0.2"] }, { fqdn: "www.b.example", target_ips: ["10.0.0.2"] }, ], }); expect(view.domains).toHaveLength(2); const bindings = repos.listBindingsByService(db, service.id); expect(bindings).toHaveLength(2); expect(bindings.map((b) => b.hostname).sort()).toEqual(["api", "www"]); }); it("updateConfig with extra FQDN per IP does not throw when group health-check is on", async () => { const db = setupDb(); const cf = mockCf(); const health = { health_check_enabled: true, health_check_type: "tcp" as const, health_check_port: 443, health_check_providers: ["local", "cloudflare", "globalping"] as const, health_check_aggregate: "majority" as const, }; repos.createDomain(db, null, "example.com", "cf-zone-example"); const group = repos.createServiceGroup( db, "VPN", "vpn", null, "vpn.example.com", { ...health }, ); const service = repos.createService(db, "GT", "gt"); repos.setServiceGroup(db, service.id, group.id); repos.setServiceEnabled(db, service.id, true); const view = await updateConfig(db, cf, service.id, { ips: ["93.115.203.183", "130.49.213.153"], domains: [ { fqdn: "gt.example.com", target_ips: ["93.115.203.183", "130.49.213.153"], ...health, }, { fqdn: "rutg.example.com", target_ips: ["93.115.203.183"], ...health, }, { fqdn: "nsgt.example.com", target_ips: ["130.49.213.153"], ...health, }, ], }); expect(view.domains).toHaveLength(3); expect(view.ips.sort()).toEqual(["130.49.213.153", "93.115.203.183"]); }); });