import { describe, expect, it } from "vitest"; import { cfdmBindingSyncItemSchema, type ServiceBindingView, } from "@cfdm/shared"; import { resolveBindingIpsForSync, resolveLbModeForSync, } from "../src/services/vps-tracker-sync.js"; function binding( partial: Partial & Pick, ): ServiceBindingView { return { domain_id: 1, service_id: 1, dns_record_id: null, group_id: null, group_name: null, service_name: "svc", service_slug: "svc", target_ip: null, target_ips: [], target_ip_weights: {}, target_ip_priorities: {}, cname_target: null, lb_mode: "off", health_check_enabled: false, health_check_type: "tcp", health_check_port: null, health_check_path: null, health_check_expected_status: null, health_check_interval_sec: 60, health_check_timeout_ms: 3000, health_check_verify_tls: true, sync_status: null, created_at: "", updated_at: "", ...partial, }; } describe("resolveBindingIpsForSync", () => { it("uses A-record IPs when present", async () => { const a = binding({ id: 1, hostname: "ihome", zone_name: "rkns.top", target_ips: ["10.0.0.5"], }); const index = { byFqdn: new Map([["ihome.rkns.top", a]]) }; const ips = await resolveBindingIpsForSync(a, ["9.9.9.9"], index); expect(ips).toEqual(["10.0.0.5"]); }); it("resolves CNAME via local binding chain to VPS IP", async () => { const target = binding({ id: 1, hostname: "ihome", zone_name: "rkns.top", target_ips: ["203.0.113.10"], }); const cname = binding({ id: 2, hostname: "imsk", zone_name: "rkns.top", cname_target: "ihome.rkns.top", target_ips: [], }); const index = { byFqdn: new Map([ ["ihome.rkns.top", target], ["imsk.rkns.top", cname], ]), }; const ips = await resolveBindingIpsForSync(cname, [], index); expect(ips).toEqual(["203.0.113.10"]); }); it("resolves short CNAME target relative to zone", async () => { const target = binding({ id: 1, hostname: "ihome", zone_name: "rkns.top", target_ips: ["203.0.113.11"], }); const cname = binding({ id: 2, hostname: "imsk", zone_name: "rkns.top", cname_target: "ihome", target_ips: [], }); const index = { byFqdn: new Map([ ["ihome.rkns.top", target], ["imsk.rkns.top", cname], ]), }; const ips = await resolveBindingIpsForSync(cname, [], index); expect(ips).toEqual(["203.0.113.11"]); }); it("falls back to service IPs when CNAME target unknown locally and DNS fails", async () => { const cname = binding({ id: 2, hostname: "imsk", zone_name: "rkns.top", cname_target: "definitely-not-resolvable-xyz.invalid", target_ips: [], }); const index = { byFqdn: new Map([["imsk.rkns.top", cname]]) }; const ips = await resolveBindingIpsForSync(cname, ["198.51.100.7"], index); expect(ips).toEqual(["198.51.100.7"]); }); it("does not treat CNAME hostname in target_ips as an IP", async () => { const target = binding({ id: 1, hostname: "ihome", zone_name: "rkns.top", target_ips: ["203.0.113.10"], }); const cname = binding({ id: 2, hostname: "imsk", zone_name: "rkns.top", cname_target: "ihome.rkns.top", // как в прод: JOIN dns_record кладёт CNAME content в target_ip → target_ips target_ips: ["ihome.rkns.top"], }); const index = { byFqdn: new Map([ ["ihome.rkns.top", target], ["imsk.rkns.top", cname], ]), }; const ips = await resolveBindingIpsForSync(cname, [], index); expect(ips).toEqual(["203.0.113.10"]); }); it("treats missing target_ips as empty instead of throwing", async () => { const cname = binding({ id: 2, hostname: "imsk", zone_name: "rkns.top", cname_target: "ihome.rkns.top", }); delete (cname as { target_ips?: string[] }).target_ips; const index = { byFqdn: new Map([["imsk.rkns.top", cname]]) }; const ips = await resolveBindingIpsForSync(cname, ["198.51.100.9"], index); expect(ips).toEqual(["198.51.100.9"]); }); it("prefers service IPs over empty CNAME resolution chain", async () => { const cname = binding({ id: 2, hostname: "mhome", zone_name: "rkns.top", cname_target: "macloud.rkns.top", target_ips: ["macloud.rkns.top"], }); const index = { byFqdn: new Map([["mhome.rkns.top", cname]]) }; const ips = await resolveBindingIpsForSync(cname, ["203.0.113.55"], index); expect(ips).toEqual(["203.0.113.55"]); }); }); describe("resolveLbModeForSync", () => { it("prefers binding lb_mode", () => { expect(resolveLbModeForSync("failover", "round_robin")).toBe("failover"); }); it("falls back to service group lb_mode", () => { expect(resolveLbModeForSync("off", "weighted")).toBe("weighted"); expect(resolveLbModeForSync(undefined, "round_robin")).toBe("round_robin"); }); it("returns undefined when neither is a known mode", () => { expect(resolveLbModeForSync("off", "off")).toBeUndefined(); expect(resolveLbModeForSync(null, undefined)).toBeUndefined(); }); }); describe("cfdmBindingSyncItemSchema lbMode", () => { const base = { bindingId: 1, serviceId: 10, serviceName: "VPN", serviceSlug: "vpn", fqdn: "vpn.example.com", zoneName: "example.com", hostname: "vpn", ips: ["203.0.113.10"], }; it("accepts optional lbMode on sync payload", () => { const parsed = cfdmBindingSyncItemSchema.parse({ ...base, lbMode: "failover" }); expect(parsed.lbMode).toBe("failover"); }); it("accepts payload without lbMode (legacy)", () => { const parsed = cfdmBindingSyncItemSchema.parse(base); expect(parsed.lbMode).toBeUndefined(); }); });