136 lines
4.4 KiB
JavaScript
136 lines
4.4 KiB
JavaScript
|
|
// Smoke test for scripts/local-setup.sh — verifies idempotency and that
|
||
|
|
// the .env upserts preserve unrelated keys. Uses a stubbed `mkcert` on
|
||
|
|
// the runner so no real CA is touched.
|
||
|
|
import { test } from "node:test";
|
||
|
|
import assert from "node:assert/strict";
|
||
|
|
import { mkdtempSync, writeFileSync, readFileSync, copyFileSync, mkdirSync, existsSync, chmodSync, cpSync, rmSync } from "node:fs";
|
||
|
|
import { spawnSync } from "node:child_process";
|
||
|
|
import { tmpdir } from "node:os";
|
||
|
|
import path from "node:path";
|
||
|
|
|
||
|
|
function runLocalSetup(projectDir, extraEnv = {}) {
|
||
|
|
const stubDir = path.join(projectDir, ".stub-bin");
|
||
|
|
mkdirSync(stubDir, { recursive: true });
|
||
|
|
|
||
|
|
// mkcert stub: writes minimal PEM-looking files so the script's
|
||
|
|
// existence check is satisfied. Also implements `-CAROOT`.
|
||
|
|
const mkcertStub = `#!/usr/bin/env bash
|
||
|
|
set -e
|
||
|
|
if [ "$1" = "-CAROOT" ]; then
|
||
|
|
echo "/tmp/fake-caroot"
|
||
|
|
exit 0
|
||
|
|
fi
|
||
|
|
cert=""; key=""
|
||
|
|
positional=()
|
||
|
|
while [ $# -gt 0 ]; do
|
||
|
|
case "$1" in
|
||
|
|
-cert-file) cert="$2"; shift 2;;
|
||
|
|
-key-file) key="$2"; shift 2;;
|
||
|
|
*) positional+=("$1"); shift;;
|
||
|
|
esac
|
||
|
|
done
|
||
|
|
san=""
|
||
|
|
for s in "\${positional[@]}"; do
|
||
|
|
if [[ "$s" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$ ]]; then
|
||
|
|
if [ -z "$san" ]; then san="IP:$s"; else san="$san,IP:$s"; fi
|
||
|
|
else
|
||
|
|
if [ -z "$san" ]; then san="DNS:$s"; else san="$san,DNS:$s"; fi
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \\
|
||
|
|
-subj "/CN=local-test" \\
|
||
|
|
-addext "subjectAltName=$san" \\
|
||
|
|
-keyout "$key" -out "$cert" >/dev/null 2>&1
|
||
|
|
`;
|
||
|
|
const stubPath = path.join(stubDir, "mkcert");
|
||
|
|
writeFileSync(stubPath, mkcertStub);
|
||
|
|
chmodSync(stubPath, 0o755);
|
||
|
|
|
||
|
|
return spawnSync("bash", [path.join(projectDir, "scripts", "local-setup.sh")], {
|
||
|
|
cwd: projectDir,
|
||
|
|
env: {
|
||
|
|
...process.env,
|
||
|
|
PATH: `${stubDir}:${process.env.PATH}`,
|
||
|
|
LOCAL_SETUP_DRY_RUN: "1",
|
||
|
|
LOCAL_SETUP_NONINTERACTIVE: "1",
|
||
|
|
OSTYPE: "linux-gnu",
|
||
|
|
...extraEnv,
|
||
|
|
},
|
||
|
|
encoding: "utf8",
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeFakeProject() {
|
||
|
|
const dir = mkdtempSync(path.join(tmpdir(), "tx-localsetup-"));
|
||
|
|
mkdirSync(path.join(dir, "scripts"), { recursive: true });
|
||
|
|
// Copy the script under test verbatim.
|
||
|
|
copyFileSync(
|
||
|
|
path.resolve("scripts/local-setup.sh"),
|
||
|
|
path.join(dir, "scripts", "local-setup.sh"),
|
||
|
|
);
|
||
|
|
chmodSync(path.join(dir, "scripts", "local-setup.sh"), 0o755);
|
||
|
|
|
||
|
|
// Minimal .env.example so bootstrap works.
|
||
|
|
writeFileSync(
|
||
|
|
path.join(dir, ".env.example"),
|
||
|
|
[
|
||
|
|
"SESSION_SECRET=change-me",
|
||
|
|
"POSTGRES_PASSWORD=change-me",
|
||
|
|
"SOME_USER_KEY=preserve-me",
|
||
|
|
"LOCAL_DOMAIN=tx.local",
|
||
|
|
"LOCAL_IP=127.0.0.1",
|
||
|
|
"BASE_URL=https://tx.local",
|
||
|
|
"PUBLIC_BASE_URL=https://tx.local",
|
||
|
|
"ALLOWED_ORIGINS=https://tx.local",
|
||
|
|
"HTTPS_MODE=local",
|
||
|
|
"",
|
||
|
|
].join("\n"),
|
||
|
|
);
|
||
|
|
return dir;
|
||
|
|
}
|
||
|
|
|
||
|
|
test("first run bootstraps .env and writes a cert", () => {
|
||
|
|
const dir = makeFakeProject();
|
||
|
|
try {
|
||
|
|
const r = runLocalSetup(dir);
|
||
|
|
assert.equal(r.status, 0, r.stderr || r.stdout);
|
||
|
|
const env = readFileSync(path.join(dir, ".env"), "utf8");
|
||
|
|
assert.match(env, /^LOCAL_DOMAIN=tx\.local$/m);
|
||
|
|
assert.match(env, /^BASE_URL=https:\/\/tx\.local$/m);
|
||
|
|
assert.match(env, /^PUBLIC_BASE_URL=https:\/\/tx\.local$/m);
|
||
|
|
assert.match(env, /^SOME_USER_KEY=preserve-me$/m);
|
||
|
|
assert.ok(existsSync(path.join(dir, "certs", "local-cert.pem")));
|
||
|
|
assert.ok(existsSync(path.join(dir, "certs", "local-key.pem")));
|
||
|
|
} finally {
|
||
|
|
rmSync(dir, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("second run is a no-op for cert generation and preserves user edits", () => {
|
||
|
|
const dir = makeFakeProject();
|
||
|
|
try {
|
||
|
|
const first = runLocalSetup(dir);
|
||
|
|
assert.equal(first.status, 0, first.stderr || first.stdout);
|
||
|
|
|
||
|
|
// Operator edits an unrelated key.
|
||
|
|
let env = readFileSync(path.join(dir, ".env"), "utf8");
|
||
|
|
env = env.replace(/^SOME_USER_KEY=.*$/m, "SOME_USER_KEY=user-edited");
|
||
|
|
writeFileSync(path.join(dir, ".env"), env);
|
||
|
|
|
||
|
|
// Capture cert mtime to verify second run didn't rewrite it.
|
||
|
|
const certPath = path.join(dir, "certs", "local-cert.pem");
|
||
|
|
const before = readFileSync(certPath);
|
||
|
|
|
||
|
|
const second = runLocalSetup(dir);
|
||
|
|
assert.equal(second.status, 0, second.stderr || second.stdout);
|
||
|
|
|
||
|
|
const after = readFileSync(certPath);
|
||
|
|
assert.deepEqual(before, after, "cert was rewritten on idempotent re-run");
|
||
|
|
|
||
|
|
const env2 = readFileSync(path.join(dir, ".env"), "utf8");
|
||
|
|
assert.match(env2, /^SOME_USER_KEY=user-edited$/m);
|
||
|
|
} finally {
|
||
|
|
rmSync(dir, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|