import { test } from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; // Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test lib/seederCompose.test.ts // // THE KILL SWITCH IS THE CONSTRUCTION (release 21 D4b), so the construction is // what this pins, in docker-compose.seeder.yml: the seeder and the tracker // have no network of their own (they live in the VPN container's namespace), // publish nothing, and the VPN container's firewall is on and its WireGuard // config is the operator's — required, never defaulted, never baked. const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); const TEXT = readFileSync(path.join(REPO, "docker-compose.seeder.yml"), "utf8"); // The services block as name → its lines (comments dropped). Two-space // service keys under a top-level `services:`; enough YAML for this file. function services(text: string): Map { const out = new Map(); let inServices = false; let current: string | null = null; for (const raw of text.split("\n")) { const line = raw.replace(/\s+#.*$/, ""); if (/^\S/.test(line)) { inServices = /^services:\s*$/.test(line); current = null; continue; } if (!inServices || !line.trim() || /^\s*#/.test(raw)) continue; const key = /^ {2}([a-z0-9-]+):\s*$/.exec(line); if (key) { current = key[1]; out.set(current, []); } else if (current) { out.get(current)!.push(line); } } return out; } const S = services(TEXT); const block = (name: string) => (S.get(name) ?? []).join("\n"); test("the profile has a VPN container, the seeder and the tracker — all behind profile `seeder`", () => { assert.deepEqual([...S.keys()].sort(), ["seeder", "seeder-vpn", "tracker"]); for (const name of S.keys()) assert.match(block(name), /^\s{4}profiles: \[seeder\]$/m, name); }); test("the seeder and the tracker share the VPN container's network namespace and nothing else", () => { for (const name of ["seeder", "tracker"]) { const b = block(name); assert.match(b, /^\s{4}network_mode: service:seeder-vpn$/m, `${name}: network_mode`); assert.doesNotMatch(b, /^\s{4}networks:/m, `${name}: no networks of its own`); assert.doesNotMatch(b, /^\s{4}ports:/m, `${name}: publishes nothing`); assert.doesNotMatch(b, /^\s{4}network_mode: (host|bridge)/m, name); // It waits for the tunnel to be up, not merely started. assert.match(b, /seeder-vpn:\n\s+condition: service_healthy/, `${name}: waits for a healthy tunnel`); // Read-only corpus: the seeder writes nothing. for (const vol of b.matchAll(/^\s{6}- [^\s:]+:\/data\/[^\s:]+$/gm)) assert.fail(`${name}: writable mount ${vol[0]}`); assert.match(b, /- corpus:\/data\/transcripts:ro$/m, `${name}: the corpus, read-only`); } }); test("the VPN container: WireGuard, the firewall on, the operator's config required and read-only, no port published", () => { const b = block("seeder-vpn"); assert.match(b, /VPN_TYPE: wireguard/); assert.match(b, /VPN_SERVICE_PROVIDER: custom/); assert.match(b, /image: qmcgaw\/gluetun:v\d+\.\d+\.\d+$/m, "a pinned release"); assert.doesNotMatch(TEXT, /FIREWALL: "?off/i, "nothing turns a firewall off"); assert.match(b, /WIREGUARD_CONF_SECRETFILE: \/gluetun\/wireguard\/wg0\.conf/); assert.doesNotMatch(b, /^\s{4}ports:/m); assert.doesNotMatch(b, /network_mode/); // `${VAR:?…}`: compose refuses to start without it; there is no default. assert.match(b, /- \$\{SEEDER_WG_CONF:\?[^}]+\}:\/gluetun\/wireguard\/wg0\.conf:ro/); assert.doesNotMatch(TEXT, /PrivateKey\s*=|WIREGUARD_PRIVATE_KEY:\s*\S/, "no key in the file"); // Inbound only when the operator forwards a port; empty by default. assert.match(b, /FIREWALL_VPN_INPUT_PORTS: \$\{SEEDER_VPN_INPUT_PORTS:-\}/); });