Repository navigation
Expand file tree
/
Copy pathcheck-availability.mjs
More file actions
116 lines (110 loc) · 5.56 KB
/
Copy pathcheck-availability.mjs
File metadata and controls
116 lines (110 loc) · 5.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
const origin = "https://www.gecode.dev";
const config = JSON.parse(await readFile("workers/docs/wrangler.jsonc", "utf8")).env.production.vars;
const version = config.LATEST_DOC_VERSION;
const revision = JSON.parse(config.DOC_REVISIONS ?? "{}")[version];
assert(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(version), "Invalid production documentation version");
assert(revision === undefined || /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(revision),
"Invalid production documentation revision");
async function bytes(response) {
const chunks = [];
let length = 0;
for await (const chunk of response.body) {
length += chunk.length;
assert(length <= 1_000_000, "Availability response exceeded 1 MB");
chunks.push(chunk);
}
return Buffer.concat(chunks);
}
async function check(path, status, verify, options = {}) {
const response = await fetch(`${origin}${path}`, {
redirect: "manual", signal: AbortSignal.timeout(10_000), ...options,
});
try {
assert.equal(response.status, status, `${path}: HTTP status`);
await verify(response);
console.log(`${options.method ?? "GET"} ${path}: ${status}`);
} catch (error) {
throw new Error(`${path}: ${error.message}`, { cause: error });
} finally {
if (!response.bodyUsed) await response.body?.cancel();
}
}
function docsHeaders(response, relative, indexable) {
assert.equal(response.headers.get("x-gecode-documentation-version"), version, "Selected version");
assert.equal(response.headers.get("x-gecode-documentation-revision"), revision ?? "legacy", "Selected revision");
assert.equal(/\bnoindex\b/i.test(response.headers.get("x-robots-tag") ?? ""), !indexable, "Indexing policy");
assert.equal(response.headers.get("link"), indexable
? `<${origin}/doc/latest/${relative}>; rel="canonical"` : null, "Documentation canonical");
}
for (const path of ["/", "/download/"]) {
await check(path, 200, async (response) => {
assert.match(response.headers.get("content-type"), /text\/html/);
const html = (await bytes(response)).toString();
assert.match(html, /<meta name="generator" content="Astro /, "Astro page");
assert(html.includes(`<link rel="canonical" href="${origin}${path}">`), "Website canonical");
assert(!/\bnoindex\b/i.test(response.headers.get("x-robots-tag") ?? ""), "Website must remain indexable");
});
}
await check("/download.html?availability=1", 308, (response) => {
assert.equal(response.headers.get("location"), `${origin}/download/?availability=1`);
});
await check("/users-archive/index.html", 200, async (response) => {
assert.match(response.headers.get("content-type"), /text\/html/);
assert.match((await bytes(response)).toString(), /Gecode users archive/);
});
await check("/robots.txt?availability=1", 200, async (response) => {
assert.match(response.headers.get("content-type"), /text\/plain/);
const text = (await bytes(response)).toString();
assert(text.includes(`Sitemap: ${origin}/doc/sitemap.xml`));
assert.match(text, /^Disallow: \/doc\/$/m);
assert.match(text, /^Disallow: \/doc-latest$/m);
assert.match(text, /^Allow: \/doc\/latest\/$/m);
assert.match(text, /^Allow: \/doc\/sitemap\.xml$/m);
assert.doesNotMatch(text, /^Disallow:\s*\/$/m,
"Robots rules must not exclude the entire website");
});
await check("/doc/latest/reference/index.html", 200, async (response) => {
docsHeaders(response, "reference/index.html", true);
assert.match(response.headers.get("content-type"), /text\/html/);
assert.match((await bytes(response)).toString(), /<html\b/i);
});
await check(`/doc/${version}/reference/index.html`, 200, (response) => {
docsHeaders(response, "reference/index.html", false);
assert.match(response.headers.get("content-type"), /text\/html/);
}, { method: "HEAD" });
await check("/doc/sitemap.xml", 200, async (response) => {
assert.match(response.headers.get("content-type"), /xml/);
const xml = (await bytes(response)).toString();
assert.match(xml, /<sitemapindex\b/);
const locations = [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((match) => match[1]);
assert(locations.length > 0 && locations.every((url) => url.startsWith(`${origin}/doc/latest/`)),
"Documentation sitemap must advertise latest URLs");
});
const pdf = `/doc/${version}/MPG.pdf`;
await check(pdf, 200, (response) => {
docsHeaders(response, "MPG.pdf", false);
assert.match(response.headers.get("content-type"), /application\/pdf/);
assert(Number(response.headers.get("content-length")) > 16, "PDF size");
}, { method: "HEAD" });
await check(pdf, 206, async (response) => {
docsHeaders(response, "MPG.pdf", false);
assert.equal(response.headers.get("content-length"), "16");
assert.match(response.headers.get("content-range"), /^bytes 0-15\/\d+$/);
const body = await bytes(response);
assert.equal(body.length, 16);
assert.equal(body.subarray(0, 5).toString(), "%PDF-");
}, { headers: { Range: "bytes=0-15" } });
if (revision) {
await check("/doc/latest/modeling/index.html", 200, async (response) => {
docsHeaders(response, "modeling/index.html", true);
assert.match(response.headers.get("content-type"), /text\/html/);
assert.match((await bytes(response)).toString(), /pagefind/i, "Modeling search UI");
});
await check("/doc/latest/modeling/pagefind/pagefind.js", 200, async (response) => {
assert.match(response.headers.get("content-type"), /(?:text|application)\/javascript/);
assert((await bytes(response)).length > 0, "Modeling search runtime");
});
}
console.log(`Production availability passed: documentation ${version}, revision ${revision ?? "legacy"}.`);