315 lines
12 KiB
JavaScript
315 lines
12 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import { promisify } from "node:util";
|
|
|
|
import {
|
|
buildDevRuntimeProvisioningReport,
|
|
parseArgs
|
|
} from "./dev-runtime-provisioning.mjs";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
|
|
const bunCommand = process.env.HWLAB_BUN_COMMAND || (process.versions.bun ? process.execPath : "bun");
|
|
|
|
test("source check parses target role/database without leaking DB URL material", async () => {
|
|
const report = await buildDevRuntimeProvisioningReport(parseArgs(["--check"]), {
|
|
env: {
|
|
HWLAB_CLOUD_DB_URL: fixturePostgresUrl({ password: fixtureSecret("super") }),
|
|
HWLAB_CLOUD_DB_SSL_MODE: "disable"
|
|
},
|
|
now: () => "2026-05-23T00:00:00.000Z"
|
|
});
|
|
|
|
assert.equal(report.conclusion.status, "ready");
|
|
assert.equal(report.actions.liveDbReadAttempted, false);
|
|
assert.equal(report.actions.liveDbWriteAttempted, false);
|
|
assert.equal(report.db.targetRoleNamePresent, true);
|
|
assert.equal(report.db.targetDatabaseNamePresent, true);
|
|
assert.equal(report.db.targetPasswordPresent, true);
|
|
assert.equal(report.safety.secretValuesPrinted, false);
|
|
assertNoFixtureSecrets(report);
|
|
});
|
|
|
|
test("cloud-api image provisioning entrypoint exposes non-secret source check", async () => {
|
|
const { stdout } = await execFileAsync(
|
|
bunCommand,
|
|
["cmd/hwlab-cloud-api/provision.ts", "--check"],
|
|
{
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
HWLAB_CLOUD_DB_URL: fixturePostgresUrl({ password: fixtureSecret("super") }),
|
|
HWLAB_CLOUD_DB_SSL_MODE: "disable"
|
|
}
|
|
}
|
|
);
|
|
const report = JSON.parse(stdout);
|
|
|
|
assert.equal(report.conclusion.status, "ready");
|
|
assert.equal(report.actions.liveDbReadAttempted, false);
|
|
assert.equal(report.actions.liveDbWriteAttempted, false);
|
|
assert.equal(report.db.targetRoleNamePresent, true);
|
|
assert.equal(report.db.targetDatabaseNamePresent, true);
|
|
assert.equal(report.db.targetPasswordPresent, true);
|
|
assert.equal(report.safety.secretValuesPrinted, false);
|
|
assertNoFixtureSecrets(report);
|
|
});
|
|
|
|
test("source check without injected DB URL is ready and performs no live access", async () => {
|
|
const report = await buildDevRuntimeProvisioningReport(parseArgs(["--check"]), {
|
|
env: {},
|
|
now: () => "2026-05-23T00:00:00.000Z"
|
|
});
|
|
|
|
assert.equal(report.conclusion.status, "ready");
|
|
assert.equal(report.actions.liveDbReadAttempted, false);
|
|
assert.equal(report.actions.liveDbWriteAttempted, false);
|
|
assert.equal(report.blockers.length, 0);
|
|
assert.equal(report.db.targetUrlPresent, false);
|
|
assert.equal(report.gates.role.status, "not_checked");
|
|
});
|
|
|
|
test("live dry-run classifies missing target role separately from missing database", async () => {
|
|
const report = await buildDevRuntimeProvisioningReport(
|
|
parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
|
|
{
|
|
env: envWithAdmin(),
|
|
adminClient: createAdminClient({ roleExists: false, databaseExists: true }),
|
|
queryClient: createRuntimeClient("auth"),
|
|
now: () => "2026-05-23T00:00:00.000Z"
|
|
}
|
|
);
|
|
|
|
assert.equal(report.conclusion.status, "blocked");
|
|
assert.equal(report.actions.adminInspectionAttempted, true);
|
|
assert.equal(report.actions.liveDbWriteAttempted, false);
|
|
assert.equal(report.provisioning.targetRole.exists, false);
|
|
assert.equal(report.provisioning.targetDatabase.exists, true);
|
|
assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-role"), true);
|
|
assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-database"), false);
|
|
assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-auth"), true);
|
|
assert.equal(
|
|
report.blockers.find((blocker) => blocker.scope === "runtime-provisioning-role").evidence.affectedRuntimeBlocker,
|
|
"runtime_durable_adapter_auth_blocked"
|
|
);
|
|
assert.equal(
|
|
report.blockers.find((blocker) => blocker.scope === "runtime-provisioning-auth").evidence.blocker,
|
|
"runtime_durable_adapter_auth_blocked"
|
|
);
|
|
assertNoFixtureSecrets(report);
|
|
});
|
|
|
|
test("live dry-run classifies missing target database separately from target role", async () => {
|
|
const report = await buildDevRuntimeProvisioningReport(
|
|
parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
|
|
{
|
|
env: envWithAdmin(),
|
|
adminClient: createAdminClient({ roleExists: true, databaseExists: false }),
|
|
queryClient: createRuntimeClient("auth"),
|
|
now: () => "2026-05-23T00:00:00.000Z"
|
|
}
|
|
);
|
|
|
|
assert.equal(report.conclusion.status, "blocked");
|
|
assert.equal(report.provisioning.targetRole.exists, true);
|
|
assert.equal(report.provisioning.targetDatabase.exists, false);
|
|
assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-role"), false);
|
|
assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-database"), true);
|
|
assert.equal(
|
|
report.blockers.find((blocker) => blocker.scope === "runtime-provisioning-database").evidence.affectedRuntimeBlocker,
|
|
"runtime_durable_adapter_auth_blocked"
|
|
);
|
|
});
|
|
|
|
test("live dry-run separates SSL from auth/schema/migration", async () => {
|
|
const report = await buildDevRuntimeProvisioningReport(
|
|
parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
|
|
{
|
|
env: envWithAdmin({ sslMode: "require" }),
|
|
adminClient: createAdminClient({ roleExists: true, databaseExists: true }),
|
|
queryClient: createRuntimeClient("ssl"),
|
|
now: () => "2026-05-23T00:00:00.000Z"
|
|
}
|
|
);
|
|
|
|
assert.equal(report.conclusion.status, "blocked");
|
|
assert.equal(report.gates.ssl.status, "blocked");
|
|
assert.equal(report.gates.auth.status, "not_checked");
|
|
assert.equal(report.gates.schema.status, "not_checked");
|
|
assert.equal(report.gates.migration.status, "not_checked");
|
|
assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-ssl"), true);
|
|
assert.equal(JSON.stringify(report).includes("does not support SSL"), false);
|
|
});
|
|
|
|
test("live dry-run leaves role/database ready while migration remains a downstream blocker", async () => {
|
|
const report = await buildDevRuntimeProvisioningReport(
|
|
parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
|
|
{
|
|
env: envWithAdmin(),
|
|
adminClient: createAdminClient({ roleExists: true, databaseExists: true }),
|
|
queryClient: createRuntimeClient("migration"),
|
|
now: () => "2026-05-23T00:00:00.000Z"
|
|
}
|
|
);
|
|
|
|
assert.equal(report.conclusion.status, "ready");
|
|
assert.equal(report.provisioning.ready, true);
|
|
assert.equal(report.provisioning.targetRole.ready, true);
|
|
assert.equal(report.provisioning.targetDatabase.ready, true);
|
|
assert.equal(report.runtime.blocker, "runtime_durable_adapter_migration_blocked");
|
|
assert.equal(report.gates.migration.status, "blocked");
|
|
assert.equal(report.blockers.length, 0);
|
|
});
|
|
|
|
test("apply creates missing role/database and grants target privileges without printing secrets", async () => {
|
|
const adminClient = createAdminClient({ roleExists: false, databaseExists: false });
|
|
const targetAdminClient = createAdminClient({ roleExists: true, databaseExists: true });
|
|
const report = await buildDevRuntimeProvisioningReport(
|
|
parseArgs(["--apply", "--confirm-dev", "--confirmed-non-production"]),
|
|
{
|
|
env: envWithAdmin(),
|
|
adminClient,
|
|
queryClient: createRuntimeClient("ready"),
|
|
now: () => "2026-05-23T00:00:00.000Z",
|
|
adminDbUrlOverride: null,
|
|
pgModuleLoader: async () => {
|
|
throw new Error("pgModuleLoader should not be used when adminClient is injected");
|
|
}
|
|
}
|
|
);
|
|
|
|
assert.equal(targetAdminClient.calls.length, 0);
|
|
assert.equal(report.conclusion.status, "ready");
|
|
assert.equal(report.actions.roleCreated, true);
|
|
assert.equal(report.actions.databaseCreated, true);
|
|
assert.equal(report.actions.rolePasswordSynchronized, true);
|
|
assert.equal(report.actions.databaseConnectGranted, true);
|
|
assert.equal(report.actions.schemaPrivilegesGranted, true);
|
|
assert.equal(report.provisioning.ready, true);
|
|
assert.ok(adminClient.calls.some((call) => call.sql.startsWith("CREATE ROLE ")));
|
|
assert.ok(adminClient.calls.some((call) => call.sql.startsWith("CREATE DATABASE ")));
|
|
assert.ok(adminClient.calls.some((call) => call.sql.startsWith("GRANT CONNECT ON DATABASE ")));
|
|
assert.ok(adminClient.calls.some((call) => call.sql.startsWith("GRANT USAGE, CREATE ON SCHEMA public ")));
|
|
assertNoFixtureSecrets(report);
|
|
});
|
|
|
|
function envWithAdmin({ sslMode = "disable" } = {}) {
|
|
return {
|
|
HWLAB_CLOUD_DB_URL: fixturePostgresUrl({ password: fixtureSecret("app") }),
|
|
HWLAB_CLOUD_DB_ADMIN_URL: fixturePostgresUrl({
|
|
user: "postgres",
|
|
password: fixtureSecret("admin"),
|
|
database: "postgres"
|
|
}),
|
|
HWLAB_CLOUD_DB_SSL_MODE: sslMode
|
|
};
|
|
}
|
|
|
|
function fixturePostgresUrl({
|
|
user = "hwlab_app",
|
|
password = fixtureSecret("app"),
|
|
host = "db.example.test",
|
|
database = "hwlab_runtime"
|
|
} = {}) {
|
|
return `${["postgresql", "://"].join("")}${user}:${password}@${host}:5432/${database}`;
|
|
}
|
|
|
|
function fixtureSecret(kind) {
|
|
return `${kind}-${"secret"}`;
|
|
}
|
|
|
|
function assertNoFixtureSecrets(value) {
|
|
const text = JSON.stringify(value);
|
|
for (const forbidden of [
|
|
fixtureSecret("super"),
|
|
fixtureSecret("app"),
|
|
fixtureSecret("admin"),
|
|
"db.example.test",
|
|
"hwlab_app",
|
|
"hwlab_runtime"
|
|
]) {
|
|
assert.equal(text.includes(forbidden), false);
|
|
}
|
|
}
|
|
|
|
function createAdminClient({ roleExists, databaseExists }) {
|
|
const state = {
|
|
roleExists,
|
|
databaseExists,
|
|
calls: []
|
|
};
|
|
return {
|
|
get calls() {
|
|
return state.calls;
|
|
},
|
|
async query(sql, params = []) {
|
|
state.calls.push({ sql, params });
|
|
if (sql.includes("pg_catalog.pg_roles")) {
|
|
return { rows: state.roleExists ? [{ present: 1 }] : [] };
|
|
}
|
|
if (sql.includes("pg_catalog.pg_database")) {
|
|
return { rows: state.databaseExists ? [{ present: 1 }] : [] };
|
|
}
|
|
if (sql.startsWith("CREATE ROLE ")) {
|
|
state.roleExists = true;
|
|
return { rows: [] };
|
|
}
|
|
if (sql.startsWith("ALTER ROLE ")) {
|
|
return { rows: [] };
|
|
}
|
|
if (sql.startsWith("CREATE DATABASE ")) {
|
|
state.databaseExists = true;
|
|
return { rows: [] };
|
|
}
|
|
if (sql.startsWith("GRANT CONNECT ON DATABASE ") || sql.startsWith("GRANT USAGE, CREATE ON SCHEMA public ")) {
|
|
return { rows: [] };
|
|
}
|
|
throw new Error(`unexpected admin query: ${sql}`);
|
|
}
|
|
};
|
|
}
|
|
|
|
function createRuntimeClient(mode) {
|
|
return {
|
|
async query(sql) {
|
|
if (sql.includes("information_schema.columns")) {
|
|
if (mode === "ssl") {
|
|
const error = new Error("fixture SSL blocked: server does not support SSL");
|
|
error.code = "08P01";
|
|
throw error;
|
|
}
|
|
if (mode === "auth") {
|
|
const error = new Error("fixture auth blocked");
|
|
error.code = "28P01";
|
|
throw error;
|
|
}
|
|
return { rows: schemaRows() };
|
|
}
|
|
if (sql.startsWith("SELECT id, schema_version FROM hwlab_schema_migrations")) {
|
|
if (mode === "migration") return { rows: [] };
|
|
return { rows: [{ id: "0001_cloud_core_skeleton", schema_version: "runtime-durable-postgres-v1" }] };
|
|
}
|
|
if (sql.startsWith("SELECT COUNT(*)::int AS count FROM ")) {
|
|
return { rows: [{ count: 0 }] };
|
|
}
|
|
throw new Error(`unexpected runtime query: ${sql}`);
|
|
}
|
|
};
|
|
}
|
|
|
|
function schemaRows() {
|
|
const schema = {
|
|
gateway_sessions: ["id", "project_id", "gateway_service_id", "status", "started_at", "ended_at", "gateway_session_json"],
|
|
box_resources: ["id", "project_id", "gateway_session_id", "resource_state", "labels_json", "resource_json", "updated_at"],
|
|
box_capabilities: ["id", "box_resource_id", "capability_type", "capability_json", "updated_at"],
|
|
hardware_operations: ["id", "project_id", "requested_by", "operation_type", "operation_json", "status", "requested_at", "updated_at"],
|
|
audit_events: ["id", "request_id", "actor", "source", "operation", "target", "result", "timestamp", "event_json"],
|
|
evidence_records: ["id", "project_id", "operation_id", "evidence_type", "uri", "metadata_json", "created_at"]
|
|
};
|
|
return Object.entries(schema).flatMap(([table, columns]) =>
|
|
columns.map((column) => ({ table_name: table, column_name: column }))
|
|
);
|
|
}
|