Files
uptime-monitoring/test/status.spec.ts
T
2026-08-28 16:30:50 +07:00

160 lines
5.7 KiB
TypeScript

import { applyD1Migrations, env, SELF, type D1Migration } from "cloudflare:test";
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
const DAY_MS = 24 * 60 * 60 * 1000;
type PublicStatusResponse = {
overall: "operational" | "degraded" | "down";
updatedAt: number;
services: Array<{
id: number;
name: string;
status: "up" | "down" | "unknown";
lastCheckedAt: string | null;
uptime90d: number | null;
history: Array<{ day: number; uptimePct: number | null }>;
}>;
};
async function resetDatabase() {
await env.DB.batch([
env.DB.prepare("DELETE FROM checks"),
env.DB.prepare("DELETE FROM incidents"),
env.DB.prepare("DELETE FROM monitor_daily_stats"),
env.DB.prepare("DELETE FROM notification_settings"),
env.DB.prepare("DELETE FROM monitors"),
env.DB.prepare("DELETE FROM login_attempts"),
env.DB.prepare("DELETE FROM sessions"),
env.DB.prepare("DELETE FROM admin_credentials"),
]);
}
async function insertMonitor(input: {
name: string;
url?: string;
enabled?: boolean;
lastOk?: boolean | null;
lastStatusCode?: number | null;
lastLatencyMs?: number | null;
lastError?: string | null;
}) {
const now = Date.now();
const result = await env.DB.prepare(`
INSERT INTO monitors (
name, url, method, expected_status, interval_seconds, timeout_ms,
enabled, alerts_enabled, last_ok, last_status_code, last_latency_ms,
last_error, last_checked_at, created_at, updated_at
) VALUES (?, ?, 'GET', 200, 300, 10000, ?, 1, ?, ?, ?, ?, ?, ?, ?)
`)
.bind(
input.name,
input.url ?? `https://${input.name.toLowerCase()}.example.com/health`,
input.enabled === false ? 0 : 1,
input.lastOk === null || input.lastOk === undefined ? null : input.lastOk ? 1 : 0,
input.lastStatusCode ?? null,
input.lastLatencyMs ?? null,
input.lastError ?? null,
now,
now,
now,
)
.run();
return Number(result.meta.last_row_id);
}
function statusFetch(path = "/api/status") {
return SELF.fetch(`https://example.com${path}`);
}
describe("public status API", () => {
beforeAll(async () => {
const testEnv = env as Env & { TEST_MIGRATIONS: D1Migration[] };
await applyD1Migrations(testEnv.DB, testEnv.TEST_MIGRATIONS);
});
beforeEach(resetDatabase);
it("returns public status without authentication and omits sensitive monitor fields", async () => {
await insertMonitor({
name: "Public API",
lastOk: true,
lastStatusCode: 200,
lastLatencyMs: 42,
lastError: "sensitive diagnostic",
});
const response = await statusFetch();
const body = await response.json<PublicStatusResponse>();
expect(response.status).toBe(200);
expect(body.overall).toBe("operational");
expect(body.updatedAt).toEqual(expect.any(Number));
expect(body.services).toHaveLength(1);
expect(body.services[0]).toMatchObject({
name: "Public API",
status: "up",
uptime90d: null,
history: [],
});
for (const privateField of ["url", "lastError", "lastStatusCode", "lastLatencyMs", "method", "timeoutMs"]) {
expect(body.services[0]).not.toHaveProperty(privateField);
}
});
it("excludes disabled monitors and reports degraded health for a partial outage", async () => {
await insertMonitor({ name: "Healthy", lastOk: true });
await insertMonitor({ name: "Unavailable", lastOk: false });
await insertMonitor({ name: "Disabled secret", enabled: false, lastOk: false });
await insertMonitor({ name: "New service", lastOk: null });
const response = await statusFetch();
const body = await response.json<PublicStatusResponse>();
expect(response.status).toBe(200);
expect(body.overall).toBe("degraded");
expect(body.services.map((service) => service.name)).toEqual(["Healthy", "Unavailable", "New service"]);
expect(body.services.map((service) => service.status)).toEqual(["up", "down", "unknown"]);
});
it("reports a total outage when every checked service is down", async () => {
await insertMonitor({ name: "Website", lastOk: false });
await insertMonitor({ name: "API", lastOk: false });
await insertMonitor({ name: "Not checked", lastOk: null });
const body = await (await statusFetch()).json<PublicStatusResponse>();
expect(body.overall).toBe("down");
});
it("combines historical daily rollups and today's checks into 90-day uptime", async () => {
const id = await insertMonitor({ name: "Aggregated", lastOk: true });
const now = new Date();
const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
await env.DB.batch([
env.DB.prepare("INSERT INTO monitor_daily_stats (monitor_id, day, total_checks, up_checks, avg_latency_ms, min_latency_ms, max_latency_ms) VALUES (?, ?, 8, 6, 100, 50, 150)").bind(id, today - DAY_MS),
env.DB.prepare("INSERT INTO checks (monitor_id, ok, status_code, latency_ms, checked_at) VALUES (?, 1, 200, 90, ?)").bind(id, today + 1_000),
env.DB.prepare("INSERT INTO checks (monitor_id, ok, status_code, latency_ms, checked_at) VALUES (?, 1, 200, 110, ?)").bind(id, today + 2_000),
]);
const body = await (await statusFetch()).json<PublicStatusResponse>();
const service = body.services[0];
expect(service.uptime90d).toBe(80);
expect(service.history).toEqual([
{ day: today - DAY_MS, uptimePct: 75 },
{ day: today, uptimePct: 100 },
]);
});
it("keeps the administrative monitor collection protected", async () => {
const response = await SELF.fetch("https://example.com/api/monitors");
expect(response.status).toBe(401);
expect(await response.json()).toEqual({ message: "Authentication required" });
});
it("does not expose a favicon for a disabled service", async () => {
const id = await insertMonitor({ name: "Private", enabled: false });
const response = await statusFetch(`/api/status/${id}/favicon`);
expect(response.status).toBe(404);
expect(await response.json()).toEqual({ message: "Service not found" });
});
});