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

One test walks every public page and fails any that lacks a title, description, og tags or canonical URL #9637

Merged opened this

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

Merged

scout/forge:scout/9625-one-test-walks-every-public-page-and-fai → main 1 commit fork

  • Merged. scout/9625-one-test-walks-every-public-page-and-fai was merged into main as d71c35b.

Review

Sign in to review this change.
1 file changed +138 −0
test/public-head.test.ts added +138
@@ -0,0 +1,138 @@
1 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2 import { mkdtemp, rm } from "node:fs/promises";
3 import { tmpdir } from "node:os";
4 import { join } from "node:path";
5 import { createApp as createForge, type App } from "../src/app.ts";
6 import { createRepo } from "../src/actions/repos.ts";
7 import { getRepo } from "../src/core/repos.ts";
8 import { createIssue } from "../src/core/issues.ts";
9 import { createApp as createAppRecord } from "../src/core/apps.ts";
10 import { createUser } from "../src/core/users.ts";
11 import { catalog } from "../src/packs/catalog.ts";
12 import { serve, type Servers } from "../src/http/server.ts";
13
14 /** The content of the first tag matching, or undefined when there is none. */
15 function meta(html: string, key: "name" | "property", value: string): string | undefined {
16 for (const m of html.matchAll(/<meta\s[^>]*>/g)) {
17 const tag = m[0];
18 if (!new RegExp(`${key}="${value}"`).test(tag)) continue;
19 return /content="([^"]*)"/.exec(tag)?.[1];
20 }
21 return undefined;
22 }
23
24 function canonical(html: string): string | undefined {
25 for (const m of html.matchAll(/<link\s[^>]*>/g))
26 if (/rel="canonical"/.test(m[0])) return /href="([^"]*)"/.exec(m[0])?.[1];
27 return undefined;
28 }
29
30 /** Every missing or empty head tag on a page, as "tag" names. */
31 function headProblems(html: string, origin: string): string[] {
32 const out: string[] = [];
33 const title = /<title>([\s\S]*?)<\/title>/.exec(html)?.[1];
34 if (!title?.trim()) out.push("<title>");
35 for (const [label, key, value] of [
36 ["meta name=description", "name", "description"],
37 ["og:title", "property", "og:title"],
38 ["og:description", "property", "og:description"],
39 ["og:image", "property", "og:image"],
40 ] as const)
41 if (!meta(html, key, value)?.trim()) out.push(label);
42 const c = canonical(html);
43 if (!c?.trim()) out.push("link rel=canonical");
44 else {
45 let ok = false;
46 try {
47 ok = new URL(c).origin === origin && c.startsWith(origin + "/");
48 } catch {}
49 if (!ok) out.push(`link rel=canonical (${c} is not an absolute URL on ${origin})`);
50 }
51 return out;
52 }
53
54 describe("every public page names itself", () => {
55 let dataDir: string;
56 let app: App;
57 let servers: Servers;
58 let base: string;
59 let pages: string[] = [];
60
61 beforeAll(async () => {
62 dataDir = await mkdtemp(join(tmpdir(), "forge-publichead-"));
63 app = createForge({ dataDir, port: 0 });
64 servers = await serve(app);
65 base = servers.url;
66 const u = await createUser(app.db, {
67 origin: "seed",
68 trusted: true,
69 username: "alice",
70 email: "[email protected]",
71 password: "alice-passphrase",
72 adminIfFirst: true,
73 });
74 if (!u.ok) throw new Error(u.error);
75 for (const name of ["shown", "widget"]) {
76 const made = await createRepo(app, u.value, { name, visibility: "public" });
77 if (!made.ok) throw new Error(String(made.error));
78 }
79 const shown = getRepo(app.db, "alice", "shown")!.id;
80 for (const kind of ["issue", "pull"] as const) {
81 const i = createIssue(app.db, { repoId: shown, authorId: u.value.id, title: `A ${kind}`, kind });
82 if (!i.ok) throw new Error(String(i.error));
83 if (kind === "pull")
84 app.db
85 .query(
86 `INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, head_repo_id)
87 VALUES (?1, 'main', 'topic', '', '', ?2)`,
88 )
89 .run(i.value.id, shown);
90 }
91 const rec = createAppRecord(app.db, { repoId: getRepo(app.db, "alice", "widget")!.id, slug: "widget", access: "public" });
92 if (!rec.ok) throw new Error(rec.error);
93 const pack = catalog().packs[0];
94 if (!pack) throw new Error("the catalog ships no pack to walk");
95 // Walked as a stranger. Left out, on purpose: redirects to sign in (/map,
96 // /new, /board, /feed...), feeds and files (/changelog.atom, /sitemap.xml),
97 // and /signin-style forms, which og-tags.test.ts already covers.
98 pages = [
99 "/",
100 "/explore",
101 "/alice/shown",
102 "/alice/widget",
103 "/alice/shown/issues/1",
104 "/alice/shown/pulls/2",
105 "/packs",
106 `/packs/${pack.name}`,
107 "/api", // the API reference: the docs page a stranger can open
108 "/agents", // the page for AI assistants
109 "/changelog",
110 ];
111 });
112
113 afterAll(async () => {
114 await servers.stop();
115 await rm(dataDir, { recursive: true, force: true });
116 });
117
118 test("the head checker names what is missing", () => {
119 const bad = `<html><head><title> </title><link rel="canonical" href="https://elsewhere.test/x"></head></html>`;
120 const found = headProblems(bad, "http://forge.test");
121 expect(found).toContain("<title>");
122 expect(found).toContain("og:image");
123 expect(found.some((f) => f.startsWith("link rel=canonical (https://elsewhere.test/x"))).toBe(true);
124 });
125
126 test("home, explore, repo, app, issue, pull, packs, docs and changelog carry a full head", async () => {
127 const failures: string[] = [];
128 for (const path of pages) {
129 const res = await fetch(base + path, { redirect: "manual" });
130 if (res.status !== 200) {
131 failures.push(`${path}: answered ${res.status}, not a page`);
132 continue;
133 }
134 for (const p of headProblems(await res.text(), new URL(base).origin)) failures.push(`${path}: ${p}`);
135 }
136 expect(failures).toEqual([]);
137 });
138 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs