Files
cloudflare-domain-manager/apps/api/test/service-groups-health.test.ts
Denozordec 2c92e78b24
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 44s
CD / quality (push) Successful in 1m44s
CD / publish (push) Successful in 1m35s
feat(health-checks): enhance health check configuration and logging
- Added new health worker URL and token fields to the AppConfig interface, allowing for Cloudflare Worker integration.
- Updated health check routes to utilize the new worker configuration, enabling dynamic health checks via Cloudflare Workers.
- Introduced a health log endpoint for services, providing detailed logs of health probe results.
- Enhanced health check service logic to support both local and Cloudflare Worker providers, improving flexibility in health monitoring.
- Updated UI components to reflect changes in health check provider settings and display relevant health information.

This commit significantly improves the health check management capabilities, allowing for better integration with Cloudflare Workers and enhanced logging features.
2026-08-19 16:27:34 +07:00

238 lines
8.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { buildApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
import { createMemoryDb, repos, runMigrations } from "@cfdm/db";
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("service groups health enrichment", () => {
it("GET /service-groups returns health_status from ip_health_status", async () => {
const app = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(app);
const domain = repos.createDomain(app.db, null, "example.com", "zone-1");
const group = repos.createServiceGroup(
app.db,
"VPN",
"vpn",
null,
"vpn.example.com",
);
repos.updateServiceGroup(app.db, group.id, "VPN", "vpn", null, "vpn.example.com", {
health_check_enabled: true,
health_check_type: "tcp",
health_check_port: 443,
});
const service = repos.createService(app.db, "Panel", "panel");
repos.setServiceGroup(app.db, service.id, group.id);
repos.setServiceEnabled(app.db, service.id, true);
repos.replaceServiceIps(app.db, service.id, ["1.2.3.4"]);
const binding = repos.insertBinding(
app.db,
domain.id,
service.id,
"panel",
null,
);
repos.replaceBindingIpsWithMeta(app.db, binding.id, [
{ ip: "1.2.3.4", weight: 1, priority: 1 },
]);
repos.upsertIpHealthStatus(
app.db,
"binding",
binding.id,
"1.2.3.4",
"degraded",
120,
1,
null,
);
repos.upsertIpHealthStatus(
app.db,
"group",
group.id,
"1.2.3.4",
"up",
40,
0,
null,
);
const res = await app.inject({
method: "GET",
url: "/api/v1/service-groups",
headers,
});
expect(res.statusCode).toBe(200);
const body = res.json() as {
groups: Array<{
id: number;
health_status: string;
health_latency_ms: number | null;
services: Array<{
id: number;
health_status: string;
health_latency_ms: number | null;
ip_health: Array<{
ip: string;
status: string;
latency_ms: number | null;
}>;
}>;
}>;
};
const groupView = body.groups.find((g) => g.id === group.id);
expect(groupView).toBeDefined();
expect(groupView!.services[0]?.health_status).toBe("degraded");
expect(groupView!.services[0]?.health_latency_ms).toBe(120);
expect(groupView!.services[0]?.ip_health).toEqual([
{
ip: "1.2.3.4",
status: "degraded",
latency_ms: 120,
last_checked_at: expect.any(String),
last_error: null,
provider: "local",
colo: null,
},
]);
// group worst = degraded (from service) over up (group scope)
expect(groupView!.health_status).toBe("degraded");
await app.close();
});
it("group badge ignores disabled service health and pool-only dead IPs", async () => {
const app = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(app);
const domain = repos.createDomain(app.db, null, "rkns.top", "zone-tg");
const group = repos.createServiceGroup(
app.db,
"TG Proxy",
"custom",
null,
"gt.rkns.top",
);
repos.updateServiceGroup(app.db, group.id, "TG Proxy", "custom", null, "gt.rkns.top", {
health_check_enabled: true,
health_check_type: "tcp",
health_check_port: 443,
});
const live = repos.createService(app.db, "Live", "live");
repos.setServiceGroup(app.db, live.id, group.id);
repos.setServiceEnabled(app.db, live.id, true);
repos.replaceServiceIps(app.db, live.id, ["10.0.0.1", "10.0.0.99"]);
const liveBinding = repos.insertBinding(app.db, domain.id, live.id, "gt", null);
repos.replaceBindingIpsWithMeta(app.db, liveBinding.id, [
{ ip: "10.0.0.1", weight: 1, priority: 1 },
]);
const dead = repos.createService(app.db, "Dead", "dead");
repos.setServiceGroup(app.db, dead.id, group.id);
repos.setServiceEnabled(app.db, dead.id, false);
const deadBinding = repos.insertBinding(app.db, domain.id, dead.id, "old", null);
repos.replaceBindingIpsWithMeta(app.db, deadBinding.id, [
{ ip: "10.0.0.2", weight: 1, priority: 1 },
]);
// Live binding + group FQDN are up; pool-only IP and disabled service are down.
repos.upsertIpHealthStatus(app.db, "binding", liveBinding.id, "10.0.0.1", "up", 20, 0, null);
repos.upsertIpHealthStatus(app.db, "group", group.id, "10.0.0.1", "up", 20, 0, null);
repos.upsertIpHealthStatus(app.db, "group", group.id, "10.0.0.99", "down", null, 5, "timeout");
repos.upsertIpHealthStatus(app.db, "binding", deadBinding.id, "10.0.0.2", "down", null, 5, "timeout");
const res = await app.inject({
method: "GET",
url: "/api/v1/service-groups",
headers,
});
expect(res.statusCode).toBe(200);
const body = res.json() as {
groups: Array<{ id: number; health_status: string }>;
};
const groupView = body.groups.find((g) => g.id === group.id);
expect(groupView?.health_status).toBe("up");
await app.close();
});
});
describe("group health check targets", () => {
it("probes binding IPs for group scope, not unused pool IPs", () => {
const { db, sqlite } = createMemoryDb();
runMigrations(sqlite);
const domain = repos.createDomain(db, null, "rkns.top", "zone-1");
const group = repos.createServiceGroup(db, "TG Proxy", "custom", null, "gt.rkns.top");
repos.updateServiceGroup(db, group.id, "TG Proxy", "custom", null, "gt.rkns.top", {
health_check_enabled: true,
health_check_type: "tcp",
health_check_port: 443,
});
const service = repos.createService(db, "Svc", "svc");
repos.setServiceGroup(db, service.id, group.id);
repos.setServiceEnabled(db, service.id, true);
repos.replaceServiceIps(db, service.id, ["10.0.0.1", "10.0.0.99"]);
const binding = repos.insertBinding(db, domain.id, service.id, "gt", null);
repos.replaceBindingIpsWithMeta(db, binding.id, [
{ ip: "10.0.0.1", weight: 1, priority: 1 },
]);
const targets = repos.listHealthCheckTargets(db);
const groupScope = targets.filter((t) => t.scope === "group" && t.ref_id === group.id);
expect(groupScope.map((t) => t.ip).sort()).toEqual(["10.0.0.1"]);
expect(groupScope.every((t) => t.hostname === "gt.rkns.top")).toBe(true);
});
it("pruneStaleIpHealthStatus drops pool-only and orphan group rows", () => {
const { db, sqlite } = createMemoryDb();
runMigrations(sqlite);
const domain = repos.createDomain(db, null, "rkns.top", "zone-1");
const group = repos.createServiceGroup(db, "TG Proxy", "custom", null, "gt.rkns.top");
repos.updateServiceGroup(db, group.id, "TG Proxy", "custom", null, "gt.rkns.top", {
health_check_enabled: true,
health_check_type: "tcp",
health_check_port: 443,
});
const service = repos.createService(db, "Svc", "svc");
repos.setServiceGroup(db, service.id, group.id);
repos.setServiceEnabled(db, service.id, true);
repos.replaceServiceIps(db, service.id, ["10.0.0.1", "10.0.0.99"]);
const binding = repos.insertBinding(db, domain.id, service.id, "gt", null);
repos.replaceBindingIpsWithMeta(db, binding.id, [
{ ip: "10.0.0.1", weight: 1, priority: 1 },
]);
repos.upsertIpHealthStatus(db, "group", group.id, "10.0.0.1", "up", 10, 0, null);
repos.upsertIpHealthStatus(db, "group", group.id, "10.0.0.99", "down", null, 3, "dead pool");
repos.upsertIpHealthStatus(db, "group", 999, "1.1.1.1", "down", null, 3, "orphan ref");
const targets = repos.listHealthCheckTargets(db);
const deleted = repos.pruneStaleIpHealthStatus(db, targets);
expect(deleted).toBeGreaterThanOrEqual(2);
const rows = repos.listIpHealthStatus(db, "group", group.id);
expect(rows.map((r) => r.ip)).toEqual(["10.0.0.1"]);
expect(repos.listIpHealthStatus(db, "group", 999)).toEqual([]);
});
});