Files
cloudflare-domain-manager/apps/api/test/health-check.test.ts
T
Denozordec a58d91ff5e
Build and Push CFDM Docker Image / build-and-push (push) Successful in 1m51s
Build and Push CFDM Docker Image / create-release (push) Skipped
Build and Push CFDM Docker Image / update-wiki (push) Successful in 5s
feat(health-check): enhance HTTP probe functionality with URL helpers
Added new utility functions `hostForUrl` and `buildHttpProbeUrl` to improve URL construction for health checks. Updated `httpProbe` to utilize these functions, ensuring proper handling of FQDN and IP addresses. Enhanced tests to validate the new URL building logic and ensure correct behavior with different hostnames and IPs.
2026-07-20 03:41:55 +07:00

186 lines
5.5 KiB
TypeScript

import { describe, expect, it, beforeAll, afterAll } from "vitest";
import { createServer, type Server } from "node:net";
import { createServer as createHttpServer, type Server as HttpServer } from "node:http";
import * as healthCheckService from "../src/services/health-check-service.js";
import type { HealthCheckTarget } from "@cfdm/shared";
function startTcpServer(): Promise<{ server: Server; port: number }> {
return new Promise((resolve) => {
const server = createServer();
server.listen(0, "127.0.0.1", () => {
const address = server.address();
const port =
typeof address === "object" && address ? address.port : 0;
resolve({ server, port });
});
});
}
describe("health-check URL helpers", () => {
it("buildHttpProbeUrl uses FQDN in URL (IP pinned via DNS interceptor)", () => {
expect(
healthCheckService.buildHttpProbeUrl("gt.rkns.top", 443, "/", true),
).toBe("https://gt.rkns.top/");
expect(
healthCheckService.buildHttpProbeUrl("gt.rkns.top", 8080, "/health", false),
).toBe("http://gt.rkns.top:8080/health");
expect(
healthCheckService.buildHttpProbeUrl("2001:db8::1", 443, "/", true),
).toBe("https://[2001:db8::1]/");
});
});
describe("health-check probeTarget", () => {
let server: Server;
let port: number;
beforeAll(async () => {
const started = await startTcpServer();
server = started.server;
port = started.port;
});
afterAll(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
});
it("tcp probe succeeds for open port", async () => {
const target: HealthCheckTarget = {
scope: "binding",
ref_id: 1,
ip: "127.0.0.1",
hostname: "test.local",
type: "tcp",
port,
path: null,
expected_status: null,
timeout_ms: 1000,
};
const result = await healthCheckService.probeTarget(target);
expect(result.ok).toBe(true);
expect(result.error).toBeNull();
expect(result.latencyMs).toBeGreaterThanOrEqual(0);
});
it("tcp probe fails for closed port", async () => {
const target: HealthCheckTarget = {
scope: "binding",
ref_id: 1,
ip: "127.0.0.1",
hostname: "test.local",
type: "tcp",
port: 1,
path: null,
expected_status: null,
timeout_ms: 500,
};
const result = await healthCheckService.probeTarget(target);
expect(result.ok).toBe(false);
expect(result.error).not.toBeNull();
});
it("http probe hits IP with Host=hostname (same IP, different FQDN)", async () => {
let seenHost: string | undefined;
const httpServer: HttpServer = createHttpServer((req, res) => {
seenHost = req.headers.host;
res.writeHead(200);
res.end("ok");
});
const httpPort = await new Promise<number>((resolve) => {
httpServer.listen(0, "127.0.0.1", () => {
const address = httpServer.address();
resolve(typeof address === "object" && address ? address.port : 0);
});
});
try {
const groupTarget: HealthCheckTarget = {
scope: "group",
ref_id: 1,
ip: "127.0.0.1",
hostname: "gt.rkns.top",
type: "http",
port: httpPort,
path: "/",
expected_status: 200,
timeout_ms: 1000,
};
const bindingTarget: HealthCheckTarget = {
...groupTarget,
scope: "binding",
hostname: "rutg.rkns.top",
};
const groupResult = await healthCheckService.probeTarget(groupTarget);
expect(groupResult.ok).toBe(true);
expect(seenHost?.startsWith("gt.rkns.top")).toBe(true);
const bindingResult = await healthCheckService.probeTarget(bindingTarget);
expect(bindingResult.ok).toBe(true);
expect(seenHost?.startsWith("rutg.rkns.top")).toBe(true);
// Same loopback IP → latencies in the same ballpark (not ~1s DNS skew)
expect(Math.abs(groupResult.latencyMs - bindingResult.latencyMs)).toBeLessThan(200);
} finally {
await new Promise<void>((resolve) => httpServer.close(() => resolve()));
}
});
});
describe("health-check state derivation via runAllChecks", () => {
it("marks ip down after threshold failures and up after recovery", async () => {
const { createMemoryDb, repos, runMigrations } = await import("@cfdm/db");
const { db, sqlite } = createMemoryDb();
runMigrations(sqlite);
const domain = repos.createDomain(db, null, "example.com", "zone-id");
const service = repos.createService(db, "Svc", "svc");
const binding = repos.insertBinding(
db,
domain.id,
service.id,
"@",
null,
);
repos.updateBindingLbConfig(db, binding.id, {
health_check_enabled: true,
health_check_type: "tcp",
health_check_port: 1,
health_check_timeout_ms: 200,
});
repos.replaceBindingIpsWithMeta(db, binding.id, [
{ ip: "127.0.0.1", weight: 1, priority: 1 },
]);
await healthCheckService.runAllChecks(db, {
thresholds: {
degradedFailures: 1,
downFailures: 2,
latencyWarnMs: 1000,
},
});
let status = repos.getIpHealthStatusRow(
db,
"binding",
binding.id,
"127.0.0.1",
);
expect(status?.status).toBe("degraded");
await healthCheckService.runAllChecks(db, {
thresholds: {
degradedFailures: 1,
downFailures: 2,
latencyWarnMs: 1000,
},
});
status = repos.getIpHealthStatusRow(
db,
"binding",
binding.id,
"127.0.0.1",
);
expect(status?.status).toBe("down");
});
});