Files
cloudflare-domain-manager/apps/api/test/services-create-list.test.ts
T
Denozordec d63c86065c
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 8s
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
quality / web (push) Successful in 51s
quality / api (push) Successful in 43s
CD / quality (push) Successful in 1m43s
CD / publish (push) Successful in 1m38s
feat(health-checks): implement health check IP toggling and configuration updates
- Added functionality to toggle individual IPs for services, allowing for dynamic management of IP health status.
- Enhanced the health check configuration in the UI, enabling users to set parameters directly from the settings page.
- Updated service views to include IP health tracking, improving visibility into the status of each IP associated with a service.
- Refactored relevant components to support the new IP toggling feature, ensuring a seamless user experience.

This commit significantly enhances the health management capabilities of services, providing users with more control over IP configurations and health monitoring.
2026-08-19 15:33:47 +07:00

259 lines
7.9 KiB
TypeScript

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<ReturnType<typeof buildApp>>) {
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<string, unknown>;
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<string, boolean>;
};
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<string, boolean> };
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();
});
});