Files
cloudflare-domain-manager/apps/api/test/health-worker.test.ts
T
DenozordecandCursor 4c4908558b
quality / changes (push) Successful in 9s
quality / commitlint (push) Skipped
quality / docker-check (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / web (push) Successful in 1m4s
quality / api (push) Successful in 54s
CD / quality (push) Successful in 2m17s
CD / publish (push) Successful in 2m21s
feat(health): деплоить probe-Worker из CFDM и опрашивать цели с edge
Worker сам ходит на origin по Cron Trigger; CFDM кладёт цели в KV и забирает результаты без POST /probe.

Co-authored-by: Cursor <[email protected]>
2026-08-19 17:22:07 +07:00

246 lines
6.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { buildApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
import { repos, type Db } from "@cfdm/db";
import * as healthCheckService from "../src/services/health-check-service.js";
import type { HealthMailbox } from "../src/services/health/mailbox.js";
import { originProbeKey } from "../src/services/health/mailbox.js";
import type { HealthCheckTarget } from "@cfdm/shared";
async function authHeaders(app: Awaited<ReturnType<typeof buildApp>>) {
const res = await app.inject({
method: "POST",
url: "/api/v1/auth/login",
payload: { username: "admin", password: "admin" },
});
expect(res.statusCode).toBe(200);
const { token } = res.json() as { token: string };
return { authorization: `Bearer ${token}` };
}
async function seedBinding(
db: Db,
opts: { provider: "local" | "cloudflare"; ip: string },
) {
const domain = repos.createDomain(db, null, "example.com", "zone-1");
const service = repos.createService(db, "Panel", "panel");
repos.setServiceEnabled(db, service.id, true);
repos.replaceServiceIps(db, service.id, [opts.ip]);
const binding = repos.insertBinding(db, domain.id, service.id, "panel", null);
repos.replaceBindingIpsWithMeta(db, binding.id, [
{ ip: opts.ip, weight: 1, priority: 1 },
]);
repos.updateBindingLbConfig(db, binding.id, {
health_check_enabled: true,
health_check_type: "tcp",
health_check_port: 1,
health_check_timeout_ms: 400,
health_check_provider: opts.provider,
});
return { service, binding, domain };
}
const thresholds = {
degradedFailures: 1,
downFailures: 2,
latencyWarnMs: 1000,
successRecoveries: 2,
};
function memoryMailbox(opts?: {
resultsOk?: boolean;
colo?: string;
probedAt?: string;
}): HealthMailbox {
let targets: unknown = null;
return {
async getTargets() {
return targets as never;
},
async putTargets(doc) {
targets = doc;
},
async getResults() {
if (!opts) return null;
const dummy: HealthCheckTarget = {
scope: "binding",
ref_id: 1,
ip: "203.0.113.10",
hostname: "panel.example.com",
type: "tcp",
port: 1,
path: null,
expected_status: null,
timeout_ms: 400,
verify_tls: false,
provider: "cloudflare",
};
return {
probedAt: opts.probedAt ?? new Date().toISOString(),
colo: opts.colo ?? "AMS",
items: [
{
key: originProbeKey(dummy),
ok: opts.resultsOk !== false,
latencyMs: 42,
error: opts.resultsOk === false ? "down" : null,
},
],
};
},
};
}
describe("health-check XOR worker mailbox", () => {
it("lists only local providers when no cloudflare bindings", async () => {
const app = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
await seedBinding(app.db, {
provider: "local",
ip: "10.0.0.1",
});
const targets = repos.listHealthCheckTargets(app.db);
expect(targets.length).toBeGreaterThan(0);
expect(targets.every((t) => t.provider === "local")).toBe(true);
expect(targets.some((t) => t.provider === "cloudflare")).toBe(false);
await app.close();
});
it("cloudflare without mailbox does not fall back to local", async () => {
const app = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const { binding } = await seedBinding(app.db, {
provider: "cloudflare",
ip: "127.0.0.1",
});
await healthCheckService.runAllChecks(app.db, {
thresholds,
probeGapMs: 0,
mailbox: null,
});
const row = repos.getIpHealthStatusRow(
app.db,
"binding",
binding.id,
"127.0.0.1",
);
expect(row?.last_error).toMatch(/Worker не настроен/i);
expect(row?.provider).toBe("cloudflare");
await app.close();
});
it("KV results write colo and last_checked_at without HTTP /probe", async () => {
const app = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(app);
const { service, binding } = await seedBinding(app.db, {
provider: "cloudflare",
ip: "203.0.113.10",
});
const targets = repos.listHealthCheckTargets(app.db);
const cfTarget = targets.find((t) => t.ip === "203.0.113.10")!;
const mailbox: HealthMailbox = {
async getTargets() {
return null;
},
async putTargets() {
/* fingerprint sync */
},
async getResults() {
return {
probedAt: new Date().toISOString(),
colo: "AMS",
items: [
{
key: originProbeKey(cfTarget),
ok: true,
latencyMs: 42,
error: null,
},
],
};
},
};
await healthCheckService.runAllChecks(app.db, {
thresholds,
probeGapMs: 0,
mailbox,
});
const row = repos.getIpHealthStatusRow(
app.db,
"binding",
binding.id,
"203.0.113.10",
);
expect(row?.status).toBe("up");
expect(row?.colo).toBe("AMS");
expect(row?.last_checked_at).toBeTruthy();
expect(row?.provider).toBe("cloudflare");
const res = await app.inject({
method: "GET",
url: `/api/v1/services/${service.id}`,
headers,
});
expect(res.statusCode).toBe(200);
const body = res.json() as {
ip_health: Array<{
ip: string;
colo: string | null;
last_checked_at: string | null;
provider: string;
}>;
};
const ipRow = body.ip_health.find((item) => item.ip === "203.0.113.10");
expect(ipRow?.colo).toBe("AMS");
expect(ipRow?.provider).toBe("cloudflare");
const logRes = await app.inject({
method: "GET",
url: `/api/v1/services/${service.id}/health-log`,
headers,
});
expect(logRes.statusCode).toBe(200);
const logBody = logRes.json() as { items: Array<{ colo: string | null }> };
expect(logBody.items[0]?.colo).toBe("AMS");
await app.close();
});
it("stale KV results are recorded, not local probe", async () => {
const app = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const { binding } = await seedBinding(app.db, {
provider: "cloudflare",
ip: "203.0.113.20",
});
await healthCheckService.runAllChecks(app.db, {
thresholds,
probeGapMs: 0,
mailbox: memoryMailbox({
resultsOk: true,
colo: "SIN",
probedAt: new Date(Date.now() - 60 * 60_000).toISOString(),
}),
staleAfterMs: 60_000,
});
const row = repos.getIpHealthStatusRow(
app.db,
"binding",
binding.id,
"203.0.113.20",
);
expect(row?.last_error).toMatch(/устарели|KV/i);
expect(row?.provider).toBe("cloudflare");
await app.close();
});
});