import { describe, expect, it } from "vitest"; import { serviceGroupsResponseSchema } from "@cfdm/shared"; 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 { listGroupViews, 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}`, 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, }), deleteDnsRecord: async () => undefined, verifyToken: async () => true, listZones: async () => [{ id: "zone-1", name: "example.com", status: "active" }], } as unknown as CloudflareClient; } async function authHeaders(app: Awaited>) { const config = loadConfig(); const res = await app.inject({ method: "POST", url: "/api/v1/auth/login", payload: { username: config.adminUsername, password: "admin" }, }); expect(res.statusCode).toBe(200); const { token } = res.json() as { token: string }; return { authorization: `Bearer ${token}` }; } describe("create service then list groups", () => { it("create + updateConfig then listGroupViews parses with shared Zod schema", async () => { const app = await buildApp({ config: { ...loadConfig(), staticDir: null }, memory: true, }); const headers = await authHeaders(app); const cf = mockCf(); repos.createDomain(app.db, null, "example.com", "zone-1"); const group = repos.createServiceGroup( app.db, "VPN", "vpn", null, "vpn.example.com", ); const createRes = await app.inject({ method: "POST", url: "/api/v1/services", headers, payload: { name: "Panel", slug: "panel", service_group_id: group.id, }, }); expect(createRes.statusCode).toBe(200); const created = createRes.json() as { id: number; name: string; enabled: boolean; }; expect(created.id).toBeTypeOf("number"); expect(created.enabled).toBe(true); await updateConfig(app.db, cf, created.id, { ips: ["1.2.3.4"], service_group_id: group.id, domains: [ { fqdn: "panel.example.com", target_ips: ["1.2.3.4"], target_ip_weights: { "1.2.3.4": 1 }, target_ip_priorities: { "1.2.3.4": 1 }, lb_mode: "round_robin", health_check_enabled: true, health_check_type: "tcp", health_check_port: 443, health_check_path: null, health_check_expected_status: null, health_check_interval_sec: 30, health_check_timeout_ms: 3000, health_check_verify_tls: false, }, ], }); const body = await listGroupViews(app.db); const parsed = serviceGroupsResponseSchema.safeParse(body); expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true); const listRes = await app.inject({ method: "GET", url: "/api/v1/service-groups", headers, }); expect(listRes.statusCode).toBe(200); const httpParsed = serviceGroupsResponseSchema.safeParse(listRes.json()); expect(httpParsed.success, JSON.stringify(httpParsed.error?.issues)).toBe( true, ); expect( httpParsed.data!.groups .find((g) => g.id === group.id) ?.services.some((s) => s.id === created.id), ).toBe(true); await app.close(); }); it("POST /services returns resolved ServiceView with numeric id", async () => { const app = await buildApp({ config: { ...loadConfig(), staticDir: null }, memory: true, }); const headers = await authHeaders(app); const createRes = await app.inject({ method: "POST", url: "/api/v1/services", headers, payload: { name: "Bare", slug: "bare" }, }); expect(createRes.statusCode).toBe(200); const created = createRes.json() as Record; expect(typeof created.id).toBe("number"); expect(created.name).toBe("Bare"); expect(created.enabled).toBe(true); expect(Array.isArray(created.ips)).toBe(true); expect(Array.isArray(created.domains)).toBe(true); const listRes = await app.inject({ method: "GET", url: "/api/v1/service-groups", headers, }); expect(listRes.statusCode).toBe(200); const parsed = serviceGroupsResponseSchema.safeParse(listRes.json()); expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true); expect(parsed.data!.ungrouped.some((s) => s.slug === "bare")).toBe(true); await app.close(); }); it("PATCH /services/:id/ips/toggle keeps IP in pool and removes it from A-binding", async () => { const app = await buildApp({ config: { ...loadConfig(), staticDir: null }, memory: true, }); const headers = await authHeaders(app); const cf = mockCf(); repos.createDomain(app.db, null, "example.com", "zone-1"); const group = repos.createServiceGroup( app.db, "VPN", "vpn", null, "vpn.example.com", ); const createRes = await app.inject({ method: "POST", url: "/api/v1/services", headers, payload: { name: "Panel", slug: "panel-ip-toggle", service_group_id: group.id, }, }); expect(createRes.statusCode).toBe(200); const created = createRes.json() as { id: number }; await updateConfig(app.db, cf, created.id, { ips: ["1.2.3.4", "5.6.7.8"], service_group_id: group.id, domains: [ { fqdn: "panel.example.com", target_ips: ["1.2.3.4", "5.6.7.8"], target_ip_weights: { "1.2.3.4": 1, "5.6.7.8": 1 }, target_ip_priorities: { "1.2.3.4": 1, "5.6.7.8": 1 }, lb_mode: "round_robin", health_check_enabled: false, health_check_type: "tcp", health_check_port: 443, health_check_path: null, health_check_expected_status: null, health_check_interval_sec: 30, health_check_timeout_ms: 3000, health_check_verify_tls: false, }, ], }); // HTTP toggle uses request.server.cf; disable DNS push so the test // does not call the real Cloudflare client. repos.setServiceEnabled(app.db, created.id, false); const offRes = await app.inject({ method: "PATCH", url: `/api/v1/services/${created.id}/ips/toggle`, headers, payload: { ip: "1.2.3.4", enabled: false }, }); expect(offRes.statusCode, JSON.stringify(offRes.json())).toBe(200); const offView = offRes.json() as { ips: string[]; ip_enabled: Record; }; expect(offView.ips).toEqual(expect.arrayContaining(["1.2.3.4", "5.6.7.8"])); expect(offView.ip_enabled["1.2.3.4"]).toBe(false); expect(offView.ip_enabled["5.6.7.8"]).toBe(true); const binding = repos.listBindingsByService(app.db, created.id)[0]!; expect(repos.listBindingIps(app.db, binding.id)).toEqual(["5.6.7.8"]); const onRes = await app.inject({ method: "PATCH", url: `/api/v1/services/${created.id}/ips/toggle`, headers, payload: { ip: "1.2.3.4", enabled: true }, }); expect(onRes.statusCode).toBe(200); const onView = onRes.json() as { ip_enabled: Record }; expect(onView.ip_enabled["1.2.3.4"]).toBe(true); expect(repos.listBindingIps(app.db, binding.id)).toEqual( expect.arrayContaining(["1.2.3.4", "5.6.7.8"]), ); await app.close(); }); });