177 lines
5.8 KiB
JavaScript
177 lines
5.8 KiB
JavaScript
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('<form><input value="csrf-token-123" name="csrf" type="hidden"></form>');
|
|
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);
|
|
});
|
|
});
|