index.test.ts

 1import { exports } from "cloudflare:workers";
 2import { beforeEach, describe, expect, it, vi } from "vitest";
 3import { mergePage } from "../src/index";
 4import { mimeMessage } from "../src/email";
 5import { esc } from "../src/html";
 6import type { Check } from "../src/monitor";
 7
 8const auth = (user: string, pass: string) => ({ authorization: "Basic " + btoa(`${user}:${pass}`) });
 9const get = (path: string, headers: HeadersInit = {}) =>
10  exports.default.fetch(new Request(`https://monitor.test${path}`, { headers }));
11
12// Rendering the homepage starts the regional alarm loops; keep their checks off the network.
13beforeEach(() => {
14  vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("{}"));
15});
16
17describe("auth", () => {
18  it("rejects missing or wrong credentials", async () => {
19    const none = await get("/");
20    expect(none.status).toBe(401);
21    expect(none.headers.get("www-authenticate")).toContain("Basic");
22    expect((await get("/", auth("admin", "wrong"))).status).toBe(401);
23    expect((await get("/", { authorization: "Basic !!!" })).status).toBe(401);
24  });
25
26  it("serves pages with valid credentials", async () => {
27    const home = await get("/", auth("admin", "secret"));
28    expect(home.status).toBe(200);
29    const text = await home.text();
30    expect(text).toContain("a.test/healthz");
31    expect(text).toContain(`/endpoint?url=${encodeURIComponent("https://a.test/healthz")}`);
32
33    const hist = await get(`/endpoint?url=${encodeURIComponent("https://a.test/healthz")}&failures=1`, auth("admin", "secret"));
34    expect(hist.status).toBe(200);
35    expect(await hist.text()).toContain("Non-200 only");
36  });
37
38  it("rejects unknown endpoints", async () => {
39    const res = await get(`/endpoint?url=${encodeURIComponent("https://evil.test")}`, auth("admin", "secret"));
40    expect(res.status).toBe(404);
41  });
42});
43
44describe("mergePage", () => {
45  const c = (region: Check["region"], ts: number): Check =>
46    ({ region, endpoint: "e", ts, status: 200, duration_ms: 1, body: null, error: null });
47
48  it("merges newest first and sets the cursor", () => {
49    const { checks, nextBefore } = mergePage([[c("wnam", 9), c("wnam", 5), c("wnam", 1)], [c("weur", 8), c("weur", 4)]], 3);
50    expect(checks.map((x) => x.ts)).toEqual([9, 8, 5]);
51    expect(nextBefore).toBe(5);
52  });
53
54  it("keeps rows sharing the cut-off timestamp together", () => {
55    const { checks, nextBefore } = mergePage([[c("wnam", 9), c("wnam", 5)], [c("weur", 5), c("weur", 2)]], 2);
56    expect(checks.map((x) => x.ts)).toEqual([9, 5, 5]);
57    expect(nextBefore).toBe(5);
58  });
59
60  it("returns no cursor when everything fits", () => {
61    expect(mergePage([[c("wnam", 3)], []], 50).nextBefore).toBeNull();
62  });
63});
64
65describe("esc", () => {
66  it("escapes HTML", () => {
67    expect(esc(`<script>"&'`)).toBe("&lt;script&gt;&quot;&amp;&#39;");
68  });
69});
70
71describe("mimeMessage", () => {
72  it("builds a plain-text message with encoded subject and body", () => {
73    const raw = mimeMessage({
74      from: "[email protected]",
75      to: "[email protected]",
76      subject: "[monitor] DOWN: héllo",
77      text: "line one\n" + "x".repeat(200) + " ✓",
78      date: new Date("2026-10-10T05:50:21Z"),
79    });
80    const [head, body] = raw.split("\r\n\r\n");
81    expect(head).toContain("From: Monitor <[email protected]>\r\nTo: [email protected]\r\n");
82    expect(head).toContain("Date: Sat, 10 Oct 2026 05:50:21 GMT");
83    expect(head).toMatch(/Message-ID: <[0-9a-f-]{36}@rtw\.run>/);
84    const subject = head.match(/Subject: =\?UTF-8\?B\?(.+)\?=/)![1];
85    const decode = (b64: string) => new TextDecoder().decode(Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)));
86    expect(decode(subject)).toBe("[monitor] DOWN: héllo");
87    expect(body.split("\r\n").every((l) => l.length <= 76)).toBe(true);
88    expect(decode(body.replace(/\r\n/g, ""))).toBe("line one\n" + "x".repeat(200) + " ✓");
89  });
90});