/** * @file Tests for A2 data isolation: machine-level secrets, per-lane database * lifecycle (create once / reset / drop), and `.env` seeding. * * The database lifecycle tests use a SQLite-shaped fixture hook (write a file, * remove a file) rather than a real Postgres — the property worth proving is * CCAM's own orchestration (when a hook runs, what name it is trusted with, * that a repeat call is a no-op), not any particular database engine. * @author Nguyễn Ngọc Trí Vĩ */ const os = require("node:os"); const pathMod = require("node:path"); const fsMod = require("node:fs"); const SUITE_ROOT = fsMod.mkdtempSync(pathMod.join(os.tmpdir(), "ccam-data-")); process.env.DASHBOARD_DB_PATH = pathMod.join(SUITE_ROOT, "dashboard.db"); process.env.LANES_ROOT = pathMod.join(SUITE_ROOT, "lanes"); process.env.CCAM_SECRETS_PATH = pathMod.join(SUITE_ROOT, "secrets.env"); const { describe, it, after } = require("node:test"); const assert = require("node:assert/strict"); const lanesLib = require("../lib/lanes"); const slots = require("../lib/lane-slots"); const profileLib = require("../lib/lane-profile"); const runtime = require("../lib/lane-runtime"); const envLib = require("../lib/lane-env"); const servicesLib = require("../lib/lane-services"); const secretsLib = require("../lib/secrets"); after(() => fsMod.rmSync(SUITE_ROOT, { recursive: true, force: true })); let laneSeq = 0; /** A managed lane row backed by a real directory. */ function makeLane(over = {}) { laneSeq += 1; const cwd = pathMod.join(SUITE_ROOT, `data-lane-cwd-${laneSeq}`); fsMod.mkdirSync(cwd, { recursive: true }); return lanesLib.createLane({ title: `data-lane ${laneSeq}`, cwd, kind: "managed", source_repo: cwd, ...over, }); } /** Write a profile into a repo directory. `hooks` maps name -> script body. */ function writeProfile(root, envText, hooks = {}) { const dir = pathMod.join(root, ".ccam", "profile"); fsMod.mkdirSync(pathMod.join(dir, "hooks"), { recursive: true }); fsMod.writeFileSync(pathMod.join(dir, "profile.env"), envText); for (const [name, body] of Object.entries(hooks)) { const file = pathMod.join(dir, "hooks", `${name}.sh`); fsMod.writeFileSync(file, body); fsMod.chmodSync(file, 0o755); } return dir; } // A file-based "database" so the create/drop lifecycle is provable without a // real engine: db-create WRITES (so a wrongly-repeated call would clobber // existing content, proving the once-only marker actually matters), db-drop // removes. const SQLITE_DB_HOOKS = { "db-create": '#!/usr/bin/env bash\nset -euo pipefail\necho created > "$LANE_DIR/${1:-$DB_NAME}.db"\n', "db-drop": '#!/usr/bin/env bash\nset -euo pipefail\nrm -f "$LANE_DIR/${1:-$DB_NAME}.db"\n', }; describe("secrets", () => { const secretsPath = process.env.CCAM_SECRETS_PATH; it("falls back to defaults when the file is absent", () => { assert.equal(fsMod.existsSync(secretsPath), false); const s = secretsLib.readSecrets(); assert.equal(s.PG_HOST, "127.0.0.1"); assert.equal(s.PG_USER, "postgres"); }); it("refuses a group/world-readable secrets file, falling back to defaults", () => { fsMod.writeFileSync(secretsPath, "PG_USER=leaked-user\n"); fsMod.chmodSync(secretsPath, 0o644); const s = secretsLib.readSecrets(); assert.equal(s.PG_USER, "postgres"); }); it("loads a mode-0600 file", () => { fsMod.writeFileSync(secretsPath, "PG_USER=custom-user\nPG_PASS=custom-pass\n"); fsMod.chmodSync(secretsPath, 0o600); const s = secretsLib.readSecrets(); assert.equal(s.PG_USER, "custom-user"); assert.equal(s.PG_PASS, "custom-pass"); }); }); describe("lane-env seedEnv", () => { it("rewrites ENV_REWRITE keys, keeping every other line byte-identical", () => { const source = pathMod.join(SUITE_ROOT, "env-source-repo"); fsMod.mkdirSync(pathMod.join(source, "backend"), { recursive: true }); fsMod.writeFileSync( pathMod.join(source, "backend", ".env"), "FOO=bar\nDATABASE_URL=stale\nBAZ=qux\n" ); const lane = makeLane({ source_repo: source }); writeProfile( lane.cwd, ["DB_PREFIX=envtest_l", "ENV_FILES=backend/.env", "ENV_REWRITE=DATABASE_URL"].join("\n") ); slots.allocateSlot(lane.id); const current = lanesLib.getLane(lane.id); const profile = profileLib.resolveProfile(current); envLib.seedEnv(current, profile, secretsLib.readSecrets()); const lines = fsMod.readFileSync(pathMod.join(current.cwd, "backend/.env"), "utf8").split("\n"); assert.equal(lines[0], "FOO=bar"); assert.equal(lines[2], "BAZ=qux"); assert.match(lines[1], /^DATABASE_URL=postgresql:\/\//); slots.releaseSlot(lane.id); }); it("--force preserves ENV_PRESERVE keys from the lane's own existing file", () => { const source = pathMod.join(SUITE_ROOT, "env-source-repo-2"); fsMod.mkdirSync(pathMod.join(source, "backend"), { recursive: true }); fsMod.writeFileSync( pathMod.join(source, "backend", ".env"), "JWT_SECRET=source-secret\nOTHER=from-source\n" ); const lane = makeLane({ source_repo: source }); writeProfile(lane.cwd, ["ENV_FILES=backend/.env", "ENV_PRESERVE=JWT_SECRET"].join("\n")); slots.allocateSlot(lane.id); let current = lanesLib.getLane(lane.id); const profile = profileLib.resolveProfile(current); envLib.seedEnv(current, profile, secretsLib.readSecrets()); fsMod.writeFileSync( pathMod.join(current.cwd, "backend/.env"), "JWT_SECRET=lane-own-secret\nOTHER=from-source\n" ); envLib.seedEnv(current, profile, secretsLib.readSecrets(), { force: true }); const text = fsMod.readFileSync(pathMod.join(current.cwd, "backend/.env"), "utf8"); assert.match(text, /JWT_SECRET=lane-own-secret/); slots.releaseSlot(lane.id); }); it("falls back to .env.example when the source .env is missing", () => { const source = pathMod.join(SUITE_ROOT, "env-source-repo-3"); fsMod.mkdirSync(pathMod.join(source, "backend"), { recursive: true }); fsMod.writeFileSync(pathMod.join(source, "backend", ".env.example"), "TEMPLATE=1\n"); const lane = makeLane({ source_repo: source }); writeProfile(lane.cwd, "ENV_FILES=backend/.env\n"); slots.allocateSlot(lane.id); const current = lanesLib.getLane(lane.id); const profile = profileLib.resolveProfile(current); const result = envLib.seedEnv(current, profile, secretsLib.readSecrets()); assert.equal(result.seeded[0].fromExample, true); assert.match( fsMod.readFileSync(pathMod.join(current.cwd, "backend/.env"), "utf8"), /TEMPLATE=1/ ); slots.releaseSlot(lane.id); }); it("throws ENOTMANAGED for an adopted lane", () => { const lane = makeLane({ kind: "adopted" }); writeProfile(lane.cwd, "ENV_FILES=backend/.env\n"); assert.throws( () => envLib.seedEnv( lanesLib.getLane(lane.id), profileLib.resolveProfile(lanesLib.getLane(lane.id)), secretsLib.readSecrets() ), (err) => err.code === "ENOTMANAGED" ); }); it("is a no-op when ENV_FILES is not declared", () => { const lane = makeLane(); writeProfile(lane.cwd, "DB_PREFIX=noenv_l\n"); const result = envLib.seedEnv(lane, profileLib.resolveProfile(lane), secretsLib.readSecrets()); assert.equal(result.skipped, true); }); }); describe("lane-services database guard", () => { it("refuses to drop a name that isn't this lane's own derived one, before spawning anything", async () => { const lane = makeLane(); writeProfile(lane.cwd, "DB_PREFIX=guard_l\n", SQLITE_DB_HOOKS); slots.allocateSlot(lane.id); const current = lanesLib.getLane(lane.id); const profile = profileLib.resolveProfile(current); await assert.rejects( () => servicesLib.dropDatabase(current, profile, "someone-elses-db"), (err) => err.code === "EBADDBNAME" ); assert.equal(fsMod.existsSync(pathMod.join(current.cwd, "someone-elses-db.db")), false); slots.releaseSlot(lane.id); }); it("throws ENOTMANAGED for an adopted lane's own derived name", async () => { const lane = makeLane({ kind: "adopted" }); writeProfile(lane.cwd, "DB_PREFIX=guard2_l\n", SQLITE_DB_HOOKS); slots.allocateSlot(lane.id); const current = lanesLib.getLane(lane.id); const profile = profileLib.resolveProfile(current); await assert.rejects( () => servicesLib.dropDatabase(current, profile, slots.dbName(profile, current.slot)), (err) => err.code === "ENOTMANAGED" ); slots.releaseSlot(lane.id); }); it("creates a database once, tracked by a marker so a repeat call is a no-op", async () => { const lane = makeLane(); writeProfile(lane.cwd, "DB_PREFIX=once_l\n", SQLITE_DB_HOOKS); slots.allocateSlot(lane.id); const current = lanesLib.getLane(lane.id); const profile = profileLib.resolveProfile(current); const first = await servicesLib.ensureDatabase(current, profile, {}); assert.equal(first.created, true); const dbFile = pathMod.join(current.cwd, `once_l${current.slot}.db`); fsMod.writeFileSync(dbFile, "sentinel"); const second = await servicesLib.ensureDatabase(current, profile, {}); assert.equal(second.created, false); assert.equal(fsMod.readFileSync(dbFile, "utf8"), "sentinel", "hook must not re-run"); slots.releaseSlot(lane.id); }); it("is a no-op when DB_PREFIX is not declared", async () => { const lane = makeLane(); writeProfile(lane.cwd, "", SQLITE_DB_HOOKS); slots.allocateSlot(lane.id); const current = lanesLib.getLane(lane.id); const result = await servicesLib.ensureDatabase( current, profileLib.resolveProfile(current), {} ); assert.deepEqual(result, { name: null, created: false }); slots.releaseSlot(lane.id); }); }); describe("lane data lifecycle: provision / reset / remove", () => { it("provision creates the database; reset recreates it; remove drops it", async () => { const lane = makeLane(); writeProfile(lane.cwd, "DB_PREFIX=lifecycle_l\n", SQLITE_DB_HOOKS); const provisioned = await runtime.provisionLane(lanesLib.getLane(lane.id)); const dbFile = pathMod.join(provisioned.cwd, `lifecycle_l${provisioned.slot}.db`); assert.ok(fsMod.existsSync(dbFile), "provision creates the database"); fsMod.writeFileSync(dbFile, "old-feature-data"); const profile = profileLib.resolveProfile(provisioned); await runtime.resetLaneData(lanesLib.getLane(lane.id), profile, {}); assert.equal( fsMod.readFileSync(dbFile, "utf8").trim(), "created", "reset drops and recreates, losing the old content" ); await runtime.removeLaneData(lanesLib.getLane(lane.id), profile, {}); assert.equal(fsMod.existsSync(dbFile), false, "remove drops the database"); slots.releaseSlot(lane.id); }); it("--keep-db leaves the database untouched across reset", async () => { const lane = makeLane(); writeProfile(lane.cwd, "DB_PREFIX=keepdb_l\n", SQLITE_DB_HOOKS); const provisioned = await runtime.provisionLane(lanesLib.getLane(lane.id)); const dbFile = pathMod.join(provisioned.cwd, `keepdb_l${provisioned.slot}.db`); fsMod.writeFileSync(dbFile, "must-survive"); const profile = profileLib.resolveProfile(provisioned); await runtime.resetLaneData(lanesLib.getLane(lane.id), profile, { keepDb: true }); assert.equal(fsMod.readFileSync(dbFile, "utf8"), "must-survive"); slots.releaseSlot(lane.id); }); it("removeLaneData never touches an adopted lane's database", async () => { const lane = makeLane(); writeProfile(lane.cwd, "DB_PREFIX=adopted_l\n", SQLITE_DB_HOOKS); const provisioned = await runtime.provisionLane(lanesLib.getLane(lane.id)); const dbFile = pathMod.join(provisioned.cwd, `adopted_l${provisioned.slot}.db`); assert.ok(fsMod.existsSync(dbFile)); lanesLib.setProvisioningFacts(lane.id, { kind: "adopted" }); const adoptedLane = lanesLib.getLane(lane.id); const profile = profileLib.resolveProfile(adoptedLane); await runtime.removeLaneData(adoptedLane, profile, {}); assert.ok(fsMod.existsSync(dbFile), "an adopted lane's data is never CCAM's to destroy"); slots.releaseSlot(lane.id); }); it("isolates two lanes' databases from each other", async () => { const a = makeLane(); const b = makeLane(); writeProfile(a.cwd, "DB_PREFIX=iso_l\n", SQLITE_DB_HOOKS); writeProfile(b.cwd, "DB_PREFIX=iso_l\n", SQLITE_DB_HOOKS); const pa = await runtime.provisionLane(lanesLib.getLane(a.id)); const pb = await runtime.provisionLane(lanesLib.getLane(b.id)); assert.notEqual(pa.slot, pb.slot); const dbA = pathMod.join(pa.cwd, `iso_l${pa.slot}.db`); const dbB = pathMod.join(pb.cwd, `iso_l${pb.slot}.db`); fsMod.writeFileSync(dbA, "rows-for-a"); fsMod.writeFileSync(dbB, "rows-for-b"); assert.notEqual(dbA, dbB); assert.notEqual(fsMod.readFileSync(dbA, "utf8"), fsMod.readFileSync(dbB, "utf8")); slots.releaseSlot(a.id); slots.releaseSlot(b.id); }); }); describe("runtimeFacts never leaks a secret value", () => { it("reports only names and an index, never a connection string", async () => { fsMod.writeFileSync(process.env.CCAM_SECRETS_PATH, "PG_PASS=super-secret-password\n"); fsMod.chmodSync(process.env.CCAM_SECRETS_PATH, 0o600); const lane = makeLane(); writeProfile(lane.cwd, "DB_PREFIX=leak_l\nREDIS=1\n", SQLITE_DB_HOOKS); const provisioned = await runtime.provisionLane(lanesLib.getLane(lane.id)); const facts = await runtime.runtimeFacts(lanesLib.getLane(provisioned.id)); assert.equal(facts.database.name, `leak_l${provisioned.slot}`); assert.equal(facts.redisIndex, provisioned.slot); assert.doesNotMatch(JSON.stringify(facts), /super-secret-password/); slots.releaseSlot(lane.id); }); it("redacts a hook echoing $DATABASE_URL, including its URL-encoded form", async () => { // A password with a character that changes shape once URL-encoded, so // both the raw and encoded substrings actually get exercised. fsMod.writeFileSync(process.env.CCAM_SECRETS_PATH, "PG_PASS=p@ss/word\n"); fsMod.chmodSync(process.env.CCAM_SECRETS_PATH, 0o600); const lane = makeLane(); writeProfile(lane.cwd, "DB_PREFIX=echo_l\n", { "db-create": '#!/usr/bin/env bash\nset -euo pipefail\necho "connecting to $DATABASE_URL"\n', }); slots.allocateSlot(lane.id); const current = lanesLib.getLane(lane.id); const profile = profileLib.resolveProfile(current); const result = await profileLib.runHook(current, profile, "db-create", [ slots.dbName(profile, current.slot), ]); assert.doesNotMatch(result.output, /p@ss\/word/); assert.match(result.output, /\[REDACTED\]/); assert.doesNotMatch(fsMod.readFileSync(result.logPath, "utf8"), /p@ss\/word/); slots.releaseSlot(lane.id); }); });