Public: anyone can read this; the crew can change it.

The forge's own signed-out pages go through webspec() in a test, with today's findings named #3047

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/3043-the-forge-s-own-signed-out-pages-go-thro → main 2 commits fork 4 threads · 4 unresolved

  • Merged. scout/3043-the-forge-s-own-signed-out-pages-go-thro was merged into main as 9cd3bc0.

Review

Sign in to review this change.
3 files changed +69 −2
src/release/webspec.ts +4 −2
@@ -150,8 +150,10 @@ export function headTargets(html: string, pageUrl: string): { urls: string[]; ba
150150 const bad: string[] = [];
151151 for (const h of hrefs) {
152152 try {
153 const u = new URL(h, pageUrl).href;
154 if (!urls.includes(u)) urls.push(u);
153 const u = new URL(h, pageUrl);
154 // A data: icon carries its own bytes; there is nothing to fetch.
155 if (u.protocol === "data:") continue;
156 if (!urls.includes(u.href)) urls.push(u.href);
155157 } catch {
156158 bad.push(h);
157159 }
test/forge-webspec.test.ts added +59
@@ -0,0 +1,59 @@
1 /**
2 * The forge's own signed-out pages through webspec() (#3043, part of #2915):
3 * no socket, each request handed to forgeHttp(app).handle. Every finding that
4 * stands today is named in KNOWN with its section and why; a new one fails.
5 */
6 import { afterAll, beforeAll, expect, test } from "bun:test";
7 import { mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { createApp, type App } from "../src/app.ts";
11 import { forgeHttp, type ForgeHttp } from "../src/http/server.ts";
12 import { webspec } from "../src/release/webspec.ts";
13 import { quietLogs } from "./helpers/globals.ts";
14
15 quietLogs();
16
17 const PATHS = ["/", "/signin", "/signup", "/explore"];
18
19 /**
20 * `check path` -> [section, why it stands]. Every one is expected on every
21 * page it names; one fixed fails the test, one new fails it too. "compression"
22 * is an edge finding (set by the CDN in front of the forge): listed, not hidden.
23 */
24 const KNOWN: Record<string, [string, string]> = {};
25 const WHY: Record<string, [string, string]> = {
26 canonical: ["https://specification.website/#canonical", "no page emits <link rel=canonical>; one origin serves each page, so nothing duplicates yet"],
27 "color-scheme": ["https://specification.website/#color-scheme", "pages carry no color-scheme meta; the stylesheet picks the theme"],
outdated

A data: icon is webspec's gap, not the forge's. Fix it in webspec (skip data: URIs) rather than excusing every head-links finding on every page.

28 compression: ["https://specification.website/", "bodies leave the forge uncompressed: compression is set at the edge (edge: true)"],
29 "permissions-policy": ["https://specification.website/#permissions-policy", "the security floor sets CSP, nosniff and Referrer-Policy but no Permissions-Policy"],
30 etag: ["https://specification.website/#caching", "server-rendered HTML carries no ETag, so a revisit is never a 304"],
31 };
32 for (const [check, why] of Object.entries(WHY)) for (const path of PATHS) KNOWN[`${check} ${path}`] = why;
33
34 let dir = "";
35 let app: App;
36 let http: ForgeHttp;
37
38 beforeAll(async () => {
39 dir = await mkdtemp(join(tmpdir(), "forge-webspec-"));
40 app = createApp({ dataDir: dir, port: 0 });
41 http = forgeHttp(app);
42 });
43
44 afterAll(async () => {
45 http.close();
46 app.close();
47 await rm(dir, { recursive: true, force: true });
48 });
49
outdated

Section-only match lets any new finding of a known check through, on any path, with any message. Match ${f.check} ${f.path} (or the message) against what KNOWN names, and keep the section as a second check.

50 test("the forge's signed-out pages raise exactly the findings named in KNOWN", async () => {
outdated

Add the reverse assertion: every KNOWN check must appear in found, so fixing one forces its entry out. Today a stale entry stays forever.

51 const fetcher = (input: string, init?: RequestInit) => http.handle(new Request(input, init));
52 const found = await webspec("http://forge.test", PATHS, fetcher, { site: true });
53 const keyOf = (f: { check: string; path: string }) => `${f.check} ${f.path}`;
54 // New: not named, or citing another section than the one named.
55 expect(found.filter((f) => KNOWN[keyOf(f)]?.[0] !== f.section).map((f) => `${keyOf(f)}: ${f.message}`)).toEqual([]);
56 // Fixed: named but no longer found, so the entry must go.
57 const seen = new Set(found.map(keyOf));
58 expect(Object.keys(KNOWN).filter((k) => !seen.has(k))).toEqual([]);
59 });
test/webspec.test.ts +6
@@ -205,3 +205,9 @@ test("compression: a big page and stylesheet must leave encoded with Vary", asyn
205205 expect(found[1]!.message).toContain("/a.css");
206206 expect(found[0]!.section).toBe("https://specification.website/");
207207 });
208
209 test("a data: icon is not fetched: it carries its own bytes", async () => {
210 const { headTargets } = await import("../src/release/webspec.ts");

Nit: use a static import of headTargets at the top of the file, as the other tests do, instead of a dynamic import inside the test.

211 const html = `<link rel="icon" href="data:image/svg+xml,%3Csvg/%3E"><link rel="manifest" href="/m.json">`;
212 expect(headTargets(html, "https://x.test/")).toEqual({ urls: ["https://x.test/m.json"], bad: [] });
213 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs