103 lines
5.1 KiB
JavaScript
103 lines
5.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { describe, it } from "vitest";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const rootDir = path.resolve(__dirname, "..");
|
|
|
|
const smokeScripts = [
|
|
"scripts/docker-web-runtime-smoke.sh",
|
|
"scripts/docker-api-runtime-smoke.sh",
|
|
"scripts/docker-worker-runtime-smoke.sh",
|
|
];
|
|
|
|
describe("Docker runtime smoke contract", () => {
|
|
it.each(smokeScripts)("reads the expected revision from the built image label: %s", (relativePath) => {
|
|
const source = fs.readFileSync(path.join(rootDir, relativePath), "utf8");
|
|
|
|
assert.match(source, /docker image inspect -f '\{\{ index \.Config\.Labels "org\.opencontainers\.image\.revision" \}\}' "\$IMAGE"/);
|
|
assert.match(source, /SMOKE_REVISION="\$\(resolve_smoke_revision\)"/);
|
|
});
|
|
|
|
it.each(smokeScripts)("rejects Dockerfile's default unknown revision: %s", (relativePath) => {
|
|
const source = fs.readFileSync(path.join(rootDir, relativePath), "utf8");
|
|
|
|
assert.match(source, /\$SMOKE_REVISION" == "unknown"/);
|
|
assert.match(source, /does not expose a concrete org\.opencontainers\.image\.revision/);
|
|
});
|
|
|
|
it.each(smokeScripts)("does not override APP_REVISION at container runtime: %s", (relativePath) => {
|
|
const source = fs.readFileSync(path.join(rootDir, relativePath), "utf8");
|
|
|
|
assert.doesNotMatch(source, /-e APP_REVISION=/);
|
|
});
|
|
|
|
it.each(["scripts/docker-web-runtime-smoke.sh", "scripts/docker-api-runtime-smoke.sh"])("validates the runtime OpenAPI JSON contract: %s", (relativePath) => {
|
|
const source = fs.readFileSync(path.join(rootDir, relativePath), "utf8");
|
|
|
|
assert.match(source, /expect_openapi_json/);
|
|
assert.match(source, /\/v1\/openapi\.json/);
|
|
assert.match(source, /payload\.paths\["\/v1\/session\/current-user"\]/);
|
|
assert.doesNotMatch(source, /payload\.paths\["\/v1\/workflows\/run\/\{id\}"\]/);
|
|
});
|
|
|
|
it("boots the web image through the entrypoint migration path (MIGRATE_ON_BOOT/SEED_ON_BOOT)", () => {
|
|
const source = fs.readFileSync(path.join(rootDir, "scripts/docker-web-runtime-smoke.sh"), "utf8");
|
|
|
|
assert.match(source, /-e MIGRATE_ON_BOOT=true/);
|
|
assert.match(source, /-e SEED_ON_BOOT=true/);
|
|
assert.match(source, /MIGRATE_ON_BOOT migration banner/);
|
|
assert.match(source, /SEED_ON_BOOT seed banner/);
|
|
// Migrations must not run in a separate one-off container anymore — the
|
|
// smoke rehearses the exact Coolify boot contract of the web resource.
|
|
assert.doesNotMatch(source, /--entrypoint \/bin\/sh/);
|
|
assert.doesNotMatch(source, /DATABASE_URL="\$HOST_DATABASE_URL" pnpm exec prisma migrate deploy/);
|
|
});
|
|
|
|
it("runs the API smoke migration inside the final API image network", () => {
|
|
const source = fs.readFileSync(path.join(rootDir, "scripts/docker-api-runtime-smoke.sh"), "utf8");
|
|
|
|
assert.match(source, /docker run --rm --network "\$NETWORK_NAME"/);
|
|
assert.match(source, /-e DATABASE_URL="\$CONTAINER_DATABASE_URL"/);
|
|
assert.match(source, /--entrypoint \/bin\/sh/);
|
|
assert.match(source, /pnpm exec prisma migrate deploy --config prisma\.config\.ts/);
|
|
assert.doesNotMatch(source, /HOST_DATABASE_URL/);
|
|
assert.doesNotMatch(source, /DATABASE_URL="\$HOST_DATABASE_URL" pnpm exec prisma migrate deploy/);
|
|
});
|
|
|
|
it("runs the worker smoke migration inside the final worker image network", () => {
|
|
const source = fs.readFileSync(path.join(rootDir, "scripts/docker-worker-runtime-smoke.sh"), "utf8");
|
|
|
|
assert.match(source, /docker run --rm --network "\$NETWORK_NAME"/);
|
|
assert.match(source, /-e DATABASE_URL="\$CONTAINER_DATABASE_URL"/);
|
|
assert.match(source, /--entrypoint \/bin\/sh/);
|
|
assert.match(source, /pnpm exec prisma migrate deploy --config prisma\.config\.ts/);
|
|
assert.doesNotMatch(source, /HOST_DATABASE_URL/);
|
|
assert.doesNotMatch(source, /DATABASE_URL="\$HOST_DATABASE_URL" pnpm exec prisma migrate deploy/);
|
|
});
|
|
|
|
it("proves worker queue-group routing for general and mail workers", () => {
|
|
const source = fs.readFileSync(path.join(rootDir, "scripts/docker-worker-runtime-smoke.sh"), "utf8");
|
|
|
|
assert.match(source, /-e WORKER_QUEUE_GROUP=general/);
|
|
assert.match(source, /-e WORKER_QUEUE_GROUP=mail/);
|
|
assert.match(source, /expect_worker_health "\$enabled_health_output" ready true general hivegrid\.notifications/);
|
|
assert.match(source, /expect_worker_health "\$mail_health_output" ready true mail hivegrid\.email/);
|
|
assert.match(source, /report\.workerGroup !== expectedGroup/);
|
|
assert.match(source, /report\.queues\.includes\(expectedQueue\)/);
|
|
});
|
|
|
|
it("boots the MCP image with a token hash secret so the production fail-closed guard is satisfied", () => {
|
|
// The MCP runtime refuses to boot in production without a token hash secret
|
|
// (security audit #7). The smoke runs NODE_ENV=production, so it must supply
|
|
// one or the container exits before the health check.
|
|
const source = fs.readFileSync(path.join(rootDir, "scripts/docker-mcp-runtime-smoke.sh"), "utf8");
|
|
|
|
assert.match(source, /-e NODE_ENV=production/);
|
|
assert.match(source, /-e MCP_OAUTH_TOKEN_HASH_SECRET=/);
|
|
});
|
|
});
|