relay-exporter/test/smoke.test.ts
2026-03-22 16:02:18 -07:00

110 lines
3.1 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import http from "node:http";
import { setTimeout as sleep } from "node:timers/promises";
function randomPort(): number {
return 20_000 + Math.floor(Math.random() * 10_000);
}
function httpGet(
port: number,
path: string,
): Promise<{ statusCode: number; body: string; headers: http.IncomingHttpHeaders }> {
return new Promise((resolve, reject) => {
const req = http.request(
{
hostname: "127.0.0.1",
port,
path,
method: "GET",
timeout: 4_000,
},
(res) => {
const chunks: Buffer[] = [];
res.on("data", (chunk) => {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
res.on("end", () => {
resolve({
statusCode: res.statusCode ?? 0,
body: Buffer.concat(chunks).toString("utf8"),
headers: res.headers,
});
});
},
);
req.on("timeout", () => {
req.destroy(new Error(`timeout GET ${path}`));
});
req.on("error", reject);
req.end();
});
}
async function waitForStartup(child: ChildProcessWithoutNullStreams, port: number, timeoutMs: number): Promise<void> {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
if (child.exitCode !== null) {
throw new Error(`exporter exited early with code ${child.exitCode}`);
}
try {
const res = await httpGet(port, "/healthz");
if ([200, 503].includes(res.statusCode)) return;
} catch {
// server not up yet
}
await sleep(125);
}
throw new Error("timeout waiting for exporter startup");
}
test("exporter serves healthz and metrics without crashing", async (t) => {
const port = randomPort();
const child = spawn(
"pnpm",
["exec", "tsx", "src/index.ts"],
{
cwd: process.cwd(),
env: {
...process.env,
RELAYS: "wss://relay.damus.io,wss://nos.lol",
WRITE_CHECK_ENABLED: "false",
PROBE_TIMEOUT_SECONDS: "2",
PROBE_INTERVAL_SECONDS: "3",
PORT: String(port),
LOG_LEVEL: "error",
},
stdio: ["ignore", "pipe", "pipe"],
},
);
let stderr = "";
child.stderr.on("data", (d: Buffer) => {
stderr += d.toString("utf8");
});
t.after(() => {
if (child.exitCode === null) {
child.kill("SIGTERM");
}
});
await waitForStartup(child, port, 8000);
const health = await httpGet(port, "/healthz");
assert.ok([200, 503].includes(health.statusCode), `unexpected /healthz status ${health.statusCode}`);
assert.match(health.body, /"status": "(ok|degraded)"/);
const metrics = await httpGet(port, "/metrics");
assert.equal(metrics.statusCode, 200);
assert.match(metrics.body, /nostr_relay_up/);
assert.match(metrics.body, /nostr_relay_open_ok/);
assert.match(metrics.body, /nostr_relay_probe_runs_total/);
assert.match(metrics.body, /process_cpu_user_seconds_total|nodejs_eventloop_lag_seconds/);
await sleep(3500);
assert.equal(child.exitCode, null, `exporter crashed unexpectedly: ${stderr}`);
});