187 lines
6.9 KiB
JavaScript
187 lines
6.9 KiB
JavaScript
import { EventEmitter } from "node:events";
|
|
import assert from "node:assert/strict";
|
|
import { describe, it } from "vitest";
|
|
|
|
import {
|
|
buildMigrateStatusCommand,
|
|
formatMigrateStatusResult,
|
|
redactSecrets,
|
|
verifyDbMigrateStatus,
|
|
} from "./verify-db-migrate-status.mjs";
|
|
|
|
function createSpawnStub({ exitCode = 0, stdout = "", stderr = "", error = null } = {}) {
|
|
const calls = [];
|
|
const spawnImpl = (command, args, options) => {
|
|
calls.push({ command, args, options });
|
|
|
|
if (error?.throwSync) {
|
|
throw error.value;
|
|
}
|
|
|
|
const child = new EventEmitter();
|
|
child.stdout = new EventEmitter();
|
|
child.stderr = new EventEmitter();
|
|
|
|
queueMicrotask(() => {
|
|
if (stdout) {
|
|
child.stdout.emit("data", Buffer.from(stdout));
|
|
}
|
|
if (stderr) {
|
|
child.stderr.emit("data", Buffer.from(stderr));
|
|
}
|
|
if (error) {
|
|
child.emit("error", error.value ?? error);
|
|
} else {
|
|
child.emit("close", exitCode);
|
|
}
|
|
});
|
|
|
|
return child;
|
|
};
|
|
|
|
return { spawnImpl, calls };
|
|
}
|
|
|
|
const SECRET_URL = "postgresql://ci_user:super-secret@db.example.test:5432/toolctrl";
|
|
|
|
describe("verify-db-migrate-status", () => {
|
|
it("constructs the package-owned Prisma migrate status command", () => {
|
|
const command = buildMigrateStatusCommand();
|
|
|
|
assert.equal(command.command, "pnpm");
|
|
assert.deepEqual(command.args, ["--filter", "@toolctrl/db", "prisma:migrate:status"]);
|
|
assert.equal(command.display, "pnpm --filter @toolctrl/db prisma:migrate:status");
|
|
});
|
|
|
|
it("passes when Prisma migrate status exits successfully", async () => {
|
|
const { spawnImpl, calls } = createSpawnStub({ stdout: "Database schema is up to date\n", exitCode: 0 });
|
|
|
|
const result = await verifyDbMigrateStatus({
|
|
env: { DATABASE_URL: SECRET_URL, PATH: "/bin" },
|
|
spawnImpl,
|
|
cwd: "/repo",
|
|
});
|
|
const output = formatMigrateStatusResult(result);
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.phase, "prisma:migrate:status");
|
|
assert.equal(result.exitCode, 0);
|
|
assert.equal(calls.length, 1);
|
|
assert.equal(calls[0].command, "pnpm");
|
|
assert.deepEqual(calls[0].args, ["--filter", "@toolctrl/db", "prisma:migrate:status"]);
|
|
assert.equal(calls[0].options.cwd, "/repo");
|
|
assert.equal(calls[0].options.env.CI, "1");
|
|
assert.equal(calls[0].options.env.DATABASE_URL, SECRET_URL);
|
|
assert.match(output, /^PASS database migrate status verification/);
|
|
assert.match(output, /phase: prisma:migrate:status/);
|
|
assert.match(output, /command: pnpm --filter @toolctrl\/db prisma:migrate:status/);
|
|
assert.match(output, /exitCode: 0/);
|
|
});
|
|
|
|
it("fails before spawning when DATABASE_URL is missing", async () => {
|
|
const { spawnImpl, calls } = createSpawnStub();
|
|
|
|
const result = await verifyDbMigrateStatus({ env: { PATH: "/bin" }, spawnImpl });
|
|
const output = formatMigrateStatusResult(result);
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.phase, "preflight");
|
|
assert.equal(calls.length, 0);
|
|
assert.match(output, /^FAIL database migrate status verification/);
|
|
assert.match(output, /status: missing DATABASE_URL/);
|
|
assert.match(output, /DATABASE_URL is required/);
|
|
});
|
|
|
|
it("fails with sanitized output when Prisma migrate status exits non-zero", async () => {
|
|
const { spawnImpl } = createSpawnStub({
|
|
exitCode: 1,
|
|
stdout: `Connecting with DATABASE_URL=${SECRET_URL}\n`,
|
|
stderr: `Migration failed for ${SECRET_URL}\n`,
|
|
});
|
|
|
|
const result = await verifyDbMigrateStatus({ env: { DATABASE_URL: SECRET_URL }, spawnImpl });
|
|
const output = formatMigrateStatusResult(result);
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.exitCode, 1);
|
|
assert.equal(result.status, "prisma migrate status failed");
|
|
assert.match(output, /^FAIL database migrate status verification/);
|
|
assert.match(output, /exitCode: 1/);
|
|
assert.doesNotMatch(output, /super-secret/);
|
|
assert.doesNotMatch(output, new RegExp(SECRET_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
assert.match(output, /<redacted:DATABASE_URL>/);
|
|
});
|
|
|
|
it("passes unreachable database failures only when explicitly allowed", async () => {
|
|
const { spawnImpl } = createSpawnStub({
|
|
exitCode: 1,
|
|
stderr: "Error: P1001: Can't reach database server at `db.internal:5432`\n",
|
|
});
|
|
|
|
const result = await verifyDbMigrateStatus({
|
|
env: {
|
|
DATABASE_URL: SECRET_URL,
|
|
DB_MIGRATE_STATUS_ALLOW_UNREACHABLE: "1",
|
|
},
|
|
spawnImpl,
|
|
});
|
|
const output = formatMigrateStatusResult(result);
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.exitCode, 1);
|
|
assert.equal(result.status, "database unreachable; skipped by DB_MIGRATE_STATUS_ALLOW_UNREACHABLE");
|
|
assert.match(output, /^PASS database migrate status verification/);
|
|
});
|
|
|
|
it("still fails unreachable database errors without the explicit allow flag", async () => {
|
|
const { spawnImpl } = createSpawnStub({
|
|
exitCode: 1,
|
|
stderr: "Error: P1001: Can't reach database server at `db.internal:5432`\n",
|
|
});
|
|
|
|
const result = await verifyDbMigrateStatus({ env: { DATABASE_URL: SECRET_URL }, spawnImpl });
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.status, "prisma migrate status failed");
|
|
});
|
|
|
|
it("fails with spawn diagnostics when command execution errors", async () => {
|
|
const { spawnImpl } = createSpawnStub({ error: { value: new Error(`spawn failed for ${SECRET_URL}`) } });
|
|
|
|
const result = await verifyDbMigrateStatus({ env: { DATABASE_URL: SECRET_URL }, spawnImpl });
|
|
const output = formatMigrateStatusResult(result);
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.phase, "spawn");
|
|
assert.equal(result.exitCode, null);
|
|
assert.match(output, /status: spawn error/);
|
|
assert.match(output, /error: spawn failed for <redacted:DATABASE_URL>/);
|
|
assert.doesNotMatch(output, /super-secret/);
|
|
});
|
|
|
|
it("redacts literal DATABASE_URL values and generic URL credentials", () => {
|
|
const output = redactSecrets(
|
|
`DATABASE_URL=${SECRET_URL}\njson={"DATABASE_URL":"${SECRET_URL}"}\nother=mysql://user:password@localhost:3306/app`,
|
|
{ DATABASE_URL: SECRET_URL },
|
|
);
|
|
|
|
assert.doesNotMatch(output, /super-secret/);
|
|
assert.doesNotMatch(output, /password/);
|
|
assert.doesNotMatch(output, /ci_user/);
|
|
assert.match(output, /DATABASE_URL=<redacted:DATABASE_URL>/);
|
|
assert.match(output, /mysql:\/\/<redacted>@localhost/);
|
|
});
|
|
|
|
it("captures synchronous spawn failures as sanitized failures", async () => {
|
|
const { spawnImpl } = createSpawnStub({ error: { throwSync: true, value: new Error(`ENOENT ${SECRET_URL}`) } });
|
|
|
|
const result = await verifyDbMigrateStatus({ env: { DATABASE_URL: SECRET_URL }, spawnImpl });
|
|
const output = formatMigrateStatusResult(result);
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.phase, "spawn");
|
|
assert.match(output, /ENOENT <redacted:DATABASE_URL>/);
|
|
assert.doesNotMatch(output, /super-secret/);
|
|
});
|
|
});
|