import assert from "node:assert/strict"; import { spawn, spawnSync } from "node:child_process"; import fs from "node:fs"; import http from "node:http"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, it } from "vitest"; const scriptPath = path.resolve("scripts/rotate-lighthouse-session-cookie.mjs"); const fixtures = []; function createTempDir() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "lighthouse-cookie-")); fixtures.push(dir); return dir; } function spawnScript(env) { return new Promise((resolve) => { const child = spawn(process.execPath, [scriptPath], { env, stdio: ["ignore", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); child.stdout.on("data", (chunk) => { stdout += chunk; }); child.stderr.on("data", (chunk) => { stderr += chunk; }); child.on("close", (status) => { resolve({ status, stdout, stderr }); }); }); } function startLoginServer() { const requests = []; const assertionErrors = []; const server = http.createServer((req, res) => { requests.push(req); if (req.method === "GET" && req.url === "/login") { res.writeHead(200, { "content-type": "text/html", "set-cookie": "RJ_session=seed-cookie; Path=/; HttpOnly", }); res.end('
'); return; } if (req.method === "POST" && req.url === "/login") { let body = ""; req.setEncoding("utf8"); req.on("data", (chunk) => { body += chunk; }); req.on("end", () => { try { const params = new URLSearchParams(body); assert.equal(req.headers["x-csrf-token"], "csrf-token-123"); assert.equal(req.headers.cookie, "RJ_session=seed-cookie"); assert.equal(params.get("action"), "login"); assert.equal(params.get("csrf"), "csrf-token-123"); assert.equal(params.get("email"), "lighthouse@example.test"); assert.equal(params.get("password"), "super-secret-password"); res.writeHead(302, { location: "/", "set-cookie": "RJ_session=fresh-cookie; Path=/; HttpOnly; Secure", }); res.end(); } catch (error) { assertionErrors.push(error); res.writeHead(500); res.end("assertion failed"); } }); return; } res.writeHead(404); res.end("not found"); }); return new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => { resolve({ url: `http://127.0.0.1:${server.address().port}/login`, assertionErrors, requests, close: () => new Promise((closeResolve) => server.close(closeResolve)), }); }); }); } afterEach(() => { while (fixtures.length > 0) { fs.rmSync(fixtures.pop(), { recursive: true, force: true }); } }); describe("rotate-lighthouse-session-cookie", () => { it("logs in with CSRF protection and writes the fresh cookie to GITHUB_ENV", async () => { const server = await startLoginServer(); const tempDir = createTempDir(); const githubEnv = path.join(tempDir, "github-env"); try { const result = await spawnScript({ ...process.env, GITHUB_ENV: githubEnv, LIGHTHOUSE_LOGIN_URL: server.url, LIGHTHOUSE_LOGIN_EMAIL: "lighthouse@example.test", LIGHTHOUSE_LOGIN_PASSWORD: "super-secret-password", LIGHTHOUSE_SESSION_COOKIE_NAME: "RJ_session", LIGHTHOUSE_COOKIE_DOMAIN: "boiler.saas-ctrl.com", }); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /Minted Lighthouse session cookie RJ_session/); assert.doesNotMatch(result.stdout, /fresh-cookie|super-secret-password/); assert.doesNotMatch(result.stderr, /fresh-cookie|super-secret-password/); const envFile = fs.readFileSync(githubEnv, "utf8"); assert.match(envFile, /^LIGHTHOUSE_SESSION_COOKIE=fresh-cookie$/m); assert.match(envFile, /^LIGHTHOUSE_SESSION_COOKIE_NAME=RJ_session$/m); assert.match(envFile, /^LIGHTHOUSE_COOKIE_DOMAIN=boiler\.saas-ctrl\.com$/m); assert.equal(server.requests.length, 2); assert.deepEqual(server.assertionErrors, []); } finally { await server.close(); } }); it("fails fast when credentials are missing without printing secret material", () => { const env = { ...process.env }; delete env.LIGHTHOUSE_SESSION_COOKIE; env.LIGHTHOUSE_LOGIN_EMAIL = ""; env.LIGHTHOUSE_LOGIN_PASSWORD = ""; const result = spawnSync(process.execPath, [scriptPath], { encoding: "utf8", env, }); assert.equal(result.status, 1); assert.match(result.stderr, /missing required env var LIGHTHOUSE_LOGIN_EMAIL/); assert.doesNotMatch(result.stderr, /password/i); }); it("falls back to a pre-provisioned cookie while login credentials are being rolled out", () => { const tempDir = createTempDir(); const githubEnv = path.join(tempDir, "github-env"); const result = spawnSync(process.execPath, [scriptPath], { encoding: "utf8", env: { ...process.env, GITHUB_ENV: githubEnv, LIGHTHOUSE_LOGIN_EMAIL: "", LIGHTHOUSE_LOGIN_PASSWORD: "", LIGHTHOUSE_SESSION_COOKIE: "existing-cookie", LIGHTHOUSE_SESSION_COOKIE_NAME: "RJ_session", }, }); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /Using pre-provisioned Lighthouse session cookie RJ_session/); assert.doesNotMatch(result.stdout, /existing-cookie/); assert.match(fs.readFileSync(githubEnv, "utf8"), /^LIGHTHOUSE_SESSION_COOKIE=existing-cookie$/m); }); });