| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { StatusIncident, StatusMaintenance } from "@g1t/contracts"; |
| 5 | |
| 6 | import { components } from "./components.ts"; |
| 7 | import { type DayRow, buildPage } from "./model.ts"; |
| 8 | import { |
| 9 | THEME_KEY, |
| 10 | THEME_SCRIPT, |
| 11 | byMonth, |
| 12 | dayDetail, |
| 13 | escape, |
| 14 | prose, |
| 15 | renderBadge, |
| 16 | renderHistory, |
| 17 | renderIncident, |
| 18 | renderMaintenance, |
| 19 | renderMessage, |
| 20 | renderPage, |
| 21 | renderSubscribe, |
| 22 | } from "./render.ts"; |
| 23 | |
| 24 | const NOW = new Date("2026-10-05T12:00:00Z"); |
| 25 | const OPTIONS = { siteUrl: "https://g1t.sh", supportUrl: "https://g1t.sh/support", ogImage: "", selfUrl: "https://status.g1t.sh/", now: NOW, email: true }; |
| 26 | const parts = components( |
| 27 | { |
| 28 | SITE_URL: "https://g1t.sh", |
| 29 | API_URL: "https://api.g1t.sh", |
| 30 | DOCS_URL: "https://docs.g1t.sh", |
| 31 | MODELS_URL: "https://models.g1t.sh", |
| 32 | PROBE_REPO: "flagon-io/g1t", |
| 33 | }, |
| 34 | false, |
| 35 | ); |
| 36 | |
| 37 | function page(states: Record<string, "up" | "degraded" | "down">, incidents: StatusIncident[] = [], maintenance: StatusMaintenance[] = []) { |
| 38 | const current = new Map( |
| 39 | Object.entries(states).map(([k, state]) => [k, { state, detail: state === "down" ? "Failed: HTTP 502" : "Answered in 80 ms", latency_ms: 80 }]), |
| 40 | ); |
| 41 | const days: DayRow[] = [ |
| 42 | { component: "api", day: "2026-10-05", checks: 720, up: 719, degraded: 0, down: 1, latency_total: 71900, latency_count: 719 }, |
| 43 | ]; |
| 44 | return buildPage({ parts, current, checkedAt: "2026-10-05T11:59:40Z", days, incidents, maintenance, now: NOW }); |
| 45 | } |
| 46 | |
| 47 | const INCIDENT: StatusIncident = { |
| 48 | id: "abc", |
| 49 | title: "Sign-in <failing>", |
| 50 | impact: "down", |
| 51 | status: "identified", |
| 52 | components: ["site"], |
| 53 | component_impacts: [{ key: "site", impact: "major_outage" }], |
| 54 | started_at: "2026-10-05T11:40:00Z", |
| 55 | resolved_at: null, |
| 56 | url: "https://status.g1t.sh/incidents/abc", |
| 57 | postmortem_published_at: null, |
| 58 | updates: [{ id: "u1", at: "2026-10-05T11:45:00Z", status: "identified", text: "A bad deploy.\n\nRolling back." }], |
| 59 | }; |
| 60 | |
| 61 | const MAINT: StatusMaintenance = { |
| 62 | id: "m1", |
| 63 | title: "Database upgrade", |
| 64 | message: "Pushes pause for up to five minutes.", |
| 65 | components: ["git"], |
| 66 | starts_at: "2026-10-06T02:00:00Z", |
| 67 | ends_at: "2026-10-06T03:00:00Z", |
| 68 | state: "scheduled", |
| 69 | url: "https://status.g1t.sh/maintenance/m1", |
| 70 | updates: [{ id: "mu1", at: "2026-10-05T10:00:00Z", text: "Pushes pause for up to five minutes." }], |
| 71 | }; |
| 72 | |
| 73 | test("escape covers HTML's specials", () => { |
| 74 | assert.equal(escape(`<a href="x">'&'</a>`), "<a href="x">'&'</a>"); |
| 75 | }); |
| 76 | |
| 77 | test("the page: banner, every part with its bar, and no incidents", () => { |
| 78 | const html = renderPage(page({ site: "up", api: "up", git: "up", docs: "up", agents: "up" }), OPTIONS); |
| 79 | assert.match(html, /<h1>All systems normal<\/h1>/); |
| 80 | for (const name of ["Website and sign-in", "API", "Git and repositories", "Documentation", "Sandboxes"]) assert.ok(html.includes(name), name); |
| 81 | assert.equal((html.match(/<ol class="bar"/g) ?? []).length, 5, "Sandboxes has no bar"); |
| 82 | assert.ok(html.includes("No incidents in the last 14 days")); |
| 83 | assert.ok(html.includes('action="/subscribe"'), "a subscribe form"); |
| 84 | assert.ok(html.includes('href="/feed.xml"')); |
| 85 | assert.match(html, /99\.86% uptime/); |
| 86 | assert.match(html, /aria-label="g1t"/); |
| 87 | assert.doesNotMatch(html, /Happening now/); |
| 88 | }); |
| 89 | |
| 90 | test("an outage and an open incident are said at the top, escaped", () => { |
| 91 | const html = renderPage( |
| 92 | page({ site: "down", api: "up", git: "up", docs: "up", agents: "up" }, [ |
| 93 | INCIDENT, |
| 94 | ]), |
| 95 | OPTIONS, |
| 96 | ); |
| 97 | assert.match(html, /<h1>Major outage<\/h1>/); |
| 98 | assert.match(html, /Happening now/); |
| 99 | assert.ok(html.includes("Sign-in <failing>")); |
| 100 | assert.ok(!html.includes("<failing>")); |
| 101 | assert.match(html, /<p>A bad deploy\.<\/p><p>Rolling back\.<\/p>/); |
| 102 | assert.match(html, /Identified · Major outage/); |
| 103 | assert.ok(html.includes('href="/incidents/abc"'), "the title links to its page"); |
| 104 | }); |
| 105 | |
| 106 | test("every page follows the system's light or dark setting, with a switch kept in this browser", () => { |
| 107 | for (const html of [ |
| 108 | renderPage(page({ site: "up" }), OPTIONS), |
| 109 | renderHistory([], [], OPTIONS), |
| 110 | renderMessage(OPTIONS, { title: "Not found", text: "Nothing here." }), |
| 111 | ]) { |
| 112 | const head = html.slice(0, html.indexOf("</head>")); |
| 113 | // The choice is applied by a script that blocks in the head, before anything is drawn. |
| 114 | assert.match(head, /<script src="\/theme\.js"><\/script>/); |
| 115 | assert.match(head, /<meta name="color-scheme" content="dark light">/); |
| 116 | assert.match(head, /theme-color" content="#fbfbfa" media="\(prefers-color-scheme: light\)"/); |
| 117 | // Dark is the base; light on the system's say-so unless Dark was chosen, or when Light was. |
| 118 | assert.match(head, /:root\{--bg:#0f0f11;/); |
| 119 | assert.match(head, /@media \(prefers-color-scheme:light\)\{:root:not\(\[data-theme=dark\]\)\{--bg:#fbfbfa;/); |
| 120 | assert.match(head, /:root\[data-theme=light\]\{--bg:#fbfbfa;/); |
| 121 | for (const choice of ["auto", "light", "dark"]) assert.ok(html.includes(`data-theme-choice="${choice}"`), choice); |
| 122 | } |
| 123 | assert.ok(THEME_SCRIPT.includes(JSON.stringify(THEME_KEY))); |
| 124 | assert.ok(!/<script>/.test(renderPage(page({ site: "up" }), OPTIONS)), "no inline script: the page's policy allows only its own files"); |
| 125 | }); |
| 126 | |
| 127 | test("the theme script picks the stored choice, else auto", () => { |
| 128 | for (const [stored, expected] of [["light", "light"], ["dark", "dark"], [null, "auto"], ["purple", "auto"]] as const) { |
| 129 | const root = { dataset: {} as Record<string, string> }; |
| 130 | const run = new Function("localStorage", "document", THEME_SCRIPT); |
| 131 | run({ getItem: (key: string) => (key === THEME_KEY ? stored : null) }, { documentElement: root }); |
| 132 | assert.equal(root.dataset.theme, expected, String(stored)); |
| 133 | } |
| 134 | // A browser that refuses storage still gets a page. |
| 135 | const root = { dataset: {} as Record<string, string> }; |
| 136 | new Function("localStorage", "document", THEME_SCRIPT)({ getItem: () => { throw new Error("denied"); } }, { documentElement: root }); |
| 137 | assert.equal(root.dataset.theme, "auto"); |
| 138 | }); |
| 139 | |
| 140 | test("a stale page says the checks stopped", () => { |
| 141 | const model = page({ api: "up" }); |
| 142 | const html = renderPage({ ...model, stale: true }, OPTIONS); |
| 143 | assert.match(html, /have not run since/); |
| 144 | }); |
| 145 | |
| 146 | test("a day's detail names what happened", () => { |
| 147 | assert.equal( |
| 148 | dayDetail({ day: "2026-10-05", state: "up", uptime: 99.86, checks: 720, failed: 1, slow: 0, avg_ms: 100, incidents: ["Pushes failing"] }), |
| 149 | "5 Oct 2026 · 99.86% answered · 1 failed check of 720 · average 100 ms · Incident: Pushes failing", |
| 150 | ); |
| 151 | assert.equal( |
| 152 | dayDetail({ day: "2026-10-01", state: "none", uptime: null, checks: 0, failed: 0, slow: 0, avg_ms: null, incidents: [] }), |
| 153 | "1 Oct 2026 · No data", |
| 154 | ); |
| 155 | }); |
| 156 | |
| 157 | test("the badge is an SVG coloured by state", () => { |
| 158 | const svg = renderBadge("degraded", "Partial outage"); |
| 159 | assert.match(svg, /^<svg/); |
| 160 | assert.match(svg, /#ffbd8c/); |
| 161 | assert.match(svg, /partial outage/); |
| 162 | }); |
| 163 | |
| 164 | test("maintenance: upcoming, then in progress with its parts under maintenance", () => { |
| 165 | const ahead = renderPage(page({ site: "up", api: "up", git: "up" }, [], [MAINT]), OPTIONS); |
| 166 | assert.ok(ahead.includes("Upcoming maintenance") && ahead.includes("Database upgrade") && !ahead.includes("Maintenance in progress")); |
| 167 | const during = { ...OPTIONS, now: new Date("2026-10-06T02:30:00Z") }; |
| 168 | const model = buildPage({ |
| 169 | parts, |
| 170 | current: new Map([["git", { state: "down" as const, detail: "Failed: HTTP 503", latency_ms: null }], ["site", { state: "up" as const, detail: "ok", latency_ms: 1 }]]), |
| 171 | checkedAt: "2026-10-06T02:29:40Z", |
| 172 | days: [], |
| 173 | incidents: [], |
| 174 | maintenance: [MAINT], |
| 175 | now: during.now, |
| 176 | }); |
| 177 | const html = renderPage(model, during); |
| 178 | assert.ok(html.includes("Maintenance in progress")); |
| 179 | assert.ok(html.includes("<h1>Under maintenance</h1>")); |
| 180 | assert.ok(html.includes('class="state s-maintenance"')); |
| 181 | }); |
| 182 | |
| 183 | test("an incident's page: every update, its parts' impact, and the postmortem once published", () => { |
| 184 | const names = new Map([["site", "Website and sign-in"]]); |
| 185 | const resolved = { ...INCIDENT, status: "resolved" as const, resolved_at: "2026-10-05T12:40:00Z", postmortem_published_at: "2026-10-06T09:00:00Z" }; |
| 186 | const pm = { summary: "A deploy broke sign-in.", impact: "", timeline: "- 11:40 declared\n- 12:40 resolved", root_cause: "A <bad> cookie.", went_well: "", went_badly: "", action_items: "- Canary deploys", published_at: "2026-10-06T09:00:00Z" }; |
| 187 | const html = renderIncident(resolved, pm, names, { ...OPTIONS, selfUrl: resolved.url }); |
| 188 | assert.ok(html.includes('id="update-u1"')); |
| 189 | assert.ok(html.includes("Website and sign-in")); |
| 190 | assert.ok(html.includes("Major outage")); |
| 191 | assert.ok(html.includes('id="postmortem"')); |
| 192 | assert.ok(html.includes("<h3>Root cause</h3>") && html.includes("A <bad> cookie.")); |
| 193 | assert.ok(!html.includes("<h3>Impact</h3>"), "empty sections are left out"); |
| 194 | assert.ok(html.includes("<ul><li>Canary deploys</li></ul>")); |
| 195 | assert.ok(html.includes("lasted 1h 00m")); |
| 196 | const draft = renderIncident(resolved, null, names, OPTIONS); |
| 197 | assert.ok(!draft.includes('id="postmortem"') && draft.includes("If a postmortem is written")); |
| 198 | }); |
| 199 | |
| 200 | test("prose: paragraphs and lists, escaped", () => { |
| 201 | assert.equal(prose("One\ntwo\n\n- a\n- <b>"),"<p>One<br>two</p><ul><li>a</li><li><b></li></ul>"); |
| 202 | }); |
| 203 | |
| 204 | test("history: months newest first, empty ones said so", () => { |
| 205 | const old = { ...INCIDENT, id: "old", started_at: "2026-08-10T00:00:00Z", resolved_at: "2026-08-10T01:00:00Z" }; |
| 206 | const groups = byMonth([INCIDENT, old], [MAINT]); |
| 207 | assert.deepEqual(groups.map((g) => g.month), ["2026-10", "2026-08"]); |
| 208 | const html = renderHistory([INCIDENT, old], [MAINT], OPTIONS, 3); |
| 209 | assert.ok(html.includes("October 2026") && html.includes("September 2026") && html.includes("August 2026")); |
| 210 | assert.ok(html.includes("No incidents or maintenance.")); |
| 211 | assert.ok(html.indexOf("October 2026") < html.indexOf("August 2026")); |
| 212 | assert.ok(html.includes('href="/maintenance/m1"')); |
| 213 | }); |
| 214 | |
| 215 | test("subscribe pages: the form, its parts, and a confirm button rather than a link that acts", () => { |
| 216 | const html = renderSubscribe(OPTIONS, [{ key: "git", name: "Git" }]); |
| 217 | assert.ok(html.includes('name="components" value="git"') && html.includes('name="website"')); |
| 218 | assert.ok(renderSubscribe({ ...OPTIONS, email: false }, []).includes("not available")); |
| 219 | const confirm = renderMessage(OPTIONS, { title: "Confirm", text: "One more step", form: { action: "/subscribe/confirm", fields: { token: 'a"b' }, button: "Confirm subscription" } }); |
| 220 | assert.ok(confirm.includes('method="post" action="/subscribe/confirm"') && confirm.includes('value="a"b"')); |
| 221 | assert.ok(renderMaintenance(MAINT, new Map([["git", "Git"]]), OPTIONS).includes("Affects Git")); |
| 222 | }); |