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>) { 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(); }); });