vps-health-api/__tests__/ci.test.js
le king fu 156ca818ca
All checks were successful
CI / verification (pull_request) Successful in 19s
CI / verification (push) Successful in 20s
ci: pin the review follow-ups of the CI workflow (#33)
- Forbid `continue-on-error` in ci.yml: that mutation left every test green,
  and a failing step would turn the commit status green in silence.
- boot-from-copy.sh refuses a port already held by something else (any curl
  exit other than 7). Without it, the wait loop could reach the neighbour and
  assert on a server it never started — the new test reproduces exactly that
  when the check is removed.
- Pin every require of the COPY'd runtime modules at column 0, with no
  dynamic import: the boot check only calls /health, so it only proves the
  modules loaded at startup.

Fixes #33

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:54:47 -04:00

160 lines
6.2 KiB
JavaScript

const fs = require("node:fs");
const path = require("node:path");
const http = require("node:http");
const { spawn } = require("node:child_process");
// Pins on the CI workflow and on what the boot check assumes of the Dockerfile.
// The runtime stays 0-dependency and vitest ships no YAML parser, so these read
// the files with narrow regexes: each one looks at a single key, never at the
// YAML structure as a whole.
const ROOT = path.join(__dirname, "..");
const CI_PATH = path.join(ROOT, ".forgejo", "workflows", "ci.yml");
const ci = fs.readFileSync(CI_PATH, "utf-8");
const dockerfile = fs.readFileSync(path.join(ROOT, "Dockerfile"), "utf-8");
// Comment lines stripped, so an explanation that quotes a key cannot satisfy
// (or break) a pin.
const ciCode = ci
.split("\n")
.filter((line) => !/^\s*#/.test(line))
.join("\n");
// Lines of a top-level-indented block (` pull_request:`), up to the next key at
// the same or a lower indentation.
function block(text, key) {
const lines = text.split("\n");
const start = lines.findIndex((l) => new RegExp(`^(\\s*)${key}:`).test(l));
if (start === -1) return null;
const indent = lines[start].match(/^(\s*)/)[1].length;
const out = [lines[start]];
for (const line of lines.slice(start + 1)) {
if (line.trim() === "") continue;
if (line.match(/^(\s*)/)[1].length <= indent) break;
out.push(line);
}
return out.join("\n");
}
describe("CI workflow", () => {
test("runs in the Dockerfile's own base image", () => {
const from = dockerfile.match(/^\s*FROM\s+(\S+)/m);
const container = ciCode.match(/^\s*container:\s*["']?([^"'\s]+)["']?\s*$/m);
expect(from).not.toBeNull();
expect(container).not.toBeNull();
expect(container[1]).toBe(from[1]);
});
test("runs the unit tests, the socket pass and the boot check", () => {
expect(ciCode).toMatch(/^\s*run:\s*npm ci\s*$/m);
expect(ciCode).toMatch(/^\s*run:\s*npm test\s*$/m);
expect(ciCode).toMatch(/^\s*run:\s*bash test-curl\.sh\s*$/m);
expect(ciCode).toMatch(/^\s*run:\s*sh scripts\/boot-from-copy\.sh\s*$/m);
});
test("builds every pull request, whatever branch it targets", () => {
// A `branches: [main]` here would leave the intermediate links of a stack
// without any status.
const pr = block(ciCode, "pull_request");
expect(pr).not.toBeNull();
expect(pr).not.toMatch(/branches(-ignore)?:/);
});
test("builds pushes to main", () => {
const push = block(ciCode, "push");
expect(push).not.toBeNull();
expect(push).toMatch(/branches:\s*\[\s*main\s*\]/);
});
test("avoids the two forms the runner rejects before the first command", () => {
// `defaults:` anywhere, and `working-directory` on a `uses:` step.
expect(ciCode).not.toMatch(/^\s*defaults:/m);
const steps = ciCode.split(/^\s*- (?=name:|uses:|run:)/m);
for (const step of steps) {
if (/^\s*uses:|\n\s*uses:/.test(step)) expect(step).not.toMatch(/working-directory:/);
}
});
test("never lets a failing step pass", () => {
// `continue-on-error: true` turns a red step green without a trace in the
// commit status. Anchored on the key position, so only a real key counts.
expect(ciCode).not.toMatch(/^\s*(- )?continue-on-error\s*:/m);
});
});
describe("what the boot check assumes of the Dockerfile", () => {
test("the image still drops to the node user, as the script does", () => {
expect(dockerfile).toMatch(/^\s*USER\s+node\s*$/m);
});
test("CMD is in exec form, the only one the script parses", () => {
const cmd = dockerfile.match(/^\s*CMD\s+(.+)$/m);
expect(cmd).not.toBeNull();
const argv = JSON.parse(cmd[1]);
expect(Array.isArray(argv)).toBe(true);
expect(argv.length).toBeGreaterThan(0);
});
});
// Runs the script with a listener already on its port: without the pre-check,
// the wait loop could reach this neighbour and assert on a server it never
// started. Async spawn, so the listener keeps answering while the script runs.
function runBootScript(env) {
return new Promise((resolve, reject) => {
const child = spawn("sh", ["scripts/boot-from-copy.sh"], {
cwd: ROOT,
env: { ...process.env, ...env },
});
let stderr = "";
child.stderr.on("data", (d) => (stderr += d));
child.on("error", reject);
child.on("close", (code) => resolve({ code, stderr }));
});
}
describe("boot-from-copy.sh", () => {
test("refuses a port something else already holds", async () => {
const neighbour = http.createServer((req, res) => res.end("not the API"));
await new Promise((resolve) => neighbour.listen(0, "127.0.0.1", resolve));
try {
const { port } = neighbour.address();
const { code, stderr } = await runBootScript({ BOOT_PORT: String(port) });
expect(code).toBe(1);
expect(stderr).toMatch(new RegExp(`port ${port} is already taken`));
} finally {
await new Promise((resolve) => neighbour.close(resolve));
}
}, 15000);
});
// The boot check only calls /health, so it proves the existence of the modules
// loaded at startup, nothing more. A require inside a route handler would load
// only when that route runs and escape it. Heuristic stand-in for "loaded at
// startup": every require starts at column 0 (a top-level statement), and there
// is no dynamic import. Comment lines are skipped, so an explanation quoting
// require( cannot break the pin.
describe("runtime modules are all loaded at startup", () => {
const copyLine = dockerfile.match(/^\s*COPY\s+(.+)$/m);
const sources = copyLine ? copyLine[1].trim().split(/\s+/).slice(0, -1) : [];
const runtimeJs = sources.filter((f) => f.endsWith(".js"));
test("the COPY line lists runtime modules", () => {
expect(runtimeJs.length).toBeGreaterThan(0);
});
test.each(runtimeJs)("%s requires only at module level", (file) => {
const lines = fs.readFileSync(path.join(ROOT, file), "utf-8").split("\n");
const offenders = [];
lines.forEach((line, i) => {
if (/^\s*(\/\/|\/?\*)/.test(line)) return;
if (/\brequire\(/.test(line) && /^\s/.test(line)) offenders.push(`${file}:${i + 1}: ${line.trim()}`);
if (/\bimport\(/.test(line)) offenders.push(`${file}:${i + 1}: ${line.trim()}`);
});
expect(offenders).toEqual([]);
});
});