import { describe, expect, it } from "vitest"; import { repos } from "@cfdm/db"; import type { CloudflareClient } from "../src/lib/cf-client.js"; import { buildApp } from "../src/app.js"; import { loadConfig } from "../src/config.js"; import { changeBindingIp } from "../src/services/change-ip-service.js"; import { withBindingLock } from "../src/services/routing/index.js"; import { updateConfig } from "../src/services/service-config-service.js"; function mockCf(): CloudflareClient { return { listDnsRecords: async () => [], createDnsRecord: async (_zoneId: string, payload: { type: string; name: string; content: string }) => ({ id: `cf-${payload.name}-${payload.content}`, type: payload.type, name: payload.name, content: payload.content, ttl: 1, proxied: false, }), 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, }), patchDnsRecord: async ( _zoneId: string, id: string, payload: { content?: string }, ) => ({ id, type: "A", name: "panel.example.com", content: payload.content ?? "0.0.0.0", ttl: 1, proxied: false, }), deleteDnsRecord: async () => undefined, listZones: async () => [{ id: "zone-1", name: "example.com", status: "active" }], } as unknown as CloudflareClient; } describe("change-ip", () => { it("dry-run previews from → to without writing", async () => { const app = await buildApp({ config: { ...loadConfig(), staticDir: null }, memory: true, }); const cf = mockCf(); repos.createDomain(app.db, null, "example.com", "zone-1"); const service = repos.createService(app.db, "Panel", "panel"); await updateConfig(app.db, cf, service.id, { ips: ["10.0.0.10"], domains: [{ fqdn: "panel.example.com", target_ips: ["10.0.0.10"] }], }); const bindings = repos.listBindingsByService(app.db, service.id); const preview = await changeBindingIp(app.db, cf, bindings[0]!.id, { from_ip: "10.0.0.10", to_ip: "10.0.0.20", dry_run: true, }); expect(preview.applied).toBe(false); expect(preview.message).toBe("10.0.0.10 → 10.0.0.20"); expect(repos.listBindingIps(app.db, bindings[0]!.id)).toEqual(["10.0.0.10"]); await app.close(); }); it("apply patches binding IP", async () => { const app = await buildApp({ config: { ...loadConfig(), staticDir: null }, memory: true, }); const cf = mockCf(); repos.createDomain(app.db, null, "example.com", "zone-1"); const service = repos.createService(app.db, "Panel", "panel"); await updateConfig(app.db, cf, service.id, { ips: ["10.0.0.10"], domains: [{ fqdn: "panel.example.com", target_ips: ["10.0.0.10"] }], }); const binding = repos.listBindingsByService(app.db, service.id)[0]!; const result = await changeBindingIp(app.db, cf, binding.id, { from_ip: "10.0.0.10", to_ip: "10.0.0.20", }); expect(result.applied).toBe(true); expect(repos.listBindingIps(app.db, binding.id)).toEqual(["10.0.0.20"]); await app.close(); }); it("serializes concurrent binding locks", async () => { const order: number[] = []; await Promise.all([ withBindingLock(1, async () => { await new Promise((r) => setTimeout(r, 20)); order.push(1); }), withBindingLock(1, async () => { order.push(2); }), ]); expect(order).toEqual([1, 2]); }); });