CD / update-wiki (push) Successful in 8s
quality / commitlint (push) Skipped
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
quality / web (push) Successful in 54s
quality / api (push) Successful in 46s
CD / quality (push) Successful in 1m49s
CD / publish (push) Successful in 1m40s
Несколько источников проб сразу и правило агрегации на сервисе вместо XOR Local/Cloudflare. Co-authored-by: Cursor <[email protected]>
274 lines
8.4 KiB
TypeScript
274 lines
8.4 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 {
|
|
buildMeasurementBody,
|
|
summarizeMeasurement,
|
|
} from "../src/lib/globalping-client.js";
|
|
import { aggregateHealthOk } from "@cfdm/shared";
|
|
import type { HealthCheckTarget } from "@cfdm/shared";
|
|
|
|
const thresholds = {
|
|
degradedFailures: 1,
|
|
downFailures: 2,
|
|
latencyWarnMs: 1000,
|
|
successRecoveries: 2,
|
|
};
|
|
|
|
function tcpTarget(overrides?: Partial<HealthCheckTarget>): HealthCheckTarget {
|
|
return {
|
|
scope: "binding",
|
|
ref_id: 1,
|
|
ip: "203.0.113.10",
|
|
hostname: "panel.example.com",
|
|
type: "tcp",
|
|
port: 443,
|
|
path: null,
|
|
expected_status: null,
|
|
timeout_ms: 400,
|
|
verify_tls: false,
|
|
provider: "globalping",
|
|
providers: ["globalping"],
|
|
aggregate: "majority",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function seedBinding(
|
|
db: Db,
|
|
opts: {
|
|
ip: string;
|
|
providers: Array<"local" | "cloudflare" | "globalping">;
|
|
aggregate?: "any" | "all" | "majority";
|
|
port?: number;
|
|
},
|
|
) {
|
|
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: opts.port ?? 1,
|
|
health_check_timeout_ms: 400,
|
|
health_check_providers: opts.providers,
|
|
health_check_aggregate: opts.aggregate ?? "majority",
|
|
});
|
|
return { service, binding, domain };
|
|
}
|
|
|
|
function mockFetch(handler: (url: string, init?: RequestInit) => Response): typeof fetch {
|
|
return (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
return handler(url, init);
|
|
}) as typeof fetch;
|
|
}
|
|
|
|
describe("aggregateHealthOk", () => {
|
|
it("any / all / majority", () => {
|
|
expect(aggregateHealthOk([true, false], "any")).toBe(false);
|
|
expect(aggregateHealthOk([true, false], "all")).toBe(true);
|
|
expect(aggregateHealthOk([true, false], "majority")).toBe(true);
|
|
expect(aggregateHealthOk([false, false], "majority")).toBe(false);
|
|
expect(aggregateHealthOk([true, false, false], "majority")).toBe(false);
|
|
expect(aggregateHealthOk([true, true, false], "majority")).toBe(true);
|
|
expect(aggregateHealthOk([true], "majority")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("globalping mapping", () => {
|
|
it("maps CFDM TCP to ping+TCP and HTTP to http+host", () => {
|
|
const tcp = buildMeasurementBody(tcpTarget(), { limit: 3, locations: "World" });
|
|
expect(tcp.type).toBe("ping");
|
|
expect(tcp.measurementOptions.protocol).toBe("TCP");
|
|
expect(tcp.measurementOptions.port).toBe(443);
|
|
expect(tcp.inProgressUpdates).toBe(false);
|
|
|
|
const http = buildMeasurementBody(
|
|
tcpTarget({ type: "http", port: 443, path: "/health", expected_status: 200 }),
|
|
{ limit: 2, locations: "EU,US" },
|
|
);
|
|
expect(http.type).toBe("http");
|
|
expect(http.locations).toEqual([{ magic: "EU" }, { magic: "US" }]);
|
|
expect(http.measurementOptions.request).toMatchObject({
|
|
host: "panel.example.com",
|
|
path: "/health",
|
|
method: "GET",
|
|
});
|
|
});
|
|
|
|
it("summarizes HTTP majority and TCP packet loss", () => {
|
|
const httpOk = summarizeMeasurement(
|
|
tcpTarget({ type: "http", expected_status: 200 }),
|
|
{
|
|
status: "finished",
|
|
results: [
|
|
{ probe: { city: "Frankfurt", country: "DE" }, result: { status: "finished", statusCode: 200, timings: { total: 40 } } },
|
|
{ probe: { city: "London", country: "GB" }, result: { status: "finished", statusCode: 200, timings: { total: 50 } } },
|
|
{ probe: { city: "Paris", country: "FR" }, result: { status: "finished", statusCode: 500, timings: { total: 20 } } },
|
|
],
|
|
},
|
|
);
|
|
expect(httpOk.ok).toBe(true);
|
|
expect(httpOk.colo).toBe("Frankfurt, DE");
|
|
|
|
const tcpFail = summarizeMeasurement(tcpTarget(), {
|
|
status: "finished",
|
|
results: [
|
|
{ result: { status: "finished", stats: { avg: 12, loss: 100 } } },
|
|
{ result: { status: "finished", stats: { avg: 11, loss: 100 } } },
|
|
],
|
|
});
|
|
expect(tcpFail.ok).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("globalping engine", () => {
|
|
it("POST 202 + GET finished writes colo from probe city", async () => {
|
|
const app = await buildApp({
|
|
config: { ...loadConfig(), staticDir: null },
|
|
memory: true,
|
|
});
|
|
const { binding } = await seedBinding(app.db, {
|
|
ip: "203.0.113.40",
|
|
providers: ["globalping"],
|
|
port: 443,
|
|
});
|
|
const fetchImpl = mockFetch((url) => {
|
|
if (url.endsWith("/v1/measurements")) {
|
|
return new Response(JSON.stringify({ id: "meas-1" }), { status: 202 });
|
|
}
|
|
return new Response(
|
|
JSON.stringify({
|
|
id: "meas-1",
|
|
status: "finished",
|
|
results: [
|
|
{
|
|
probe: { city: "Amsterdam", country: "NL" },
|
|
result: { status: "finished", stats: { avg: 18, loss: 0 } },
|
|
},
|
|
{
|
|
probe: { city: "Frankfurt", country: "DE" },
|
|
result: { status: "finished", stats: { avg: 22, loss: 0 } },
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200 },
|
|
);
|
|
});
|
|
await healthCheckService.runAllChecks(app.db, {
|
|
thresholds,
|
|
probeGapMs: 0,
|
|
globalping: {
|
|
token: "gp_test",
|
|
locations: "World",
|
|
limit: 2,
|
|
pollIntervalMs: 0,
|
|
fetchImpl,
|
|
},
|
|
});
|
|
const row = repos.getIpHealthStatusRow(
|
|
app.db,
|
|
"binding",
|
|
binding.id,
|
|
"203.0.113.40",
|
|
);
|
|
expect(row?.status).toBe("up");
|
|
expect(row?.provider).toBe("globalping");
|
|
expect(row?.colo).toMatch(/Amsterdam/);
|
|
await app.close();
|
|
});
|
|
|
|
it("429 fails the source and does not fall back to local", async () => {
|
|
const app = await buildApp({
|
|
config: { ...loadConfig(), staticDir: null },
|
|
memory: true,
|
|
});
|
|
const { binding } = await seedBinding(app.db, {
|
|
ip: "127.0.0.1",
|
|
providers: ["globalping"],
|
|
port: 1,
|
|
});
|
|
const fetchImpl = mockFetch(() => new Response("rate limited", { status: 429 }));
|
|
await healthCheckService.runAllChecks(app.db, {
|
|
thresholds,
|
|
probeGapMs: 0,
|
|
globalping: {
|
|
token: "gp_test",
|
|
locations: "World",
|
|
limit: 1,
|
|
pollIntervalMs: 0,
|
|
fetchImpl,
|
|
},
|
|
});
|
|
const row = repos.getIpHealthStatusRow(
|
|
app.db,
|
|
"binding",
|
|
binding.id,
|
|
"127.0.0.1",
|
|
);
|
|
expect(row?.last_error).toMatch(/429/i);
|
|
expect(row?.provider).toBe("globalping");
|
|
await app.close();
|
|
});
|
|
|
|
it("local+globalping all keeps IP up if Globalping is ok", async () => {
|
|
const app = await buildApp({
|
|
config: { ...loadConfig(), staticDir: null },
|
|
memory: true,
|
|
});
|
|
const { binding, service } = await seedBinding(app.db, {
|
|
ip: "127.0.0.1",
|
|
providers: ["local", "globalping"],
|
|
aggregate: "all",
|
|
port: 1,
|
|
});
|
|
const fetchImpl = mockFetch((url) => {
|
|
if (url.endsWith("/v1/measurements")) {
|
|
return new Response(JSON.stringify({ id: "meas-2" }), { status: 202 });
|
|
}
|
|
return new Response(
|
|
JSON.stringify({
|
|
status: "finished",
|
|
results: [
|
|
{ probe: { city: "Vienna", country: "AT" }, result: { status: "finished", stats: { avg: 9, loss: 0 } } },
|
|
],
|
|
}),
|
|
{ status: 200 },
|
|
);
|
|
});
|
|
await healthCheckService.runAllChecks(app.db, {
|
|
thresholds,
|
|
probeGapMs: 0,
|
|
globalping: {
|
|
token: "gp_test",
|
|
locations: "World",
|
|
limit: 1,
|
|
pollIntervalMs: 0,
|
|
fetchImpl,
|
|
},
|
|
});
|
|
const row = repos.getIpHealthStatusRow(
|
|
app.db,
|
|
"binding",
|
|
binding.id,
|
|
"127.0.0.1",
|
|
);
|
|
expect(row?.status).toBe("up");
|
|
expect(row?.provider).toBe("aggregate");
|
|
const logs = repos.listHealthProbeLogForService(app.db, service.id);
|
|
expect(logs.map((row) => row.provider).sort()).toEqual([
|
|
"globalping",
|
|
"local",
|
|
]);
|
|
await app.close();
|
|
});
|
|
});
|