Skip to content
144 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { isbot } from "isbot";
5
6import { components } from "./components.ts";
7import { BROWSER_USER_AGENT, USER_AGENT, coloOf, combine, confirmSlow, judgeStorage, probe, runCheck, step } from "./probe.ts";
8
9const answer = (status: number) => async () => new Response("x", { status });
10
11test("page loads say they are a browser, so the site streams them; the name stays on the end", async () => {
12 // The isbot apps/web's entry.server.tsx uses: a match waits for the full render.
13 assert.equal(isbot(USER_AGENT), true, "the plain user agent reads as a crawler");
14 assert.equal(isbot(BROWSER_USER_AGENT), false);
15 assert.match(BROWSER_USER_AGENT, /g1t-status\/1\.0/);
16 const seen: string[] = [];
17 const fetcher = async (_url: string, init: RequestInit) => {
18 seen.push(new Headers(init.headers).get("user-agent") ?? "");
19 return new Response("x");
20 };
21 await step(fetcher, { url: "https://a/", browser: true });
22 await step(fetcher, { url: "https://a/" });
23 assert.deepEqual(seen, [BROWSER_USER_AGENT, USER_AGENT]);
24 // The site's pages load as a browser would; git, the API and the rest as before.
25 const parts = components({ SITE_URL: "https://g1t.sh", API_URL: "https://api.g1t.sh", PROBE_REPO: "flagon-io/g1t", DOCS_URL: "https://docs.g1t.sh" });
26 const browsing = parts.flatMap((p) => (p.check.kind === "http" ? p.check.steps.filter((s) => s.browser).map((s) => s.url) : []));
27 assert.deepEqual(browsing, ["https://g1t.sh/login", "https://g1t.sh/flagon-io/g1t", "https://g1t.sh/explore"]);
28});
29
30test("the data centre that answered is read from cf-ray", async () => {
31 assert.equal(coloOf("8c1f2e3d4a5b6c7d-IAD"), "IAD");
32 assert.equal(coloOf("8c1f2e3d4a5b6c7d-ams"), "AMS");
33 assert.equal(coloOf(null), null);
34 assert.equal(coloOf("nonsense"), null);
35 const ray = async () => new Response("x", { headers: { "cf-ray": "8c1f2e3d4a5b6c7d-SJC" } });
36 assert.equal((await step(ray, { url: "https://a/" })).colo, "SJC");
37 assert.equal((await step(answer(200), { url: "https://a/" })).colo, undefined);
38 assert.equal(combine([{ ok: true, ms: 1 }, { ok: true, ms: 2, colo: "FRA" }]).colo, "FRA");
39});
40
41test("a slow answer is asked again at once: one slow answer is not slow, two are", async () => {
42 // Fast the second time: counts as the fast one, keeping the first time.
43 assert.deepEqual(confirmSlow({ ok: true, ms: 2400, colo: "IAD" }, { ok: true, ms: 300, colo: "IAD" }), { ok: true, ms: 300, colo: "IAD", first_ms: 2400 });
44 // Slow twice: slow, at the faster of the two.
45 assert.deepEqual(confirmSlow({ ok: true, ms: 2400 }, { ok: true, ms: 1900 }), { ok: true, ms: 1900, first_ms: 2400 });
46 // A second try that failed does not make it worse.
47 assert.deepEqual(confirmSlow({ ok: true, ms: 2400 }, { ok: false, ms: 5000, error: "timed out" }), { ok: true, ms: 2400, first_ms: 2400 });
48
49 let calls = 0;
50 const times = [1200, 200];
51 let clock = 0;
52 const timedFetch = async () => {
53 clock += times[calls++] ?? 0;
54 return new Response("x");
55 };
56 const check = { kind: "http" as const, steps: [{ url: "https://a/" }] };
57 // A fake clock through the fetch: probe() asks again only when the first is slow.
58 const realNow = Date.now;
59 Date.now = () => clock;
60 try {
61 const confirmed = await probe(check, { fetch: timedFetch, billing: null }, 800);
62 assert.equal(calls, 2);
63 assert.deepEqual(confirmed, { ok: true, ms: 200, first_ms: 1200 });
64 calls = 0;
65 clock = 0;
66 times.splice(0, 2, 300, 300);
67 assert.deepEqual(await probe(check, { fetch: timedFetch, billing: null }, 800), { ok: true, ms: 300 });
68 assert.equal(calls, 1, "a check in time is not asked twice");
69 } finally {
70 Date.now = realNow;
71 }
72 // Failures are not asked again: detection's N of M decides about them (detect.ts).
73 calls = 0;
74 const failing = async () => {
75 calls += 1;
76 return new Response("x", { status: 502 });
77 };
78 assert.equal((await probe(check, { fetch: failing, billing: null }, 800))!.ok, false);
79 assert.equal(calls, 1);
80});
81
82test("a step works on 2xx, or on exactly the status it expects", async () => {
83 assert.equal((await step(answer(200), { url: "https://a/" })).ok, true);
84 const refused = await step(answer(502), { url: "https://a/" });
85 assert.deepEqual([refused.ok, refused.error], [false, "HTTP 502"]);
86 assert.equal((await step(answer(401), { url: "https://a/", expect: 401 })).ok, true);
87 assert.equal((await step(answer(200), { url: "https://a/", expect: 401 })).ok, false);
88});
89
90test("a step that cannot connect, or takes too long, fails with why", async () => {
91 const broken = await step(async () => Promise.reject(new TypeError("boom")), { url: "https://a/" });
92 assert.equal(broken.error, "could not connect");
93 const slow = await step(
94 (_url, init) => new Promise((_, reject) => init.signal?.addEventListener("abort", () => reject(new Error("aborted")))),
95 { url: "https://a/" },
96 20,
97 );
98 assert.equal(slow.error, "timed out");
99});
100
101test("a check's steps: all must work, and it takes as long as the slowest", () => {
102 assert.deepEqual(combine([{ ok: true, ms: 10 }, { ok: true, ms: 30 }]), { ok: true, ms: 30 });
103 assert.deepEqual(combine([{ ok: true, ms: 10 }, { ok: false, ms: 5, error: "HTTP 500" }]), { ok: false, ms: 10, error: "HTTP 500" });
104});
105
106test("runCheck: billing without a binding, and a part with no check, are null", async () => {
107 const probers = { fetch: answer(200), billing: null };
108 assert.equal(await runCheck({ kind: "billing" }, probers), null);
109 assert.equal(await runCheck({ kind: "none" }, probers), null);
110 assert.equal((await runCheck({ kind: "billing" }, { ...probers, billing: async () => ({ version: 1 }) }))!.ok, true);
111 assert.equal((await runCheck({ kind: "billing" }, { ...probers, billing: async () => null }))!.error, "no price book");
112 const mixed = await runCheck({ kind: "http", steps: [{ url: "https://a/" }, { url: "https://b/", expect: 401 }] }, probers);
113 assert.equal(mixed!.ok, false);
114});
115
116test("git storage is judged by how its calls went, and quiet is up", async () => {
117 const row = (calls: number, errors: number, rate_limited: number, rejected: number, ms_total: number) => ({
118 store: "g1t",
119 calls,
120 errors,
121 rate_limited,
122 rejected,
123 ms_total,
124 });
125 const report = (...stores: ReturnType<typeof row>[]) => ({ minutes: 5, stores });
126 assert.deepEqual(judgeStorage(report()), { ok: true, ms: 0 });
127 assert.deepEqual(judgeStorage(report(row(100, 1, 0, 0, 30_000))), { ok: true, ms: 300 });
128 // A quarter or more failing, at least five: down.
129 const down = judgeStorage(report(row(10, 3, 0, 0, 1_000), row(10, 2, 0, 0, 1_000)));
130 assert.deepEqual([down.ok, down.error], [false, "25% of calls failed"]);
131 // Too few to tell.
132 assert.equal(judgeStorage(report(row(4, 4, 0, 0, 400))).ok, true);
133 // Refused by an open breaker: down.
134 assert.equal(judgeStorage(report(row(0, 0, 0, 3, 0))).ok, false);
135 // Rate limited: degraded, not down.
136 const limited = judgeStorage(report(row(100, 2, 2, 0, 10_000)));
137 assert.deepEqual([limited.ok, limited.degraded], [true, "Rate limited 2 times in 5 minutes"]);
138 // Served from the fallback store: degraded, whatever its own calls did.
139 const onFallback = judgeStorage(report({ ...row(20, 0, 0, 0, 200), store: "g1t@fallback" }));
140 assert.deepEqual([onFallback.ok, onFallback.degraded], [true, "Served from the backup store: reads work, pushes and merges wait"]);
141 const viaCheck = await runCheck({ kind: "storage" }, { fetch: answer(200), billing: null, storage: async () => report(row(1, 0, 0, 0, 5)) });
142 assert.deepEqual(viaCheck, { ok: true, ms: 5 });
143 assert.equal(await runCheck({ kind: "storage" }, { fetch: answer(200), billing: null }), null);
144});