3064 lines
119 KiB
JavaScript
3064 lines
119 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { spawn } from "node:child_process";
|
|
import http from "node:http";
|
|
import path from "node:path";
|
|
import { describe, it } from "vitest";
|
|
|
|
const scriptPath = path.resolve("scripts/coolify-deploy.mjs");
|
|
const bearerToken = "coolify-test-token-sentinel";
|
|
const deployRevision = "a".repeat(40);
|
|
const resourceUuid = "r".repeat(24);
|
|
const otherResourceUuid = "s".repeat(24);
|
|
const deploymentUuid = "d".repeat(24);
|
|
const otherDeploymentUuid = "e".repeat(24);
|
|
const mcpResourceUuid = "m".repeat(24);
|
|
const apiResourceUuid = "a".repeat(24);
|
|
const hocuspocusResourceUuid = "h".repeat(24);
|
|
const portalDomainControllerResourceUuid = "c".repeat(24);
|
|
const workerGroupResourceUuids = Object.freeze({
|
|
general: "g".repeat(24),
|
|
mail: "l".repeat(24),
|
|
import: "i".repeat(24),
|
|
webhook: "v".repeat(24),
|
|
plugin: "q".repeat(24),
|
|
});
|
|
const workerResourceUuid = "w".repeat(24);
|
|
const legacyMcpResourceUuid = "c".repeat(24);
|
|
const webResourceUuid = "b".repeat(24);
|
|
const adminResourceUuid = "n".repeat(24);
|
|
const portalResourceUuid = "p".repeat(24);
|
|
const adminDeploymentUuid = "f".repeat(24);
|
|
const mcpImage = "registry.example.test/toolctrl-labs/hivegrid/mcp";
|
|
const workerImage = "registry.example.test/toolctrl-labs/hivegrid/worker";
|
|
const webImage = "registry.example.test/toolctrl-labs/hivegrid/web";
|
|
const apiImage = "registry.example.test/toolctrl-labs/hivegrid/api";
|
|
const hocuspocusImage = "registry.example.test/toolctrl-labs/hivegrid/hocuspocus";
|
|
const portalDomainControllerImage = "registry.example.test/toolctrl-labs/hivegrid/portal-domain-controller";
|
|
const webDigest = `sha256:${"f".repeat(64)}`;
|
|
const sharedCryptoSecret = "shared-tenant-envelope-secret-sentinel";
|
|
const sharedAdminConnectionMasterKey = "shared-admin-connection-master-key-sentinel";
|
|
const gdprExportSseCKey = Buffer.alloc(32, 7).toString("base64");
|
|
const alternateGdprExportSseCKey = Buffer.alloc(32, 8).toString("base64");
|
|
const workerRedisSecrets = Object.freeze(
|
|
Object.fromEntries(Object.keys(workerGroupResourceUuids).map((group) => [group, `worker-${group}-redis-secret-sentinel`]))
|
|
);
|
|
const gdprExportStorageEnv = Object.freeze({
|
|
STORAGE_ENDPOINT: "https://storage.example.test",
|
|
STORAGE_REGION: "eu-central-1",
|
|
STORAGE_BUCKET: "private-exports",
|
|
STORAGE_ACCESS_KEY_ID: "storage-access-key-sentinel",
|
|
STORAGE_SECRET_ACCESS_KEY: "storage-secret-key-sentinel",
|
|
STORAGE_FORCE_PATH_STYLE: "false",
|
|
GDPR_EXPORT_SSE_C_KEY_BASE64: gdprExportSseCKey,
|
|
});
|
|
const portalGdprExportStorageEnv = Object.freeze({
|
|
...gdprExportStorageEnv,
|
|
STORAGE_ACCESS_KEY_ID: "portal-storage-access-key-sentinel",
|
|
STORAGE_SECRET_ACCESS_KEY: "portal-storage-secret-key-sentinel",
|
|
});
|
|
const importGdprExportStorageEnv = Object.freeze({
|
|
...gdprExportStorageEnv,
|
|
STORAGE_ACCESS_KEY_ID: "import-storage-access-key-sentinel",
|
|
STORAGE_SECRET_ACCESS_KEY: "import-storage-secret-key-sentinel",
|
|
});
|
|
const GDPR_EXPORT_STORAGE_TEST_KEYS = new Set(Object.keys(gdprExportStorageEnv));
|
|
const gdprExportRetentionStorageEnv = Object.freeze(
|
|
Object.fromEntries(
|
|
Object.entries({
|
|
...gdprExportStorageEnv,
|
|
STORAGE_ACCESS_KEY_ID: "general-storage-access-key-sentinel",
|
|
STORAGE_SECRET_ACCESS_KEY: "general-storage-secret-key-sentinel",
|
|
}).filter(([key]) => key !== "GDPR_EXPORT_SSE_C_KEY_BASE64")
|
|
)
|
|
);
|
|
const validRuntimeResourceLimits = Object.freeze({ limits_cpus: ".5", limits_memory: "512m" });
|
|
const validWebSurfaceResourceLimits = validRuntimeResourceLimits;
|
|
const vapidSubject = "mailto:ops@hivegrid.de";
|
|
const vapidPublicKey = (() => {
|
|
const bytes = Buffer.alloc(65, 1);
|
|
bytes[0] = 0x04;
|
|
return bytes.toString("base64url");
|
|
})();
|
|
const alternateVapidPublicKey = (() => {
|
|
const bytes = Buffer.alloc(65, 3);
|
|
bytes[0] = 0x04;
|
|
return bytes.toString("base64url");
|
|
})();
|
|
const vapidPrivateKey = Buffer.alloc(32, 2).toString("base64url");
|
|
|
|
function portalRuntimeEnv(overrides = {}) {
|
|
const values = {
|
|
DATABASE_URL: "postgres://hivegrid_portal:portal-secret-sentinel@db.example.test:6432/hivegrid",
|
|
DATABASE_READ_URL: "postgres://hivegrid_portal:portal-read-secret-sentinel@db.example.test/hivegrid",
|
|
DATABASE_RUNTIME_ROLE: "hivegrid_portal",
|
|
DATABASE_POOL_MAX: "10",
|
|
RLS_RUNTIME_MODE: "required",
|
|
WEB_RUNTIME_SURFACE: "portal",
|
|
PORTAL_SESSION_SECRET: "dedicated-portal-session-secret-sentinel",
|
|
CAPTCHA_PROVIDER: "mcaptcha",
|
|
MCAPTCHA_INSTANCE_URL: "https://captcha.example.test",
|
|
MCAPTCHA_SITEKEY_LOGIN: "portal-login-sitekey",
|
|
MCAPTCHA_ACCOUNT_SECRET: "portal-mcaptcha-secret-sentinel",
|
|
REDIS_URL: "rediss://portal-redis-secret-sentinel@redis.example.test:6380/3",
|
|
...overrides,
|
|
};
|
|
return Object.entries(values)
|
|
.filter(([, value]) => value !== undefined)
|
|
.map(([key, value]) => ({ key, value }));
|
|
}
|
|
|
|
function runtimeDatabaseEnv(role, overrides = {}) {
|
|
const values = {
|
|
DATABASE_URL: `postgres://${role}:runtime-secret-sentinel@db.example.test:6432/hivegrid`,
|
|
DATABASE_READ_URL: `postgres://${role}:read-secret-sentinel@db.example.test/hivegrid`,
|
|
DATABASE_RUNTIME_ROLE: role,
|
|
DATABASE_POOL_MAX: "10",
|
|
RLS_RUNTIME_MODE: "required",
|
|
...overrides,
|
|
};
|
|
return Object.entries(values)
|
|
.filter(([, value]) => value !== undefined)
|
|
.map(([key, value]) => ({ key, value }));
|
|
}
|
|
|
|
function runtimePreflightFixture() {
|
|
const apps = [
|
|
{
|
|
uuid: webResourceUuid,
|
|
name: "hivegrid-web",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validRuntimeResourceLimits,
|
|
},
|
|
{
|
|
uuid: portalResourceUuid,
|
|
name: "hivegrid-portal",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validRuntimeResourceLimits,
|
|
},
|
|
{
|
|
uuid: adminResourceUuid,
|
|
name: "hivegrid-admin",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validRuntimeResourceLimits,
|
|
},
|
|
{ uuid: apiResourceUuid, name: "hivegrid-api", docker_registry_image_name: apiImage, ...validRuntimeResourceLimits },
|
|
{ uuid: mcpResourceUuid, name: "hivegrid-mcp", docker_registry_image_name: mcpImage, ...validRuntimeResourceLimits },
|
|
{ uuid: hocuspocusResourceUuid, name: "hivegrid-hocuspocus", docker_registry_image_name: hocuspocusImage, ...validRuntimeResourceLimits },
|
|
{
|
|
uuid: portalDomainControllerResourceUuid,
|
|
name: "hivegrid-portal-domain-controller",
|
|
docker_registry_image_name: portalDomainControllerImage,
|
|
...validRuntimeResourceLimits,
|
|
},
|
|
...Object.entries(workerGroupResourceUuids).map(([group, uuid]) => ({
|
|
uuid,
|
|
name: `worker-${group}`,
|
|
docker_registry_image_name: workerImage,
|
|
...validRuntimeResourceLimits,
|
|
})),
|
|
];
|
|
const envByUuid = new Map([
|
|
[
|
|
webResourceUuid,
|
|
runtimeDatabaseEnv("hivegrid_app", {
|
|
WEB_RUNTIME_SURFACE: "core",
|
|
PORTAL_APP_HOST: "portal.example.test",
|
|
APP_BASE_DOMAIN: "example.test",
|
|
WEB_APP_HOST: "app.example.test",
|
|
ADMIN_APP_HOST: "admin.example.test",
|
|
API_HOST: "api.example.test",
|
|
CRYPTO_SECRET: sharedCryptoSecret,
|
|
ADMIN_CONNECTION_MASTER_KEY: sharedAdminConnectionMasterKey,
|
|
REDIS_URL: "rediss://core-redis-secret-sentinel@redis.example.test:6380/2",
|
|
WEB_PUSH_VAPID_PUBLIC_KEY: vapidPublicKey,
|
|
...gdprExportStorageEnv,
|
|
}),
|
|
],
|
|
[
|
|
adminResourceUuid,
|
|
runtimeDatabaseEnv("hivegrid_app", {
|
|
WEB_RUNTIME_SURFACE: "admin",
|
|
SESSION_SECRET: "dedicated-admin-session-secret-sentinel",
|
|
ADMIN_FALLBACK_LOGIN_ENABLED: "false",
|
|
AUTHENTIK_ISSUER_URL: "https://auth.example.test/application/o/hivegrid-admin/",
|
|
AUTHENTIK_ADMIN_CLIENT_ID: "hivegrid-admin",
|
|
AUTHENTIK_ADMIN_CLIENT_SECRET: "authentik-admin-client-secret-sentinel",
|
|
AUTHENTIK_ADMIN_REDIRECT_URI: "https://admin.example.test/auth/admin/callback",
|
|
AUTHENTIK_ADMIN_JIT_DATABASE_URL: "postgres://hivegrid_authentik_jit:jit-secret-sentinel@db.example.test/hivegrid",
|
|
PORTAL_APP_HOST: "portal.example.test",
|
|
APP_BASE_DOMAIN: "example.test",
|
|
WEB_APP_HOST: "app.example.test",
|
|
ADMIN_APP_HOST: "admin.example.test",
|
|
API_HOST: "api.example.test",
|
|
ADMIN_CONNECTION_MASTER_KEY: sharedAdminConnectionMasterKey,
|
|
REDIS_URL: "rediss://admin-redis-secret-sentinel@redis.example.test:6380/5",
|
|
...gdprExportStorageEnv,
|
|
}),
|
|
],
|
|
[
|
|
portalResourceUuid,
|
|
portalRuntimeEnv({
|
|
DATABASE_POOL_MAX: "6",
|
|
PORTAL_APP_HOST: "https://Portal.Example.Test:443",
|
|
APP_PROTOCOL: "https",
|
|
...portalGdprExportStorageEnv,
|
|
}),
|
|
],
|
|
[
|
|
apiResourceUuid,
|
|
runtimeDatabaseEnv("hivegrid_app", {
|
|
ADMIN_CONNECTION_MASTER_KEY: sharedAdminConnectionMasterKey,
|
|
}),
|
|
],
|
|
[mcpResourceUuid, runtimeDatabaseEnv("hivegrid_app")],
|
|
[hocuspocusResourceUuid, runtimeDatabaseEnv("hivegrid_app")],
|
|
...Object.entries(workerGroupResourceUuids).map(([group, uuid]) => [
|
|
uuid,
|
|
runtimeDatabaseEnv("hivegrid_worker", {
|
|
WORKER_QUEUE_GROUP: group,
|
|
ADMIN_CONNECTION_MASTER_KEY: sharedAdminConnectionMasterKey,
|
|
BULLMQ_REDIS_URL: `rediss://${workerRedisSecrets[group]}@redis.example.test:6380/4`,
|
|
...(group === "mail" ? { PORTAL_APP_HOST: "portal.example.test:443" } : {}),
|
|
...(group === "mail" ? { APP_PROTOCOL: "https" } : {}),
|
|
...(group === "mail" ? { CRYPTO_SECRET: sharedCryptoSecret } : {}),
|
|
...(group === "general"
|
|
? {
|
|
...gdprExportRetentionStorageEnv,
|
|
MEILI_URL: "http://meili.internal.test:7700",
|
|
MEILI_API_KEY: "hivegrid-scoped-meili-key-sentinel",
|
|
EXPORT_RETENTION_QUEUE_ENABLED: "true",
|
|
}
|
|
: {}),
|
|
...(group === "import"
|
|
? {
|
|
...importGdprExportStorageEnv,
|
|
IMPORT_QUEUE_ENABLED: "false",
|
|
IMPORT_QUEUE_RUNTIME_READY: "false",
|
|
PORTAL_GDPR_EXPORT_QUEUE_ENABLED: "true",
|
|
}
|
|
: {}),
|
|
...(group === "webhook"
|
|
? {
|
|
WEB_PUSH_VAPID_SUBJECT: vapidSubject,
|
|
WEB_PUSH_VAPID_PUBLIC_KEY: vapidPublicKey,
|
|
WEB_PUSH_VAPID_PRIVATE_KEY: vapidPrivateKey,
|
|
}
|
|
: {}),
|
|
}),
|
|
]),
|
|
]);
|
|
return {
|
|
apps,
|
|
envByUuid,
|
|
allowedCsv: apps.map(({ uuid }) => uuid).join(","),
|
|
};
|
|
}
|
|
|
|
function runRuntimePreflight(serverUrl, allowedCsv) {
|
|
return runHelper(serverUrl, "assert-runtime-preflight", allowedCsv, webImage, apiImage, mcpImage, hocuspocusImage, workerImage);
|
|
}
|
|
|
|
function webPushEnvPayload(requestUrl, overrides = {}) {
|
|
if (requestUrl === `/applications/${webResourceUuid}/envs`) {
|
|
return overrides.web ?? [{ key: "WEB_PUSH_VAPID_PUBLIC_KEY", value: vapidPublicKey }];
|
|
}
|
|
if (requestUrl === `/applications/${workerResourceUuid}/envs`) {
|
|
return (
|
|
overrides.worker ?? [
|
|
{ key: "WEB_PUSH_VAPID_SUBJECT", value: vapidSubject },
|
|
{ key: "WEB_PUSH_VAPID_PUBLIC_KEY", value: vapidPublicKey },
|
|
{ key: "WEB_PUSH_VAPID_PRIVATE_KEY", value: vapidPrivateKey },
|
|
]
|
|
);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function expectedMcpHealthConfig() {
|
|
return {
|
|
docker_registry_image_tag: deployRevision,
|
|
health_check_enabled: true,
|
|
health_check_type: "cmd",
|
|
health_check_command: `node apps/mcp/dist/deploy-health.js ${deployRevision}`,
|
|
health_check_interval: 30,
|
|
health_check_timeout: 10,
|
|
health_check_retries: 3,
|
|
health_check_start_period: 20,
|
|
};
|
|
}
|
|
|
|
function runHelperWithEnv(serverUrl, envOverrides, ...args) {
|
|
return new Promise((resolve) => {
|
|
const child = spawn(process.execPath, [scriptPath, ...args], {
|
|
env: {
|
|
PATH: process.env.PATH || "",
|
|
NODE_ENV: "test",
|
|
SELECTED_COOLIFY_API_URL: serverUrl,
|
|
SELECTED_COOLIFY_TOKEN: bearerToken,
|
|
MCP_IMAGE: mcpImage,
|
|
WORKER_IMAGE: workerImage,
|
|
...envOverrides,
|
|
},
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.on("close", (status) => {
|
|
resolve({ status, stdout, stderr });
|
|
});
|
|
});
|
|
}
|
|
|
|
function runHelper(serverUrl, ...args) {
|
|
return runHelperWithEnv(serverUrl, {}, ...args);
|
|
}
|
|
|
|
async function readBody(request) {
|
|
let body = "";
|
|
request.setEncoding("utf8");
|
|
for await (const chunk of request) {
|
|
body += chunk;
|
|
}
|
|
return body;
|
|
}
|
|
|
|
function startServer(responder) {
|
|
const requests = [];
|
|
const serverErrors = [];
|
|
const server = http.createServer(async (request, response) => {
|
|
const recorded = {
|
|
method: request.method,
|
|
url: request.url,
|
|
authorization: request.headers.authorization,
|
|
body: await readBody(request),
|
|
};
|
|
requests.push(recorded);
|
|
|
|
try {
|
|
const result = await responder(recorded, requests.length);
|
|
response.writeHead(result?.status || 200, { "content-type": "application/json" });
|
|
response.end(JSON.stringify(result?.payload ?? {}));
|
|
} catch (error) {
|
|
serverErrors.push(error);
|
|
response.writeHead(500, { "content-type": "application/json" });
|
|
response.end(JSON.stringify({ message: "test server assertion failed" }));
|
|
}
|
|
});
|
|
|
|
return new Promise((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(0, "127.0.0.1", () => {
|
|
resolve({
|
|
url: `http://127.0.0.1:${server.address().port}`,
|
|
requests,
|
|
serverErrors,
|
|
close: () => new Promise((closeResolve, closeReject) => server.close((error) => (error ? closeReject(error) : closeResolve()))),
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function assertSecretSafe(result) {
|
|
assert.doesNotMatch(result.stdout, new RegExp(bearerToken));
|
|
assert.doesNotMatch(result.stderr, new RegExp(bearerToken));
|
|
for (const value of [
|
|
gdprExportStorageEnv.STORAGE_ACCESS_KEY_ID,
|
|
gdprExportStorageEnv.STORAGE_SECRET_ACCESS_KEY,
|
|
portalGdprExportStorageEnv.STORAGE_ACCESS_KEY_ID,
|
|
portalGdprExportStorageEnv.STORAGE_SECRET_ACCESS_KEY,
|
|
importGdprExportStorageEnv.STORAGE_ACCESS_KEY_ID,
|
|
importGdprExportStorageEnv.STORAGE_SECRET_ACCESS_KEY,
|
|
gdprExportRetentionStorageEnv.STORAGE_ACCESS_KEY_ID,
|
|
gdprExportRetentionStorageEnv.STORAGE_SECRET_ACCESS_KEY,
|
|
gdprExportSseCKey,
|
|
alternateGdprExportSseCKey,
|
|
...Object.values(workerRedisSecrets),
|
|
]) {
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, new RegExp(value));
|
|
}
|
|
}
|
|
|
|
function assertWebPushValuesSafe(result) {
|
|
assertSecretSafe(result);
|
|
for (const value of [vapidPublicKey, alternateVapidPublicKey, vapidPrivateKey]) {
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, new RegExp(value));
|
|
}
|
|
}
|
|
|
|
describe("coolify-deploy CLI", () => {
|
|
it("triggers the canonical deploy endpoint and emits only the deployment UUID", async () => {
|
|
const server = await startServer(() => ({
|
|
payload: {
|
|
deployments: [
|
|
{ resource_uuid: otherResourceUuid, deployment_uuid: otherDeploymentUuid },
|
|
{ resource_uuid: resourceUuid, deployment_uuid: deploymentUuid },
|
|
],
|
|
},
|
|
}));
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "trigger", resourceUuid);
|
|
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(result.stdout.trim(), deploymentUuid);
|
|
assert.equal(result.stderr, "");
|
|
assertSecretSafe(result);
|
|
assert.deepEqual(server.serverErrors, []);
|
|
assert.deepEqual(server.requests, [
|
|
{
|
|
method: "POST",
|
|
url: `/deploy?uuid=${resourceUuid}&force=true`,
|
|
authorization: `Bearer ${bearerToken}`,
|
|
body: "",
|
|
},
|
|
]);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("fails closed when the deployment array does not contain the requested resource", async () => {
|
|
const server = await startServer(() => ({
|
|
payload: {
|
|
deployments: [{ resource_uuid: otherResourceUuid, deployment_uuid: otherDeploymentUuid }],
|
|
},
|
|
}));
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "trigger", resourceUuid);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.stdout, "");
|
|
assert.match(result.stderr, /deployment UUID was not returned/);
|
|
assert.doesNotMatch(result.stderr, new RegExp(otherDeploymentUuid));
|
|
assertSecretSafe(result);
|
|
assert.deepEqual(server.serverErrors, []);
|
|
assert.equal(server.requests.length, 1);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("does not accept a legacy UUID when Coolify returns an empty deployment array", async () => {
|
|
const server = await startServer(() => ({
|
|
payload: { deployments: [], deployment_uuid: "stale-legacy-deployment" },
|
|
}));
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "trigger", resourceUuid);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.stdout, "");
|
|
assert.match(result.stderr, /deployment UUID was not returned/);
|
|
assert.doesNotMatch(result.stderr, /stale-legacy-deployment/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("fails closed when the matching deployment entry has no UUID", async () => {
|
|
const server = await startServer(() => ({
|
|
payload: { deployments: [{ resource_uuid: resourceUuid }] },
|
|
}));
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "trigger", resourceUuid);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.stdout, "");
|
|
assert.match(result.stderr, /deployment UUID was not returned/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects malformed resource and deployment IDs without emitting injectable output", async () => {
|
|
const server = await startServer(() => ({
|
|
payload: {
|
|
deployments: [{ resource_uuid: resourceUuid, deployment_uuid: `${deploymentUuid}\nINJECTED=value` }],
|
|
},
|
|
}));
|
|
|
|
try {
|
|
const invalidResource = await runHelper(server.url, "trigger", `${resourceUuid}\nINJECTED=value`);
|
|
assert.equal(invalidResource.status, 1);
|
|
assert.equal(invalidResource.stdout, "");
|
|
assert.match(invalidResource.stderr, /valid Coolify CUID/);
|
|
assert.deepEqual(server.requests, []);
|
|
|
|
const invalidDeployment = await runHelper(server.url, "trigger", resourceUuid);
|
|
assert.equal(invalidDeployment.status, 1);
|
|
assert.equal(invalidDeployment.stdout, "");
|
|
assert.match(invalidDeployment.stderr, /deployment UUID was not returned/);
|
|
assert.doesNotMatch(invalidDeployment.stderr, /INJECTED/);
|
|
assertSecretSafe(invalidResource);
|
|
assertSecretSafe(invalidDeployment);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("accepts only applications without an APP_REVISION Coolify override", async () => {
|
|
const cleanServer = await startServer(() => ({ payload: [] }));
|
|
try {
|
|
const result = await runHelper(cleanServer.url, "assert-no-app-revision-override", webResourceUuid);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /override absent/);
|
|
assert.deepEqual(
|
|
cleanServer.requests.map(({ method, url }) => ({ method, url })),
|
|
[{ method: "GET", url: `/applications/${webResourceUuid}/envs` }]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await cleanServer.close();
|
|
}
|
|
|
|
const overrideServer = await startServer(() => ({ payload: [{ key: "APP_REVISION", value: "" }] }));
|
|
try {
|
|
const result = await runHelper(overrideServer.url, "assert-no-app-revision-override", webResourceUuid);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.stdout, "");
|
|
assert.match(result.stderr, /APP_REVISION override must be removed/);
|
|
assert.deepEqual(
|
|
overrideServer.requests.map(({ method }) => method),
|
|
["GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await overrideServer.close();
|
|
}
|
|
});
|
|
|
|
it("retries legacy Coolify 4.0 environment reads with explicit decryption", async () => {
|
|
const server = await startServer(({ url }) =>
|
|
url === `/applications/${webResourceUuid}/envs` ? { status: 404, payload: { message: "Not found" } } : { payload: [] }
|
|
);
|
|
try {
|
|
const result = await runHelper(server.url, "assert-no-app-revision-override", webResourceUuid);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /override absent/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method, url }) => ({ method, url })),
|
|
[
|
|
{ method: "GET", url: `/applications/${webResourceUuid}/envs` },
|
|
{ method: "GET", url: `/applications/${webResourceUuid}/envs?decrypt=true` },
|
|
]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("fails closed on malformed revision-override environment responses", async () => {
|
|
const server = await startServer(() => ({ payload: { unexpected: [] } }));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-no-app-revision-override", webResourceUuid);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /environment response was malformed/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("accepts only a required-RLS runtime credential for the declared role", async () => {
|
|
const server = await startServer(() => ({
|
|
payload: [
|
|
{ key: "DATABASE_URL", value: "postgres://hivegrid_app:runtime-secret-sentinel@db.example.test:6432/hivegrid" },
|
|
{ key: "DATABASE_READ_URL", value: "postgres://hivegrid_app:read-secret-sentinel@db.example.test/hivegrid" },
|
|
{ key: "DATABASE_RUNTIME_ROLE", value: "hivegrid_app" },
|
|
{ key: "DATABASE_POOL_MAX", value: "10" },
|
|
{ key: "RLS_RUNTIME_MODE", value: "required" },
|
|
{ key: "WEB_RUNTIME_SURFACE", value: "core" },
|
|
],
|
|
}));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-runtime-db-env", webResourceUuid, "hivegrid_app", "web");
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /verified for hivegrid_app/);
|
|
assert.doesNotMatch(result.stdout, /runtime-secret-sentinel/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("accepts the dedicated portal role and rejects core, OAuth, billing, migrator, and ingress credentials", async () => {
|
|
const base = portalRuntimeEnv();
|
|
const cleanServer = await startServer(() => ({ payload: base }));
|
|
try {
|
|
const result = await runHelper(cleanServer.url, "assert-runtime-db-env", portalResourceUuid, "hivegrid_portal", "portal");
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /verified for hivegrid_portal/);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /secret-sentinel/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await cleanServer.close();
|
|
}
|
|
|
|
for (const key of [
|
|
"SESSION_SECRET",
|
|
"AUTHENTIK_ADMIN_CLIENT_SECRET",
|
|
"OAUTH_CLIENT_SECRET",
|
|
"LAGO_API_KEY",
|
|
"MIGRATOR_DATABASE_URL",
|
|
"COOLIFY_TOKEN",
|
|
"SSH_PRIVATE_KEY",
|
|
"TRAEFIK_API_TOKEN",
|
|
"PORTAL_DOMAIN_CONTROLLER_TOKEN",
|
|
"PORTAL_SERVER_API_TOKEN",
|
|
]) {
|
|
const server = await startServer(() => ({ payload: [...base, { key, value: "forbidden-secret-sentinel" }] }));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-runtime-db-env", portalResourceUuid, "hivegrid_portal", "portal");
|
|
assert.equal(result.status, 1, `${key} unexpectedly passed`);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /secret-sentinel/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("rejects an optional DATABASE_READ_URL whose username differs from the declared runtime role", async () => {
|
|
const cases = [
|
|
{
|
|
resource: webResourceUuid,
|
|
role: "hivegrid_app",
|
|
surface: "web",
|
|
payload: [
|
|
{ key: "DATABASE_URL", value: "postgres://hivegrid_app:runtime-secret-sentinel@db.example.test/hivegrid" },
|
|
{ key: "DATABASE_READ_URL", value: "postgres://hivegrid_worker:read-secret-sentinel@db.example.test/hivegrid" },
|
|
{ key: "DATABASE_RUNTIME_ROLE", value: "hivegrid_app" },
|
|
{ key: "RLS_RUNTIME_MODE", value: "required" },
|
|
{ key: "WEB_RUNTIME_SURFACE", value: "core" },
|
|
],
|
|
},
|
|
{
|
|
resource: portalResourceUuid,
|
|
role: "hivegrid_portal",
|
|
surface: "portal",
|
|
payload: portalRuntimeEnv({
|
|
DATABASE_READ_URL: "postgres://hivegrid_app:read-secret-sentinel@db.example.test/hivegrid",
|
|
}),
|
|
},
|
|
{
|
|
resource: workerResourceUuid,
|
|
role: "hivegrid_worker",
|
|
surface: "service",
|
|
payload: [
|
|
{ key: "DATABASE_URL", value: "postgres://hivegrid_worker:runtime-secret-sentinel@db.example.test/hivegrid" },
|
|
{ key: "DATABASE_READ_URL", value: "postgres://hivegrid_app:read-secret-sentinel@db.example.test/hivegrid" },
|
|
{ key: "DATABASE_RUNTIME_ROLE", value: "hivegrid_worker" },
|
|
{ key: "RLS_RUNTIME_MODE", value: "required" },
|
|
],
|
|
},
|
|
];
|
|
|
|
for (const { resource, role, surface, payload } of cases) {
|
|
const server = await startServer(() => ({ payload }));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-runtime-db-env", resource, role, surface);
|
|
assert.equal(result.status, 1, `${role} unexpectedly accepted a mismatched read role`);
|
|
assert.match(result.stderr, /read database credential does not match DATABASE_RUNTIME_ROLE/);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /secret-sentinel/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("accepts Cap portal login verification with a login-specific or shared secret", async () => {
|
|
for (const secret of [{ CAP_SITE_SECRET_LOGIN: "cap-login-secret-sentinel" }, { CAP_SITE_SECRET: "cap-shared-secret-sentinel" }]) {
|
|
const payload = portalRuntimeEnv({
|
|
CAPTCHA_PROVIDER: "cap",
|
|
MCAPTCHA_INSTANCE_URL: undefined,
|
|
MCAPTCHA_SITEKEY_LOGIN: undefined,
|
|
MCAPTCHA_ACCOUNT_SECRET: undefined,
|
|
CAP_INSTANCE_URL: "https://cap.example.test",
|
|
CAP_SITEKEY_LOGIN: "portal-cap-login-sitekey",
|
|
...secret,
|
|
});
|
|
const server = await startServer(() => ({ payload }));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-runtime-db-env", portalResourceUuid, "hivegrid_portal", "portal");
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /secret-sentinel/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("rejects incomplete provider-specific portal CAPTCHA contracts", async () => {
|
|
const capBase = {
|
|
CAPTCHA_PROVIDER: "cap",
|
|
CAP_INSTANCE_URL: "https://cap.example.test",
|
|
CAP_SITEKEY_LOGIN: "portal-cap-login-sitekey",
|
|
CAP_SITE_SECRET_LOGIN: "cap-login-secret-sentinel",
|
|
};
|
|
const cases = [
|
|
{ overrides: { CAPTCHA_PROVIDER: undefined }, expectedKey: "CAPTCHA_PROVIDER" },
|
|
{ overrides: { CAPTCHA_PROVIDER: "unsupported" }, expectedKey: "CAPTCHA_PROVIDER" },
|
|
{ overrides: { MCAPTCHA_INSTANCE_URL: undefined }, expectedKey: "MCAPTCHA_INSTANCE_URL" },
|
|
{ overrides: { MCAPTCHA_SITEKEY_LOGIN: undefined }, expectedKey: "MCAPTCHA_SITEKEY_LOGIN" },
|
|
{ overrides: { MCAPTCHA_ACCOUNT_SECRET: undefined }, expectedKey: "MCAPTCHA_ACCOUNT_SECRET" },
|
|
{ overrides: { ...capBase, CAP_INSTANCE_URL: undefined }, expectedKey: "CAP_INSTANCE_URL" },
|
|
{ overrides: { ...capBase, CAP_SITEKEY_LOGIN: undefined }, expectedKey: "CAP_SITEKEY_LOGIN" },
|
|
{ overrides: { ...capBase, CAP_SITE_SECRET_LOGIN: undefined, CAP_SITE_SECRET: undefined }, expectedKey: "CAP_SITE_SECRET_LOGIN" },
|
|
];
|
|
|
|
for (const { overrides, expectedKey } of cases) {
|
|
const server = await startServer(() => ({ payload: portalRuntimeEnv(overrides) }));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-runtime-db-env", portalResourceUuid, "hivegrid_portal", "portal");
|
|
assert.equal(result.status, 1, `${expectedKey} unexpectedly passed`);
|
|
assert.match(result.stderr, new RegExp(expectedKey));
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /secret-sentinel/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("rejects missing or invalid portal Redis and BullMQ connections", async () => {
|
|
const cases = [
|
|
{ overrides: { REDIS_URL: undefined }, expectedKey: "REDIS_URL" },
|
|
{ overrides: { REDIS_URL: "https://redis-secret-sentinel.example.test" }, expectedKey: "REDIS_URL" },
|
|
{ overrides: { BULLMQ_REDIS_URL: "redis://redis.example.test/not-a-database" }, expectedKey: "BULLMQ_REDIS_URL" },
|
|
];
|
|
|
|
for (const { overrides, expectedKey } of cases) {
|
|
const server = await startServer(() => ({ payload: portalRuntimeEnv(overrides) }));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-runtime-db-env", portalResourceUuid, "hivegrid_portal", "portal");
|
|
assert.equal(result.status, 1, `${expectedKey} unexpectedly passed`);
|
|
assert.match(result.stderr, new RegExp(expectedKey));
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /secret-sentinel/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("requires the explicit core and portal runtime surfaces", async () => {
|
|
for (const [role, surface, entries] of [
|
|
[
|
|
"hivegrid_app",
|
|
"web",
|
|
[
|
|
{ key: "DATABASE_URL", value: "postgres://hivegrid_app:secret@db.example.test/hivegrid" },
|
|
{ key: "DATABASE_RUNTIME_ROLE", value: "hivegrid_app" },
|
|
{ key: "RLS_RUNTIME_MODE", value: "required" },
|
|
],
|
|
],
|
|
[
|
|
"hivegrid_portal",
|
|
"portal",
|
|
[
|
|
{ key: "DATABASE_URL", value: "postgres://hivegrid_portal:secret@db.example.test/hivegrid" },
|
|
{ key: "DATABASE_RUNTIME_ROLE", value: "hivegrid_portal" },
|
|
{ key: "RLS_RUNTIME_MODE", value: "required" },
|
|
{ key: "PORTAL_SESSION_SECRET", value: "portal-secret" },
|
|
],
|
|
],
|
|
]) {
|
|
const server = await startServer(() => ({ payload: entries }));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-runtime-db-env", surface === "web" ? webResourceUuid : portalResourceUuid, role, surface);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /WEB_RUNTIME_SURFACE/);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("requires an explicit positive pool cap and PgBouncer primary URL for every runtime", async () => {
|
|
const base = runtimeDatabaseEnv("hivegrid_app", { WEB_RUNTIME_SURFACE: "core" });
|
|
const cases = [
|
|
{
|
|
payload: base.filter(({ key }) => key !== "DATABASE_POOL_MAX"),
|
|
error: /explicit positive DATABASE_POOL_MAX/,
|
|
},
|
|
{
|
|
payload: base.map((entry) => (entry.key === "DATABASE_POOL_MAX" ? { ...entry, value: "0" } : entry)),
|
|
error: /explicit positive DATABASE_POOL_MAX/,
|
|
},
|
|
{
|
|
payload: base.map((entry) =>
|
|
entry.key === "DATABASE_URL" ? { ...entry, value: "postgres://hivegrid_app:runtime-secret-sentinel@db.example.test:5432/hivegrid" } : entry
|
|
),
|
|
error: /PgBouncer port 6432/,
|
|
},
|
|
];
|
|
|
|
for (const { payload, error } of cases) {
|
|
const server = await startServer(() => ({ payload }));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-runtime-db-env", webResourceUuid, "hivegrid_app", "web");
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, error);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /secret-sentinel/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("verifies positive hard caps on all twelve allowlisted runtimes with GET-only requests before migration", async () => {
|
|
const fixture = runtimePreflightFixture();
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /read-only runtime preflight verified/);
|
|
assert.equal(result.stderr, "");
|
|
assert.equal(server.requests.length, 12);
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assert.equal(new Set(server.requests.map(({ url }) => url)).size, 12);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects missing or invalid hard caps on API, MCP, and Hocuspocus runtimes", async () => {
|
|
const cases = [
|
|
{ uuid: apiResourceUuid, field: "limits_cpus", value: undefined, label: "API" },
|
|
{ uuid: mcpResourceUuid, field: "limits_cpus", value: "0", label: "MCP" },
|
|
{ uuid: hocuspocusResourceUuid, field: "limits_memory", value: "512", label: "Hocuspocus" },
|
|
];
|
|
|
|
for (const { uuid, field, value, label } of cases) {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.apps = fixture.apps.map((app) => (app.uuid === uuid ? { ...app, [field]: value } : app));
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1, `${label} resource limits unexpectedly passed`);
|
|
assert.match(result.stderr, new RegExp(`Coolify ${label} runtime must set positive limits_cpus and unit-suffixed limits_memory hard caps`));
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("rejects missing or invalid hard caps on every worker runtime", async () => {
|
|
const invalidLimits = [
|
|
{ field: "limits_cpus", value: undefined },
|
|
{ field: "limits_cpus", value: "Infinity" },
|
|
{ field: "limits_memory", value: undefined },
|
|
{ field: "limits_memory", value: "0m" },
|
|
{ field: "limits_memory", value: "1.5g" },
|
|
];
|
|
|
|
for (const [index, [group, uuid]] of Object.entries(workerGroupResourceUuids).entries()) {
|
|
const fixture = runtimePreflightFixture();
|
|
const { field, value } = invalidLimits[index];
|
|
fixture.apps = fixture.apps.map((app) => (app.uuid === uuid ? { ...app, [field]: value } : app));
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1, `worker-${group} resource limits unexpectedly passed`);
|
|
assert.match(result.stderr, new RegExp(`Coolify worker-${group} runtime must set positive limits_cpus and unit-suffixed limits_memory hard caps`));
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("requires positive CPU and unit-suffixed memory hard caps on the portal-domain-controller", async () => {
|
|
for (const invalid of [{ limits_cpus: "0" }, { limits_memory: "512mb" }]) {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.apps = fixture.apps.map((app) => (app.uuid === portalDomainControllerResourceUuid ? { ...app, ...invalid } : app));
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1, "portal-domain-controller resource limits unexpectedly passed");
|
|
assert.match(
|
|
result.stderr,
|
|
/Coolify portal-domain-controller runtime must set positive limits_cpus and unit-suffixed limits_memory hard caps/
|
|
);
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("fails the runtime preflight when core lacks the distributed GDPR download Redis contract", async () => {
|
|
for (const value of [undefined, "https://redis-secret-sentinel.example.test"]) {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.envByUuid.set(
|
|
webResourceUuid,
|
|
fixture.envByUuid
|
|
.get(webResourceUuid)
|
|
.filter(({ key }) => key !== "REDIS_URL")
|
|
.concat(value === undefined ? [] : [{ key: "REDIS_URL", value }])
|
|
);
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /hivegrid-web runtime requires a valid REDIS_URL for distributed GDPR export download gates/);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /redis-secret-sentinel/);
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("rejects APP_REVISION overrides on every pre-migration runtime without leaking values", async () => {
|
|
const runtimeUuids = [...runtimePreflightFixture().envByUuid.keys()];
|
|
for (const uuid of runtimeUuids) {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.envByUuid.set(uuid, [...fixture.envByUuid.get(uuid), { key: "APP_REVISION", value: `secret-${uuid}` }]);
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1, `${uuid} APP_REVISION unexpectedly passed`);
|
|
assert.match(result.stderr, /APP_REVISION overrides must be removed before migration/);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, new RegExp(`secret-${uuid}`));
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("rejects the portal session secret on the core web runtime", async () => {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.envByUuid.set(webResourceUuid, [
|
|
...fixture.envByUuid.get(webResourceUuid),
|
|
{ key: "PORTAL_SESSION_SECRET", value: "core-portal-session-secret-sentinel" },
|
|
]);
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /hivegrid-web runtime must not contain PORTAL_SESSION_SECRET/);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /core-portal-session-secret-sentinel/);
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("fails the runtime preflight when a worker group is missing or outside the allowlist", async () => {
|
|
for (const mode of ["missing", "not-allowlisted"]) {
|
|
const fixture = runtimePreflightFixture();
|
|
if (mode === "missing") {
|
|
fixture.apps = fixture.apps.filter(({ uuid }) => uuid !== workerGroupResourceUuids.import);
|
|
} else {
|
|
fixture.allowedCsv = fixture.allowedCsv
|
|
.split(",")
|
|
.filter((uuid) => uuid !== workerGroupResourceUuids.import)
|
|
.join(",");
|
|
}
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1, `${mode} worker unexpectedly passed`);
|
|
assert.match(result.stderr, /worker-import runtime is missing, ambiguous, or outside the resource allowlist/);
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("fails the runtime preflight on portal-host or HTTPS drift across boundary consumers", async () => {
|
|
const cases = [
|
|
{
|
|
uuid: webResourceUuid,
|
|
key: "PORTAL_APP_HOST",
|
|
value: "other-portal.example.test",
|
|
error: /same normalized PORTAL_APP_HOST/,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.mail,
|
|
key: "PORTAL_APP_HOST",
|
|
value: "other-portal.example.test",
|
|
error: /same normalized PORTAL_APP_HOST/,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.mail,
|
|
key: "PORTAL_APP_HOST",
|
|
value: "portal.example.test:444",
|
|
error: /same normalized PORTAL_APP_HOST/,
|
|
},
|
|
{
|
|
uuid: portalResourceUuid,
|
|
key: "APP_PROTOCOL",
|
|
value: "http",
|
|
error: /must set APP_PROTOCOL=https/,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.mail,
|
|
key: "APP_PROTOCOL",
|
|
value: "http",
|
|
error: /must set APP_PROTOCOL=https/,
|
|
},
|
|
];
|
|
|
|
for (const testCase of cases) {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.envByUuid.set(
|
|
testCase.uuid,
|
|
fixture.envByUuid.get(testCase.uuid).map((entry) => (entry.key === testCase.key ? { ...entry, value: testCase.value } : entry))
|
|
);
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, testCase.error);
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("fails the runtime preflight when worker-mail lacks the exact hivegrid-web envelope key without leaking either value", async () => {
|
|
const cases = [
|
|
{ uuid: webResourceUuid, mode: "missing" },
|
|
{ uuid: workerGroupResourceUuids.mail, mode: "missing" },
|
|
{ uuid: workerGroupResourceUuids.mail, mode: "mismatch" },
|
|
];
|
|
|
|
for (const testCase of cases) {
|
|
const fixture = runtimePreflightFixture();
|
|
const mismatchedSecret = `mismatched-${sharedCryptoSecret}`;
|
|
fixture.envByUuid.set(
|
|
testCase.uuid,
|
|
testCase.mode === "missing"
|
|
? fixture.envByUuid.get(testCase.uuid).filter(({ key }) => key !== "CRYPTO_SECRET")
|
|
: fixture.envByUuid.get(testCase.uuid).map((entry) => (entry.key === "CRYPTO_SECRET" ? { ...entry, value: mismatchedSecret } : entry))
|
|
);
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /hivegrid-web and worker-mail must share the same non-empty CRYPTO_SECRET/);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, new RegExp(sharedCryptoSecret));
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, new RegExp(mismatchedSecret));
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("fails the runtime preflight when the admin connection master-key boundary is missing, weak, mismatched, reused, or leaked to portal", async () => {
|
|
const cases = [
|
|
{
|
|
uuid: webResourceUuid,
|
|
mutate: (entries) => entries.filter(({ key }) => key !== "ADMIN_CONNECTION_MASTER_KEY"),
|
|
error: /ADMIN_CONNECTION_MASTER_KEY with at least 32 bytes on: hivegrid-web/,
|
|
},
|
|
{
|
|
uuid: apiResourceUuid,
|
|
mutate: (entries) => entries.map((entry) => (entry.key === "ADMIN_CONNECTION_MASTER_KEY" ? { ...entry, value: "too-short" } : entry)),
|
|
error: /ADMIN_CONNECTION_MASTER_KEY with at least 32 bytes on: hivegrid-api/,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.general,
|
|
mutate: (entries) =>
|
|
entries.map((entry) => (entry.key === "ADMIN_CONNECTION_MASTER_KEY" ? { ...entry, value: "mismatched-admin-connection-key-sentinel" } : entry)),
|
|
error: /must share the same ADMIN_CONNECTION_MASTER_KEY/,
|
|
},
|
|
{
|
|
uuid: webResourceUuid,
|
|
mutate: (entries) => entries.map((entry) => (entry.key === "CRYPTO_SECRET" ? { ...entry, value: sharedAdminConnectionMasterKey } : entry)),
|
|
error: /must be independent from SESSION_SECRET and CRYPTO_SECRET on: hivegrid-web/,
|
|
},
|
|
{
|
|
uuid: portalResourceUuid,
|
|
mutate: (entries) => [...entries, { key: "ADMIN_CONNECTION_MASTER_KEY", value: sharedAdminConnectionMasterKey }],
|
|
error: /hivegrid-portal must not contain ADMIN_CONNECTION_MASTER_KEY/,
|
|
},
|
|
];
|
|
|
|
for (const testCase of cases) {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.envByUuid.set(testCase.uuid, testCase.mutate(fixture.envByUuid.get(testCase.uuid)));
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, testCase.error);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, new RegExp(sharedAdminConnectionMasterKey));
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("fails the runtime preflight on encrypted GDPR export storage, key, or dedicated import-gate drift without leaking values", async () => {
|
|
const cases = [
|
|
{
|
|
uuid: webResourceUuid,
|
|
key: "STORAGE_ENDPOINT",
|
|
mode: "missing",
|
|
error: /hivegrid-web runtime is missing required encrypted GDPR export environment keys: STORAGE_ENDPOINT/,
|
|
},
|
|
{
|
|
uuid: portalResourceUuid,
|
|
key: "STORAGE_ENDPOINT",
|
|
value: "http://storage.example.test",
|
|
error: /hivegrid-portal runtime must set STORAGE_ENDPOINT to a valid HTTPS URL/,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.import,
|
|
key: "GDPR_EXPORT_SSE_C_KEY_BASE64",
|
|
value: "not-canonical-base64",
|
|
error: /worker-import runtime must set GDPR_EXPORT_SSE_C_KEY_BASE64 to canonical base64 for exactly 32 bytes/,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.import,
|
|
key: "GDPR_EXPORT_SSE_C_KEY_BASE64",
|
|
value: alternateGdprExportSseCKey,
|
|
error: /encrypted GDPR export runtime values must match after normalization for: GDPR_EXPORT_SSE_C_KEY_BASE64/,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.import,
|
|
key: "IMPORT_QUEUE_ENABLED",
|
|
value: "true",
|
|
error: /worker-import must use idle mode, full mode.*or dedicated GDPR export mode/,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.import,
|
|
key: "IMPORT_QUEUE_RUNTIME_READY",
|
|
value: "true",
|
|
error: /worker-import must use idle mode, full mode.*or dedicated GDPR export mode/,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.import,
|
|
key: "PORTAL_GDPR_EXPORT_QUEUE_ENABLED",
|
|
value: "banana",
|
|
error: /worker-import PORTAL_GDPR_EXPORT_QUEUE_ENABLED must be exactly true or false when configured/,
|
|
redactedValue: true,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.general,
|
|
key: "EXPORT_RETENTION_QUEUE_ENABLED",
|
|
value: "false",
|
|
error: /worker-general must set EXPORT_RETENTION_QUEUE_ENABLED=true/,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.general,
|
|
key: "MEILI_URL",
|
|
mode: "missing",
|
|
error: /worker-general runtime is missing required tenant hard-delete search environment keys: MEILI_URL/,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.general,
|
|
key: "MEILI_API_KEY",
|
|
mode: "missing",
|
|
error: /worker-general runtime is missing required tenant hard-delete search environment keys: MEILI_API_KEY/,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.general,
|
|
key: "STORAGE_BUCKET",
|
|
value: "other-private-exports",
|
|
error: /GDPR export-retention storage values must match.*STORAGE_BUCKET/,
|
|
},
|
|
];
|
|
|
|
for (const testCase of cases) {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.envByUuid.set(
|
|
testCase.uuid,
|
|
testCase.mode === "missing"
|
|
? fixture.envByUuid.get(testCase.uuid).filter(({ key }) => key !== testCase.key)
|
|
: fixture.envByUuid.get(testCase.uuid).map((entry) => (entry.key === testCase.key ? { ...entry, value: testCase.value } : entry))
|
|
);
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, testCase.error);
|
|
if (testCase.redactedValue) assert.doesNotMatch(`${result.stdout}${result.stderr}`, new RegExp(testCase.value));
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("accepts full worker-import mode with encrypted GDPR export storage and an unset dedicated gate", async () => {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.envByUuid.set(
|
|
workerGroupResourceUuids.import,
|
|
fixture.envByUuid
|
|
.get(workerGroupResourceUuids.import)
|
|
.filter(({ key }) => key !== "PORTAL_GDPR_EXPORT_QUEUE_ENABLED")
|
|
.map((entry) => (entry.key === "IMPORT_QUEUE_ENABLED" || entry.key === "IMPORT_QUEUE_RUNTIME_READY" ? { ...entry, value: "true" } : entry))
|
|
);
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /read-only runtime preflight verified/);
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("accepts one shared storage credential across all GDPR export consumers without leaking it", async () => {
|
|
const fixture = runtimePreflightFixture();
|
|
for (const uuid of [portalResourceUuid, workerGroupResourceUuids.import, workerGroupResourceUuids.general]) {
|
|
fixture.envByUuid.set(
|
|
uuid,
|
|
fixture.envByUuid.get(uuid).map((entry) => {
|
|
if (entry.key === "STORAGE_ACCESS_KEY_ID") {
|
|
return { ...entry, value: gdprExportStorageEnv.STORAGE_ACCESS_KEY_ID };
|
|
}
|
|
if (entry.key === "STORAGE_SECRET_ACCESS_KEY") {
|
|
return { ...entry, value: gdprExportStorageEnv.STORAGE_SECRET_ACCESS_KEY };
|
|
}
|
|
return entry;
|
|
})
|
|
);
|
|
}
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /read-only runtime preflight verified/);
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects an SSE-C read key on the delete-only general worker without leaking it", async () => {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.envByUuid.set(workerGroupResourceUuids.general, [
|
|
...fixture.envByUuid.get(workerGroupResourceUuids.general),
|
|
{ key: "GDPR_EXPORT_SSE_C_KEY_BASE64", value: alternateGdprExportSseCKey },
|
|
]);
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /worker-general must not contain GDPR_EXPORT_SSE_C_KEY_BASE64 because it is delete-only/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("requires worker-import and worker-general to share a valid tenant-export barrier Redis target without leaking credentials", async () => {
|
|
const cases = [
|
|
{
|
|
uuid: workerGroupResourceUuids.import,
|
|
mutate: (entries) => entries.filter(({ key }) => key !== "BULLMQ_REDIS_URL" && key !== "REDIS_URL"),
|
|
error: /worker-import runtime requires a valid REDIS_URL or BULLMQ_REDIS_URL/,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.general,
|
|
mutate: (entries) =>
|
|
entries.map((entry) => (entry.key === "BULLMQ_REDIS_URL" ? { ...entry, value: "https://general-redis-secret-sentinel.example.test/4" } : entry)),
|
|
error: /worker-general runtime requires a valid REDIS_URL or BULLMQ_REDIS_URL/,
|
|
},
|
|
{
|
|
uuid: workerGroupResourceUuids.import,
|
|
mutate: (entries) =>
|
|
entries.map((entry) =>
|
|
entry.key === "BULLMQ_REDIS_URL" ? { ...entry, value: "rediss://import-drift-secret-sentinel@redis.example.test:6380/5" } : entry
|
|
),
|
|
error: /worker-import and worker-general must use the same Redis endpoint and database/,
|
|
},
|
|
];
|
|
|
|
for (const testCase of cases) {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.envByUuid.set(testCase.uuid, testCase.mutate(fixture.envByUuid.get(testCase.uuid)));
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, testCase.error);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /general-redis-secret|import-drift-secret/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("accepts idle worker-import mode without export storage while web surfaces retain the encrypted download contract", async () => {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.envByUuid.set(
|
|
workerGroupResourceUuids.import,
|
|
fixture.envByUuid
|
|
.get(workerGroupResourceUuids.import)
|
|
.filter(
|
|
({ key }) =>
|
|
!GDPR_EXPORT_STORAGE_TEST_KEYS.has(key) &&
|
|
key !== "IMPORT_QUEUE_ENABLED" &&
|
|
key !== "IMPORT_QUEUE_RUNTIME_READY" &&
|
|
key !== "PORTAL_GDPR_EXPORT_QUEUE_ENABLED"
|
|
)
|
|
);
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /read-only runtime preflight verified/);
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects normalized storage-target drift across encrypted GDPR export consumers", async () => {
|
|
const cases = [
|
|
{ uuid: portalResourceUuid, key: "STORAGE_ENDPOINT", value: "https://other-storage.example.test", expectedKey: "STORAGE_ENDPOINT" },
|
|
{ uuid: workerGroupResourceUuids.import, key: "STORAGE_REGION", value: "eu-west-1", expectedKey: "STORAGE_REGION" },
|
|
{ uuid: webResourceUuid, key: "STORAGE_BUCKET", value: "other-private-exports", expectedKey: "STORAGE_BUCKET" },
|
|
{ uuid: portalResourceUuid, key: "STORAGE_FORCE_PATH_STYLE", value: "true", expectedKey: "STORAGE_FORCE_PATH_STYLE" },
|
|
];
|
|
|
|
for (const testCase of cases) {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.envByUuid.set(
|
|
testCase.uuid,
|
|
fixture.envByUuid.get(testCase.uuid).map((entry) => (entry.key === testCase.key ? { ...entry, value: testCase.value } : entry))
|
|
);
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, new RegExp(`must match after normalization for: ${testCase.expectedKey}`));
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, new RegExp(testCase.value));
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("rejects a portal host that aliases any canonical HTTPS core origin", async () => {
|
|
for (const key of ["APP_BASE_DOMAIN", "WEB_APP_HOST", "ADMIN_APP_HOST", "API_HOST"]) {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.envByUuid.set(
|
|
webResourceUuid,
|
|
fixture.envByUuid.get(webResourceUuid).map((entry) => (entry.key === key ? { ...entry, value: "https://PORTAL.EXAMPLE.TEST:443" } : entry))
|
|
);
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1, `${key} :443 alias unexpectedly passed`);
|
|
assert.match(result.stderr, /PORTAL_APP_HOST must be distinct from core APP_BASE_DOMAIN, WEB_APP_HOST, ADMIN_APP_HOST, and API_HOST/);
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("includes the existing Web Push contract in the full read-only runtime preflight", async () => {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.envByUuid.set(
|
|
workerGroupResourceUuids.webhook,
|
|
fixture.envByUuid.get(workerGroupResourceUuids.webhook).filter(({ key }) => key !== "WEB_PUSH_VAPID_PRIVATE_KEY")
|
|
);
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /WEB_PUSH_VAPID_PRIVATE_KEY/);
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("fails the runtime preflight when any application or worker loses the pool cap", async () => {
|
|
for (const uuid of [apiResourceUuid, workerGroupResourceUuids.general]) {
|
|
const fixture = runtimePreflightFixture();
|
|
fixture.envByUuid.set(
|
|
uuid,
|
|
fixture.envByUuid.get(uuid).filter(({ key }) => key !== "DATABASE_POOL_MAX")
|
|
);
|
|
const server = await startServer((request) => ({
|
|
payload: request.url === "/applications" ? fixture.apps : (fixture.envByUuid.get(request.url.split("/").at(-2)) ?? []),
|
|
}));
|
|
try {
|
|
const result = await runRuntimePreflight(server.url, fixture.allowedCsv);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /explicit positive DATABASE_POOL_MAX/);
|
|
assert.ok(server.requests.every(({ method }) => method === "GET"));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("accepts a complete shared Web Push runtime contract without emitting key material", async () => {
|
|
const server = await startServer((request) => ({ payload: webPushEnvPayload(request.url) }));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-web-push-env", webResourceUuid, workerResourceUuid);
|
|
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /Web Push runtime environment verified/);
|
|
assert.equal(result.stderr, "");
|
|
assertWebPushValuesSafe(result);
|
|
assert.deepEqual(
|
|
new Set(server.requests.map(({ method, url }) => `${method} ${url}`)),
|
|
new Set([`GET /applications/${webResourceUuid}/envs`, `GET /applications/${workerResourceUuid}/envs`])
|
|
);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects every missing Web Push runtime key without emitting configured values", async () => {
|
|
const cases = [
|
|
{ overrides: { web: [] }, expectedKey: "WEB_PUSH_VAPID_PUBLIC_KEY" },
|
|
{
|
|
overrides: {
|
|
worker: [
|
|
{ key: "WEB_PUSH_VAPID_PUBLIC_KEY", value: vapidPublicKey },
|
|
{ key: "WEB_PUSH_VAPID_PRIVATE_KEY", value: vapidPrivateKey },
|
|
],
|
|
},
|
|
expectedKey: "WEB_PUSH_VAPID_SUBJECT",
|
|
},
|
|
{
|
|
overrides: {
|
|
worker: [
|
|
{ key: "WEB_PUSH_VAPID_SUBJECT", value: vapidSubject },
|
|
{ key: "WEB_PUSH_VAPID_PRIVATE_KEY", value: vapidPrivateKey },
|
|
],
|
|
},
|
|
expectedKey: "WEB_PUSH_VAPID_PUBLIC_KEY",
|
|
},
|
|
{
|
|
overrides: {
|
|
worker: [
|
|
{ key: "WEB_PUSH_VAPID_SUBJECT", value: vapidSubject },
|
|
{ key: "WEB_PUSH_VAPID_PUBLIC_KEY", value: vapidPublicKey },
|
|
],
|
|
},
|
|
expectedKey: "WEB_PUSH_VAPID_PRIVATE_KEY",
|
|
},
|
|
];
|
|
|
|
for (const { overrides, expectedKey } of cases) {
|
|
const server = await startServer((request) => ({ payload: webPushEnvPayload(request.url, overrides) }));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-web-push-env", webResourceUuid, workerResourceUuid);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, new RegExp(expectedKey));
|
|
assertWebPushValuesSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("rejects invalid Web Push subjects and keys without emitting configured values", async () => {
|
|
const cases = [
|
|
{
|
|
worker: [
|
|
{ key: "WEB_PUSH_VAPID_SUBJECT", value: "https://user:password@example.test/#fragment" },
|
|
{ key: "WEB_PUSH_VAPID_PUBLIC_KEY", value: vapidPublicKey },
|
|
{ key: "WEB_PUSH_VAPID_PRIVATE_KEY", value: vapidPrivateKey },
|
|
],
|
|
expectedKey: "WEB_PUSH_VAPID_SUBJECT",
|
|
},
|
|
{ web: [{ key: "WEB_PUSH_VAPID_PUBLIC_KEY", value: "not-base64url!" }], expectedKey: "WEB_PUSH_VAPID_PUBLIC_KEY" },
|
|
{
|
|
worker: [
|
|
{ key: "WEB_PUSH_VAPID_SUBJECT", value: vapidSubject },
|
|
{ key: "WEB_PUSH_VAPID_PUBLIC_KEY", value: Buffer.alloc(64, 1).toString("base64url") },
|
|
{ key: "WEB_PUSH_VAPID_PRIVATE_KEY", value: vapidPrivateKey },
|
|
],
|
|
expectedKey: "WEB_PUSH_VAPID_PUBLIC_KEY",
|
|
},
|
|
{
|
|
worker: [
|
|
{ key: "WEB_PUSH_VAPID_SUBJECT", value: vapidSubject },
|
|
{ key: "WEB_PUSH_VAPID_PUBLIC_KEY", value: vapidPublicKey },
|
|
{ key: "WEB_PUSH_VAPID_PRIVATE_KEY", value: Buffer.alloc(31, 2).toString("base64url") },
|
|
],
|
|
expectedKey: "WEB_PUSH_VAPID_PRIVATE_KEY",
|
|
},
|
|
];
|
|
|
|
for (const { expectedKey, ...overrides } of cases) {
|
|
const server = await startServer((request) => ({ payload: webPushEnvPayload(request.url, overrides) }));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-web-push-env", webResourceUuid, workerResourceUuid);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, new RegExp(expectedKey));
|
|
assertWebPushValuesSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("rejects different valid web and webhook-worker public keys", async () => {
|
|
const worker = [
|
|
{ key: "WEB_PUSH_VAPID_SUBJECT", value: vapidSubject },
|
|
{ key: "WEB_PUSH_VAPID_PUBLIC_KEY", value: alternateVapidPublicKey },
|
|
{ key: "WEB_PUSH_VAPID_PRIVATE_KEY", value: vapidPrivateKey },
|
|
];
|
|
const server = await startServer((request) => ({ payload: webPushEnvPayload(request.url, { worker }) }));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-web-push-env", webResourceUuid, workerResourceUuid);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /WEB_PUSH_VAPID_PUBLIC_KEY values must match/);
|
|
assertWebPushValuesSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("fails closed on preview-only or duplicate Web Push runtime keys", async () => {
|
|
const cases = [
|
|
{ web: [{ key: "WEB_PUSH_VAPID_PUBLIC_KEY", value: vapidPublicKey, is_preview: true }] },
|
|
{
|
|
worker: [
|
|
{ key: "WEB_PUSH_VAPID_SUBJECT", value: vapidSubject },
|
|
{ key: "WEB_PUSH_VAPID_PUBLIC_KEY", value: vapidPublicKey },
|
|
{ key: "WEB_PUSH_VAPID_PUBLIC_KEY", value: alternateVapidPublicKey },
|
|
{ key: "WEB_PUSH_VAPID_PRIVATE_KEY", value: vapidPrivateKey },
|
|
],
|
|
},
|
|
];
|
|
|
|
for (const overrides of cases) {
|
|
const server = await startServer((request) => ({ payload: webPushEnvPayload(request.url, overrides) }));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-web-push-env", webResourceUuid, workerResourceUuid);
|
|
assert.equal(result.status, 1);
|
|
assertWebPushValuesSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("rejects privileged env keys, disabled RLS, and a mismatched runtime DSN without leaking credentials", async () => {
|
|
for (const payload of [
|
|
[
|
|
{ key: "DATABASE_URL", value: "postgres://hivegrid_app:runtime-secret-sentinel@db.example.test:6432/hivegrid" },
|
|
{ key: "DATABASE_RUNTIME_ROLE", value: "hivegrid_app" },
|
|
{ key: "DATABASE_POOL_MAX", value: "10" },
|
|
{ key: "RLS_RUNTIME_MODE", value: "required" },
|
|
{ key: "WEB_RUNTIME_SURFACE", value: "core" },
|
|
{ key: "DIRECT_URL", value: "postgres://owner:owner-secret-sentinel@db.example.test/hivegrid" },
|
|
],
|
|
[
|
|
{ key: "DATABASE_URL", value: "postgres://hivegrid_worker:runtime-secret-sentinel@db.example.test/hivegrid" },
|
|
{ key: "DATABASE_RUNTIME_ROLE", value: "hivegrid_app" },
|
|
{ key: "RLS_RUNTIME_MODE", value: "off" },
|
|
],
|
|
]) {
|
|
const server = await startServer(() => ({ payload }));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-runtime-db-env", webResourceUuid, "hivegrid_app", "web");
|
|
assert.equal(result.status, 1);
|
|
assert.doesNotMatch(result.stdout, /secret-sentinel/);
|
|
assert.doesNotMatch(result.stderr, /secret-sentinel/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("accepts a complete admin-only Authentik Admin JIT credential for the dedicated role", async () => {
|
|
const server = await startServer(() => ({
|
|
payload: [
|
|
{ key: "DATABASE_URL", value: "postgres://hivegrid_app:runtime-secret-sentinel@db.example.test:6432/hivegrid" },
|
|
{ key: "DATABASE_RUNTIME_ROLE", value: "hivegrid_app" },
|
|
{ key: "DATABASE_POOL_MAX", value: "10" },
|
|
{ key: "RLS_RUNTIME_MODE", value: "required" },
|
|
{ key: "WEB_RUNTIME_SURFACE", value: "admin" },
|
|
{ key: "SESSION_SECRET", value: "dedicated-admin-session-secret-sentinel" },
|
|
{ key: "ADMIN_FALLBACK_LOGIN_ENABLED", value: "false" },
|
|
{ key: "AUTHENTIK_ISSUER_URL", value: "https://auth.example.test/application/o/hivegrid-admin-panel/" },
|
|
{ key: "AUTHENTIK_ADMIN_CLIENT_ID", value: "hivegrid-admin-panel" },
|
|
{ key: "AUTHENTIK_ADMIN_CLIENT_SECRET", value: "oidc-secret-sentinel" },
|
|
{ key: "AUTHENTIK_ADMIN_REDIRECT_URI", value: "https://admin.example.test/auth/admin/callback" },
|
|
{
|
|
key: "AUTHENTIK_ADMIN_JIT_DATABASE_URL",
|
|
value: "postgres://hivegrid_authentik_jit:jit-secret-sentinel@db.example.test/hivegrid",
|
|
},
|
|
],
|
|
}));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-runtime-db-env", adminResourceUuid, "hivegrid_app", "admin");
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /secret-sentinel/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects incomplete or wrongly authenticated admin JIT configuration without leaking credentials", async () => {
|
|
for (const authentikEntries of [
|
|
[{ key: "AUTHENTIK_ISSUER_URL", value: "https://auth.example.test/application/o/hivegrid-admin-panel/" }],
|
|
[
|
|
{ key: "AUTHENTIK_ISSUER_URL", value: "https://auth.example.test/application/o/hivegrid-admin-panel/" },
|
|
{ key: "AUTHENTIK_ADMIN_CLIENT_ID", value: "hivegrid-admin-panel" },
|
|
{ key: "AUTHENTIK_ADMIN_CLIENT_SECRET", value: "oidc-secret-sentinel" },
|
|
{ key: "AUTHENTIK_ADMIN_REDIRECT_URI", value: "https://admin.example.test/auth/admin/callback" },
|
|
{
|
|
key: "AUTHENTIK_ADMIN_JIT_DATABASE_URL",
|
|
value: "postgres://hivegrid_app:jit-secret-sentinel@db.example.test/hivegrid",
|
|
},
|
|
],
|
|
]) {
|
|
const server = await startServer(() => ({
|
|
payload: [
|
|
{ key: "DATABASE_URL", value: "postgres://hivegrid_app:runtime-secret-sentinel@db.example.test/hivegrid" },
|
|
{ key: "DATABASE_RUNTIME_ROLE", value: "hivegrid_app" },
|
|
{ key: "DATABASE_POOL_MAX", value: "10" },
|
|
{ key: "RLS_RUNTIME_MODE", value: "required" },
|
|
{ key: "WEB_RUNTIME_SURFACE", value: "admin" },
|
|
{ key: "SESSION_SECRET", value: "dedicated-admin-session-secret-sentinel" },
|
|
{ key: "ADMIN_FALLBACK_LOGIN_ENABLED", value: "false" },
|
|
...authentikEntries,
|
|
],
|
|
}));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-runtime-db-env", adminResourceUuid, "hivegrid_app", "admin");
|
|
assert.equal(result.status, 1);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /secret-sentinel/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("rejects every Authentik Admin secret outside the admin resource", async () => {
|
|
for (const authentikEntry of [
|
|
{ key: "AUTHENTIK_ADMIN_CLIENT_SECRET", value: "oidc-secret-sentinel" },
|
|
{
|
|
key: "AUTHENTIK_ADMIN_JIT_DATABASE_URL",
|
|
value: "postgres://hivegrid_authentik_jit:jit-secret-sentinel@db.example.test/hivegrid",
|
|
},
|
|
]) {
|
|
const server = await startServer(() => ({
|
|
payload: [
|
|
{ key: "DATABASE_URL", value: "postgres://hivegrid_worker:runtime-secret-sentinel@db.example.test/hivegrid" },
|
|
{ key: "DATABASE_RUNTIME_ROLE", value: "hivegrid_worker" },
|
|
{ key: "RLS_RUNTIME_MODE", value: "required" },
|
|
authentikEntry,
|
|
],
|
|
}));
|
|
try {
|
|
const result = await runHelper(server.url, "assert-runtime-db-env", workerResourceUuid, "hivegrid_worker", "service");
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /restricted to the admin resource/);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /secret-sentinel/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("resolves exactly the named hivegrid-web, hivegrid-admin, and hivegrid-portal resources sharing the web image", async () => {
|
|
const server = await startServer((request) => ({
|
|
payload:
|
|
request.url === "/applications"
|
|
? [
|
|
{
|
|
uuid: webResourceUuid,
|
|
name: "hivegrid-web",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
{
|
|
uuid: adminResourceUuid,
|
|
name: "hivegrid-admin",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
{
|
|
uuid: portalResourceUuid,
|
|
name: "hivegrid-portal",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
{ uuid: otherResourceUuid, name: "hivegrid-api", docker_registry_image_name: apiImage },
|
|
]
|
|
: [],
|
|
}));
|
|
try {
|
|
const result = await runHelper(
|
|
server.url,
|
|
"resolve-web-surfaces",
|
|
`${webResourceUuid},${adminResourceUuid},${portalResourceUuid},${otherResourceUuid}`,
|
|
webImage
|
|
);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(result.stdout, `${webResourceUuid},${adminResourceUuid},${portalResourceUuid}`);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method, url }) => `${method} ${url}`),
|
|
[
|
|
"GET /applications",
|
|
`GET /applications/${webResourceUuid}/envs`,
|
|
`GET /applications/${adminResourceUuid}/envs`,
|
|
`GET /applications/${portalResourceUuid}/envs`,
|
|
]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects a Dockerfile web surface because digest pinning requires Coolify dockerimage mode", async () => {
|
|
const server = await startServer((request) => ({
|
|
payload:
|
|
request.url === "/applications"
|
|
? [
|
|
{
|
|
uuid: webResourceUuid,
|
|
name: "hivegrid-web",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
{
|
|
uuid: adminResourceUuid,
|
|
name: "hivegrid-admin",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
{
|
|
uuid: portalResourceUuid,
|
|
name: "hivegrid-portal",
|
|
build_pack: "dockerfile",
|
|
docker_registry_image_name: webImage,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
]
|
|
: [],
|
|
}));
|
|
try {
|
|
const result = await runHelper(server.url, "resolve-web-surfaces", `${webResourceUuid},${adminResourceUuid},${portalResourceUuid}`, webImage);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /must use Coolify dockerimage mode for OCI digest pinning/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method, url }) => `${method} ${url}`),
|
|
["GET /applications"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("requires independent positive CPU and unit-suffixed memory hard caps during web-trio inventory", async () => {
|
|
const invalidLimits = [
|
|
{ limits_cpus: undefined },
|
|
{ limits_cpus: "0" },
|
|
{ limits_cpus: "Infinity" },
|
|
{ limits_memory: undefined },
|
|
{ limits_memory: "0" },
|
|
{ limits_memory: "0m" },
|
|
{ limits_memory: "512" },
|
|
{ limits_memory: ".5g" },
|
|
{ limits_memory: "1.5g" },
|
|
{ limits_memory: "512mb" },
|
|
];
|
|
|
|
for (const target of ["core", "admin", "portal"]) {
|
|
for (const invalid of invalidLimits) {
|
|
const apps = [
|
|
{
|
|
uuid: webResourceUuid,
|
|
name: "hivegrid-web",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
{
|
|
uuid: adminResourceUuid,
|
|
name: "hivegrid-admin",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
{
|
|
uuid: portalResourceUuid,
|
|
name: "hivegrid-portal",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
];
|
|
Object.assign(apps[target === "core" ? 0 : target === "admin" ? 1 : 2], invalid);
|
|
const server = await startServer(() => ({ payload: apps }));
|
|
try {
|
|
const result = await runHelper(server.url, "resolve-web-surfaces", `${webResourceUuid},${adminResourceUuid},${portalResourceUuid}`, webImage);
|
|
assert.equal(result.status, 1, `${target} unexpectedly accepted ${JSON.stringify(invalid)}`);
|
|
assert.match(result.stderr, /positive limits_cpus and unit-suffixed limits_memory hard caps/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method, url }) => `${method} ${url}`),
|
|
["GET /applications"]
|
|
);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it("rejects image-first, missing, extra, or revision-overridden web surface inventories", async () => {
|
|
const cases = [
|
|
[
|
|
{ uuid: webResourceUuid, name: "some-web", docker_registry_image_name: webImage },
|
|
{ uuid: portalResourceUuid, name: "hivegrid-portal", docker_registry_image_name: webImage },
|
|
],
|
|
[{ uuid: webResourceUuid, name: "hivegrid-web", docker_registry_image_name: webImage }],
|
|
[
|
|
{ uuid: webResourceUuid, name: "hivegrid-web", docker_registry_image_name: webImage },
|
|
{ uuid: portalResourceUuid, name: "hivegrid-portal", docker_registry_image_name: webImage },
|
|
{ uuid: otherResourceUuid, name: "hivegrid-web-copy", docker_registry_image_name: webImage },
|
|
],
|
|
];
|
|
for (const apps of cases) {
|
|
const server = await startServer((request) => ({ payload: request.url === "/applications" ? apps : [] }));
|
|
try {
|
|
const result = await runHelper(server.url, "resolve-web-surfaces", `${webResourceUuid},${portalResourceUuid},${otherResourceUuid}`, webImage);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /exactly the explicit hivegrid-web, hivegrid-admin, and hivegrid-portal/);
|
|
assert.equal(server.requests.filter(({ method }) => method !== "GET").length, 0);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
|
|
const overrideServer = await startServer((request) => ({
|
|
payload:
|
|
request.url === "/applications"
|
|
? [
|
|
{
|
|
uuid: webResourceUuid,
|
|
name: "hivegrid-web",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
{
|
|
uuid: adminResourceUuid,
|
|
name: "hivegrid-admin",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
{
|
|
uuid: portalResourceUuid,
|
|
name: "hivegrid-portal",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
]
|
|
: request.url === `/applications/${portalResourceUuid}/envs`
|
|
? [{ key: "APP_REVISION", value: "" }]
|
|
: [],
|
|
}));
|
|
try {
|
|
const result = await runHelper(overrideServer.url, "resolve-web-surfaces", `${webResourceUuid},${adminResourceUuid},${portalResourceUuid}`, webImage);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /web surface APP_REVISION overrides/);
|
|
} finally {
|
|
await overrideServer.close();
|
|
}
|
|
});
|
|
|
|
it("patches and rereads all three explicit web surfaces with one full SHA and digest", async () => {
|
|
const apps = new Map([
|
|
[
|
|
webResourceUuid,
|
|
{
|
|
uuid: webResourceUuid,
|
|
name: "hivegrid-web",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
docker_registry_image_tag: "e".repeat(40),
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
],
|
|
[
|
|
adminResourceUuid,
|
|
{
|
|
uuid: adminResourceUuid,
|
|
name: "hivegrid-admin",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
docker_registry_image_tag: "e".repeat(40),
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
],
|
|
[
|
|
portalResourceUuid,
|
|
{
|
|
uuid: portalResourceUuid,
|
|
name: "hivegrid-portal",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
docker_registry_image_tag: "e".repeat(40),
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
],
|
|
]);
|
|
const server = await startServer(async (request) => {
|
|
const uuid = request.url.split("/").at(-1);
|
|
const app = apps.get(uuid);
|
|
if (request.method === "PATCH") {
|
|
Object.assign(app, JSON.parse(request.body));
|
|
}
|
|
return { payload: app };
|
|
});
|
|
try {
|
|
const result = await runHelper(
|
|
server.url,
|
|
"set-web-surface-release",
|
|
webResourceUuid,
|
|
adminResourceUuid,
|
|
portalResourceUuid,
|
|
webImage,
|
|
deployRevision,
|
|
webDigest
|
|
);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, new RegExp(`${webImage}@${webDigest}`));
|
|
const patches = server.requests.filter(({ method }) => method === "PATCH");
|
|
assert.equal(patches.length, 3);
|
|
for (const patchRequest of patches) {
|
|
assert.deepEqual(JSON.parse(patchRequest.body), {
|
|
pre_deployment_command: "",
|
|
post_deployment_command: "",
|
|
docker_registry_image_tag: `sha256-${"f".repeat(64)}`,
|
|
});
|
|
}
|
|
assert.equal(apps.get(webResourceUuid).docker_registry_image_tag, `sha256-${"f".repeat(64)}`);
|
|
assert.equal(apps.get(adminResourceUuid).docker_registry_image_tag, `sha256-${"f".repeat(64)}`);
|
|
assert.equal(apps.get(portalResourceUuid).docker_registry_image_tag, `sha256-${"f".repeat(64)}`);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects missing hard caps when explicit web UUIDs are reread before release mutation", async () => {
|
|
const apps = new Map([
|
|
[
|
|
webResourceUuid,
|
|
{
|
|
uuid: webResourceUuid,
|
|
name: "hivegrid-web",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
],
|
|
[
|
|
adminResourceUuid,
|
|
{
|
|
uuid: adminResourceUuid,
|
|
name: "hivegrid-admin",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
],
|
|
[
|
|
portalResourceUuid,
|
|
{
|
|
uuid: portalResourceUuid,
|
|
name: "hivegrid-portal",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
limits_cpus: "1",
|
|
limits_memory: "0",
|
|
},
|
|
],
|
|
]);
|
|
const server = await startServer((request) => ({ payload: apps.get(request.url.split("/").at(-1)) }));
|
|
try {
|
|
const result = await runHelper(
|
|
server.url,
|
|
"set-web-surface-release",
|
|
webResourceUuid,
|
|
adminResourceUuid,
|
|
portalResourceUuid,
|
|
webImage,
|
|
deployRevision,
|
|
webDigest
|
|
);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /positive limits_cpus and unit-suffixed limits_memory hard caps/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET", "GET", "GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("proves all three completed web deployments are healthy and pinned to the same registry digest", async () => {
|
|
const digestTag = `sha256-${"f".repeat(64)}`;
|
|
const server = await startServer((request) => {
|
|
if (request.url === `/applications/${webResourceUuid}`) {
|
|
return {
|
|
payload: {
|
|
id: 101,
|
|
uuid: webResourceUuid,
|
|
name: "hivegrid-web",
|
|
build_pack: "dockerimage",
|
|
status: "running:healthy",
|
|
docker_registry_image_name: webImage,
|
|
docker_registry_image_tag: digestTag,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
};
|
|
}
|
|
if (request.url === `/applications/${adminResourceUuid}`) {
|
|
return {
|
|
payload: {
|
|
id: 103,
|
|
uuid: adminResourceUuid,
|
|
name: "hivegrid-admin",
|
|
build_pack: "dockerimage",
|
|
status: "running:healthy",
|
|
docker_registry_image_name: webImage,
|
|
docker_registry_image_tag: digestTag,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
};
|
|
}
|
|
if (request.url === `/applications/${portalResourceUuid}`) {
|
|
return {
|
|
payload: {
|
|
id: 102,
|
|
uuid: portalResourceUuid,
|
|
name: "hivegrid-portal",
|
|
build_pack: "dockerimage",
|
|
status: "running:healthy",
|
|
docker_registry_image_name: webImage,
|
|
docker_registry_image_tag: digestTag,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
};
|
|
}
|
|
if (request.url === `/deployments/${deploymentUuid}`) {
|
|
return {
|
|
payload: {
|
|
application_id: "101",
|
|
application_name: "hivegrid-web",
|
|
deployment_uuid: deploymentUuid,
|
|
status: "finished",
|
|
},
|
|
};
|
|
}
|
|
if (request.url === `/deployments/${otherDeploymentUuid}`) {
|
|
return {
|
|
payload: {
|
|
application_id: "102",
|
|
application_name: "hivegrid-portal",
|
|
deployment_uuid: otherDeploymentUuid,
|
|
status: "finished",
|
|
},
|
|
};
|
|
}
|
|
if (request.url === `/deployments/${adminDeploymentUuid}`) {
|
|
return {
|
|
payload: {
|
|
application_id: "103",
|
|
application_name: "hivegrid-admin",
|
|
deployment_uuid: adminDeploymentUuid,
|
|
status: "finished",
|
|
},
|
|
};
|
|
}
|
|
return { status: 404, payload: {} };
|
|
});
|
|
try {
|
|
const result = await runHelper(
|
|
server.url,
|
|
"assert-web-surface-release",
|
|
webResourceUuid,
|
|
adminResourceUuid,
|
|
portalResourceUuid,
|
|
webImage,
|
|
deployRevision,
|
|
webDigest,
|
|
deploymentUuid,
|
|
adminDeploymentUuid,
|
|
otherDeploymentUuid
|
|
);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, new RegExp(`${webImage}:${deployRevision} at ${webImage}@${webDigest}`));
|
|
const requests = server.requests.map(({ method, url }) => `${method} ${url}`);
|
|
assert.deepEqual(
|
|
requests.slice(0, 3).sort(),
|
|
[`GET /applications/${webResourceUuid}`, `GET /applications/${adminResourceUuid}`, `GET /applications/${portalResourceUuid}`].sort()
|
|
);
|
|
assert.deepEqual(
|
|
requests.slice(3).sort(),
|
|
[`GET /deployments/${deploymentUuid}`, `GET /deployments/${adminDeploymentUuid}`, `GET /deployments/${otherDeploymentUuid}`].sort()
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects digest drift and a deployment that is not bound to the expected surface", async () => {
|
|
for (const drift of ["digest", "deployment"]) {
|
|
const digestTag = `sha256-${"f".repeat(64)}`;
|
|
const server = await startServer((request) => {
|
|
const isPortal = request.url === `/applications/${portalResourceUuid}`;
|
|
const isAdmin = request.url === `/applications/${adminResourceUuid}`;
|
|
if (request.url === `/applications/${webResourceUuid}` || isAdmin || isPortal) {
|
|
return {
|
|
payload: {
|
|
id: isPortal ? 102 : isAdmin ? 103 : 101,
|
|
uuid: isPortal ? portalResourceUuid : isAdmin ? adminResourceUuid : webResourceUuid,
|
|
name: isPortal ? "hivegrid-portal" : isAdmin ? "hivegrid-admin" : "hivegrid-web",
|
|
build_pack: "dockerimage",
|
|
status: "running:healthy",
|
|
docker_registry_image_name: webImage,
|
|
docker_registry_image_tag: drift === "digest" && isPortal ? `sha256-${"e".repeat(64)}` : digestTag,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
};
|
|
}
|
|
const isPortalDeployment = request.url === `/deployments/${otherDeploymentUuid}`;
|
|
const isAdminDeployment = request.url === `/deployments/${adminDeploymentUuid}`;
|
|
return {
|
|
payload: {
|
|
application_id: isPortalDeployment ? "102" : isAdminDeployment ? "103" : "101",
|
|
application_name: drift === "deployment" && isPortalDeployment ? "hivegrid-web" : isPortalDeployment ? "hivegrid-portal" : "hivegrid-web",
|
|
...(isAdminDeployment ? { application_name: "hivegrid-admin" } : {}),
|
|
deployment_uuid: isPortalDeployment ? otherDeploymentUuid : isAdminDeployment ? adminDeploymentUuid : deploymentUuid,
|
|
status: "finished",
|
|
},
|
|
};
|
|
});
|
|
try {
|
|
const result = await runHelper(
|
|
server.url,
|
|
"assert-web-surface-release",
|
|
webResourceUuid,
|
|
adminResourceUuid,
|
|
portalResourceUuid,
|
|
webImage,
|
|
deployRevision,
|
|
webDigest,
|
|
deploymentUuid,
|
|
adminDeploymentUuid,
|
|
otherDeploymentUuid
|
|
);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, drift === "digest" ? /not pinned to the expected registry digest/ : /expected named application/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("compensates the first web surface when the paired release mutation cannot update the second", async () => {
|
|
const previousRevision = "e".repeat(40);
|
|
const apps = new Map([
|
|
[
|
|
webResourceUuid,
|
|
{
|
|
uuid: webResourceUuid,
|
|
name: "hivegrid-web",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
docker_registry_image_tag: previousRevision,
|
|
...validWebSurfaceResourceLimits,
|
|
pre_deployment_command: "old-pre",
|
|
post_deployment_command: "old-post",
|
|
},
|
|
],
|
|
[
|
|
adminResourceUuid,
|
|
{
|
|
uuid: adminResourceUuid,
|
|
name: "hivegrid-admin",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
docker_registry_image_tag: previousRevision,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
],
|
|
[
|
|
portalResourceUuid,
|
|
{
|
|
uuid: portalResourceUuid,
|
|
name: "hivegrid-portal",
|
|
build_pack: "dockerimage",
|
|
docker_registry_image_name: webImage,
|
|
docker_registry_image_tag: previousRevision,
|
|
...validWebSurfaceResourceLimits,
|
|
},
|
|
],
|
|
]);
|
|
let portalPatchFailed = false;
|
|
const server = await startServer((request) => {
|
|
const uuid = request.url.split("/").at(-1);
|
|
const app = apps.get(uuid);
|
|
if (request.method === "PATCH" && uuid === portalResourceUuid && !portalPatchFailed) {
|
|
portalPatchFailed = true;
|
|
return { status: 500, payload: { message: "sentinel must stay redacted" } };
|
|
}
|
|
if (request.method === "PATCH") Object.assign(app, JSON.parse(request.body));
|
|
return { payload: app };
|
|
});
|
|
try {
|
|
const result = await runHelper(
|
|
server.url,
|
|
"set-web-surface-release",
|
|
webResourceUuid,
|
|
adminResourceUuid,
|
|
portalResourceUuid,
|
|
webImage,
|
|
deployRevision,
|
|
webDigest
|
|
);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /previous resource configuration was restored/);
|
|
assert.equal(apps.get(webResourceUuid).docker_registry_image_tag, previousRevision);
|
|
assert.equal(apps.get(webResourceUuid).pre_deployment_command, "old-pre");
|
|
assert.equal(apps.get(webResourceUuid).post_deployment_command, "old-post");
|
|
assert.equal(apps.get(adminResourceUuid).docker_registry_image_tag, previousRevision);
|
|
assert.equal(server.requests.filter(({ method }) => method === "PATCH").length, 5);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("validates the exact allowlisted web, API, and MCP image inventory before mutations", async () => {
|
|
const server = await startServer((request) => ({
|
|
payload:
|
|
request.url === "/applications"
|
|
? [
|
|
{ uuid: resourceUuid, docker_registry_image_name: webImage },
|
|
{ uuid: otherResourceUuid, docker_registry_image_name: apiImage },
|
|
{ uuid: mcpResourceUuid, docker_registry_image_name: mcpImage },
|
|
{ uuid: webResourceUuid, docker_registry_image_name: mcpImage },
|
|
]
|
|
: [],
|
|
}));
|
|
try {
|
|
const result = await runHelper(
|
|
server.url,
|
|
"validate-required-images",
|
|
`${resourceUuid},${otherResourceUuid},${mcpResourceUuid}`,
|
|
webImage,
|
|
apiImage,
|
|
mcpImage
|
|
);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /inventory validated/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method, url }) => ({ method, url })),
|
|
[
|
|
{ method: "GET", url: "/applications" },
|
|
{ method: "GET", url: `/applications/${resourceUuid}/envs` },
|
|
{ method: "GET", url: `/applications/${otherResourceUuid}/envs` },
|
|
{ method: "GET", url: `/applications/${mcpResourceUuid}/envs` },
|
|
]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects core APP_REVISION overrides during the read-only inventory preflight", async () => {
|
|
const server = await startServer((request) => ({
|
|
payload:
|
|
request.url === "/applications"
|
|
? [
|
|
{ uuid: resourceUuid, docker_registry_image_name: webImage },
|
|
{ uuid: otherResourceUuid, docker_registry_image_name: apiImage },
|
|
{ uuid: mcpResourceUuid, docker_registry_image_name: mcpImage },
|
|
]
|
|
: request.url === `/applications/${mcpResourceUuid}/envs`
|
|
? [{ key: "APP_REVISION", value: "" }]
|
|
: [],
|
|
}));
|
|
try {
|
|
const result = await runHelper(
|
|
server.url,
|
|
"validate-required-images",
|
|
`${resourceUuid},${otherResourceUuid},${mcpResourceUuid}`,
|
|
webImage,
|
|
apiImage,
|
|
mcpImage
|
|
);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /core application APP_REVISION overrides must be removed/);
|
|
assert.equal(server.requests.filter(({ method }) => method !== "GET").length, 0);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("fails required image inventory validation when a target is missing or ambiguous", async () => {
|
|
const server = await startServer(() => ({
|
|
payload: [
|
|
{ uuid: resourceUuid, docker_registry_image_name: webImage },
|
|
{ uuid: otherResourceUuid, docker_registry_image_name: apiImage },
|
|
{ uuid: mcpResourceUuid, docker_registry_image_name: apiImage },
|
|
],
|
|
}));
|
|
try {
|
|
const result = await runHelper(
|
|
server.url,
|
|
"validate-required-images",
|
|
`${resourceUuid},${otherResourceUuid},${mcpResourceUuid}`,
|
|
webImage,
|
|
apiImage,
|
|
mcpImage
|
|
);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.stdout, "");
|
|
assert.match(result.stderr, /inventory is missing, ambiguous, or invalid/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
for (const [order, entries] of [
|
|
[
|
|
"preview-first",
|
|
[
|
|
{ key: "MIGRATE_ON_BOOT", value: "true", is_preview: true },
|
|
{ key: "MIGRATE_ON_BOOT", value: "false", is_preview: false },
|
|
],
|
|
],
|
|
[
|
|
"runtime-first",
|
|
[
|
|
{ key: "MIGRATE_ON_BOOT", value: "false", is_preview: false },
|
|
{ key: "MIGRATE_ON_BOOT", value: "true", is_preview: true },
|
|
],
|
|
],
|
|
]) {
|
|
it(`updates the non-preview runtime env independent of ${order} Coolify ordering`, async () => {
|
|
const server = await startServer((request) => ({ payload: request.method === "GET" ? entries : {} }));
|
|
try {
|
|
const result = await runHelper(server.url, "set-envs", webResourceUuid, "MIGRATE_ON_BOOT=true");
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /env MIGRATE_ON_BOOT updated/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET", "PATCH"]
|
|
);
|
|
assert.deepEqual(JSON.parse(server.requests[1].body), {
|
|
key: "MIGRATE_ON_BOOT",
|
|
value: "true",
|
|
is_literal: true,
|
|
is_preview: false,
|
|
is_multiline: false,
|
|
is_shown_once: false,
|
|
});
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
}
|
|
|
|
it("rejects duplicate non-preview env keys before set-envs mutation", async () => {
|
|
const server = await startServer(() => ({
|
|
payload: [
|
|
{ key: "MIGRATE_ON_BOOT", value: "false", is_preview: false },
|
|
{ key: "MIGRATE_ON_BOOT", value: "true", is_preview: false },
|
|
],
|
|
}));
|
|
try {
|
|
const result = await runHelper(server.url, "set-envs", webResourceUuid, "MIGRATE_ON_BOOT=true");
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /duplicate non-preview keys/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("configures MCP command health and verifies the persisted state", async () => {
|
|
const app = {
|
|
uuid: mcpResourceUuid,
|
|
docker_registry_image_name: mcpImage,
|
|
docker_registry_image_tag: "previous",
|
|
health_check_enabled: false,
|
|
custom_healthcheck_found: false,
|
|
};
|
|
const server = await startServer((request) => {
|
|
if (request.method === "GET" && request.url.endsWith("/envs")) {
|
|
return { payload: [] };
|
|
}
|
|
if (request.method === "GET") {
|
|
return { payload: app };
|
|
}
|
|
if (request.method === "PATCH") {
|
|
Object.assign(app, JSON.parse(request.body));
|
|
return { payload: app };
|
|
}
|
|
return { status: 405, payload: { message: "method not allowed" } };
|
|
});
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "configure-mcp-healthcheck", mcpResourceUuid, deployRevision);
|
|
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(result.stderr, "");
|
|
assertSecretSafe(result);
|
|
assert.deepEqual(server.serverErrors, []);
|
|
assert.equal(server.requests.length, 4);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method, url, authorization }) => ({ method, url, authorization })),
|
|
[
|
|
{ method: "GET", url: `/applications/${mcpResourceUuid}/envs`, authorization: `Bearer ${bearerToken}` },
|
|
{ method: "GET", url: `/applications/${mcpResourceUuid}`, authorization: `Bearer ${bearerToken}` },
|
|
{ method: "PATCH", url: `/applications/${mcpResourceUuid}`, authorization: `Bearer ${bearerToken}` },
|
|
{ method: "GET", url: `/applications/${mcpResourceUuid}`, authorization: `Bearer ${bearerToken}` },
|
|
]
|
|
);
|
|
assert.equal(server.requests[0].body, "");
|
|
assert.equal(server.requests[1].body, "");
|
|
assert.deepEqual(JSON.parse(server.requests[2].body), expectedMcpHealthConfig());
|
|
assert.equal(server.requests[3].body, "");
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("does not patch MCP health config that already matches the immutable revision", async () => {
|
|
const server = await startServer((request) => ({
|
|
payload: request.url.endsWith("/envs")
|
|
? []
|
|
: { uuid: mcpResourceUuid, docker_registry_image_name: mcpImage, ...expectedMcpHealthConfig(), custom_healthcheck_found: false },
|
|
}));
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "configure-mcp-healthcheck", mcpResourceUuid, deployRevision);
|
|
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(result.stderr, "");
|
|
assertSecretSafe(result);
|
|
assert.deepEqual(server.serverErrors, []);
|
|
assert.deepEqual(server.requests, [
|
|
{
|
|
method: "GET",
|
|
url: `/applications/${mcpResourceUuid}/envs`,
|
|
authorization: `Bearer ${bearerToken}`,
|
|
body: "",
|
|
},
|
|
{
|
|
method: "GET",
|
|
url: `/applications/${mcpResourceUuid}`,
|
|
authorization: `Bearer ${bearerToken}`,
|
|
body: "",
|
|
},
|
|
]);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("fails when MCP command health config is not persisted after the patch", async () => {
|
|
const server = await startServer((request) => ({
|
|
payload: request.url.endsWith("/envs")
|
|
? []
|
|
: {
|
|
uuid: mcpResourceUuid,
|
|
docker_registry_image_name: mcpImage,
|
|
docker_registry_image_tag: "previous",
|
|
health_check_enabled: false,
|
|
custom_healthcheck_found: false,
|
|
},
|
|
}));
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "configure-mcp-healthcheck", mcpResourceUuid, deployRevision);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.stdout, "");
|
|
assert.match(result.stderr, /configuration was not persisted/);
|
|
assertSecretSafe(result);
|
|
assert.deepEqual(server.serverErrors, []);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET", "GET", "PATCH", "GET"]
|
|
);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects MCP APP_REVISION overrides before mutating application config", async () => {
|
|
const server = await startServer((request) => ({
|
|
payload: request.url.endsWith("/envs") ? [{ key: "APP_REVISION", real_value: "" }] : {},
|
|
}));
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "configure-mcp-healthcheck", mcpResourceUuid, deployRevision);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.stdout, "");
|
|
assert.match(result.stderr, /APP_REVISION override must be removed/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects malformed MCP environment responses before mutating application config", async () => {
|
|
const server = await startServer(() => ({ payload: { unexpected: [] } }));
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "configure-mcp-healthcheck", mcpResourceUuid, deployRevision);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.stdout, "");
|
|
assert.match(result.stderr, /environment response was malformed/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects MCP images whose built-in healthcheck would override command health", async () => {
|
|
const server = await startServer((request) => ({
|
|
payload: request.url.endsWith("/envs") ? [] : { uuid: mcpResourceUuid, docker_registry_image_name: mcpImage, custom_healthcheck_found: true },
|
|
}));
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "configure-mcp-healthcheck", mcpResourceUuid, deployRevision);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.stdout, "");
|
|
assert.match(result.stderr, /image healthcheck must be absent/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET", "GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects an MCP CUID that resolves to a different image before mutation", async () => {
|
|
const server = await startServer((request) => ({
|
|
payload: request.url.endsWith("/envs") ? [] : { uuid: mcpResourceUuid, docker_registry_image_name: webImage, custom_healthcheck_found: false },
|
|
}));
|
|
try {
|
|
const result = await runHelperWithEnv(server.url, { MCP_IMAGE: mcpImage }, "configure-mcp-healthcheck", mcpResourceUuid, deployRevision);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /does not use the expected MCP image/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET", "GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects MCP targeting when no exact expected image identity is configured", async () => {
|
|
const server = await startServer((request) => ({
|
|
payload: request.url.endsWith("/envs") ? [] : { uuid: mcpResourceUuid, docker_registry_image_name: mcpImage, custom_healthcheck_found: false },
|
|
}));
|
|
try {
|
|
const result = await runHelperWithEnv(
|
|
server.url,
|
|
{ MCP_IMAGE: "", WORKER_IMAGE: "", REGISTRY: "", IMAGE_NAMESPACE: "", IMAGE_PREFIX: "" },
|
|
"configure-mcp-healthcheck",
|
|
mcpResourceUuid,
|
|
deployRevision
|
|
);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /does not use the expected MCP image/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET", "GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects a mutable MCP health revision before calling Coolify", async () => {
|
|
const server = await startServer(() => ({ payload: {} }));
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "configure-mcp-healthcheck", mcpResourceUuid, "staging");
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.stdout, "");
|
|
assert.match(result.stderr, /full lowercase Git SHA/);
|
|
assert.deepEqual(server.requests, []);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("atomically configures and rereads degraded legacy rollback health", async () => {
|
|
const app = {
|
|
uuid: workerResourceUuid,
|
|
name: "legacy-worker",
|
|
docker_registry_image_name: workerImage,
|
|
docker_registry_image_tag: "newer",
|
|
health_check_enabled: true,
|
|
health_check_command: "new-command",
|
|
};
|
|
const server = await startServer((request) => {
|
|
if (request.method === "GET" && request.url.endsWith("/envs")) {
|
|
return { payload: [{ key: "WORKER_QUEUE_GROUP", real_value: "'mail'" }] };
|
|
}
|
|
if (request.method === "PATCH") {
|
|
Object.assign(app, JSON.parse(request.body));
|
|
return { payload: app };
|
|
}
|
|
if (request.method === "GET") {
|
|
return { payload: app };
|
|
}
|
|
return { status: 405, payload: {} };
|
|
});
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "configure-legacy-rollback-healthcheck", workerResourceUuid, deployRevision, "mail");
|
|
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(result.stderr, "");
|
|
assert.match(result.stdout, /Legacy rollback image health configured/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET", "GET", "PATCH", "GET"]
|
|
);
|
|
assert.deepEqual(JSON.parse(server.requests[2].body), {
|
|
docker_registry_image_tag: deployRevision,
|
|
health_check_enabled: false,
|
|
health_check_command: "",
|
|
});
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
for (const [order, envs] of [
|
|
[
|
|
"preview-first",
|
|
[
|
|
{ key: "WORKER_QUEUE_GROUP", value: "general", is_preview: true },
|
|
{ key: "WORKER_QUEUE_GROUP", value: "mail", is_preview: false },
|
|
],
|
|
],
|
|
[
|
|
"runtime-first",
|
|
[
|
|
{ key: "WORKER_QUEUE_GROUP", value: "mail", is_preview: false },
|
|
{ key: "WORKER_QUEUE_GROUP", value: "general", is_preview: true },
|
|
],
|
|
],
|
|
]) {
|
|
it(`uses the non-preview worker group for legacy rollback with ${order} env ordering`, async () => {
|
|
const app = {
|
|
uuid: workerResourceUuid,
|
|
docker_registry_image_name: workerImage,
|
|
docker_registry_image_tag: "newer",
|
|
health_check_enabled: true,
|
|
health_check_command: "new-command",
|
|
};
|
|
const server = await startServer((request) => {
|
|
if (request.url.endsWith("/envs")) return { payload: envs };
|
|
if (request.method === "PATCH") {
|
|
Object.assign(app, JSON.parse(request.body));
|
|
return { payload: app };
|
|
}
|
|
return { payload: app };
|
|
});
|
|
try {
|
|
const result = await runHelper(server.url, "configure-legacy-rollback-healthcheck", workerResourceUuid, deployRevision, "mail");
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET", "GET", "PATCH", "GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
}
|
|
|
|
it("does not let a preview worker group authorize legacy runtime rollback", async () => {
|
|
const server = await startServer((request) => ({
|
|
payload: request.url.endsWith("/envs")
|
|
? [
|
|
{ key: "WORKER_QUEUE_GROUP", value: "mail", is_preview: true },
|
|
{ key: "WORKER_QUEUE_GROUP", value: "general", is_preview: false },
|
|
]
|
|
: { docker_registry_image_name: workerImage },
|
|
}));
|
|
try {
|
|
const result = await runHelper(server.url, "configure-legacy-rollback-healthcheck", workerResourceUuid, deployRevision, "mail");
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /worker group does not match/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET", "GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("fails closed when legacy rollback health config is not persisted", async () => {
|
|
const server = await startServer((request) => ({
|
|
payload: request.url.endsWith("/envs")
|
|
? []
|
|
: {
|
|
name: "legacy-mcp",
|
|
docker_registry_image_name: mcpImage,
|
|
docker_registry_image_tag: "newer",
|
|
health_check_enabled: true,
|
|
health_check_command: "new-command",
|
|
},
|
|
}));
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "configure-legacy-rollback-healthcheck", legacyMcpResourceUuid, deployRevision);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.stdout, "");
|
|
assert.match(result.stderr, /configuration was not persisted/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET", "GET", "PATCH", "GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects legacy rollback revision overrides and worker-group drift before mutation", async () => {
|
|
const overrideServer = await startServer((request) => ({
|
|
payload: request.url.endsWith("/envs") ? [{ key: "APP_REVISION", value: "" }] : {},
|
|
}));
|
|
try {
|
|
const result = await runHelper(overrideServer.url, "configure-legacy-rollback-healthcheck", workerResourceUuid, deployRevision, "mail");
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /APP_REVISION override must be removed/);
|
|
assert.deepEqual(
|
|
overrideServer.requests.map(({ method }) => method),
|
|
["GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await overrideServer.close();
|
|
}
|
|
|
|
const groupServer = await startServer((request) => ({
|
|
payload: request.url.endsWith("/envs")
|
|
? [{ key: "WORKER_QUEUE_GROUP", value: "general" }]
|
|
: { name: "legacy-worker", docker_registry_image_name: workerImage },
|
|
}));
|
|
try {
|
|
const result = await runHelper(groupServer.url, "configure-legacy-rollback-healthcheck", workerResourceUuid, deployRevision, "mail");
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /worker group does not match/);
|
|
assert.deepEqual(
|
|
groupServer.requests.map(({ method }) => method),
|
|
["GET", "GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await groupServer.close();
|
|
}
|
|
});
|
|
|
|
it("requires a group for legacy workers and rejects non-MCP/non-worker resources", async () => {
|
|
const workerServer = await startServer((request) => ({
|
|
payload: request.url.endsWith("/envs")
|
|
? [{ key: "WORKER_QUEUE_GROUP", value: "mail" }]
|
|
: { name: "legacy-worker", docker_registry_image_name: workerImage },
|
|
}));
|
|
try {
|
|
const result = await runHelper(workerServer.url, "configure-legacy-rollback-healthcheck", workerResourceUuid, deployRevision);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /requires the exact worker group/);
|
|
assert.deepEqual(
|
|
workerServer.requests.map(({ method }) => method),
|
|
["GET", "GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await workerServer.close();
|
|
}
|
|
|
|
const webServer = await startServer((request) => ({
|
|
payload: request.url.endsWith("/envs") ? [] : { name: "legacy-worker-mcp", description: "worker mcp", docker_registry_image_name: webImage },
|
|
}));
|
|
try {
|
|
const result = await runHelper(webServer.url, "configure-legacy-rollback-healthcheck", webResourceUuid, deployRevision);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /restricted to MCP and worker resources/);
|
|
assert.deepEqual(
|
|
webServer.requests.map(({ method }) => method),
|
|
["GET", "GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await webServer.close();
|
|
}
|
|
});
|
|
|
|
it("does not infer a legacy rollback target from spoofed names or descriptions", async () => {
|
|
const server = await startServer((request) => ({
|
|
payload: request.url.endsWith("/envs")
|
|
? [{ key: "WORKER_QUEUE_GROUP", value: "mail" }]
|
|
: { name: "worker-mail", description: "legacy mcp worker", docker_registry_image_name: webImage },
|
|
}));
|
|
try {
|
|
const result = await runHelper(server.url, "configure-legacy-rollback-healthcheck", webResourceUuid, deployRevision, "mail");
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /restricted to MCP and worker resources/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET", "GET"]
|
|
);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects a different product's worker image before legacy rollback mutation", async () => {
|
|
const server = await startServer((request) => ({
|
|
payload: request.url.endsWith("/envs")
|
|
? [{ key: "WORKER_QUEUE_GROUP", value: "mail" }]
|
|
: { name: "worker-mail", docker_registry_image_name: "registry.example.test/other-product/worker" },
|
|
}));
|
|
try {
|
|
const result = await runHelper(server.url, "configure-legacy-rollback-healthcheck", workerResourceUuid, deployRevision, "mail");
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /restricted to MCP and worker resources/);
|
|
assert.deepEqual(
|
|
server.requests.map(({ method }) => method),
|
|
["GET", "GET"]
|
|
);
|
|
assert.equal(server.requests.filter(({ method }) => method === "PATCH").length, 0);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("rejects mutable legacy rollback revisions and invalid worker groups without calling Coolify", async () => {
|
|
const server = await startServer(() => ({ payload: {} }));
|
|
|
|
try {
|
|
const mutableResult = await runHelper(server.url, "configure-legacy-rollback-healthcheck", workerResourceUuid, "main", "mail");
|
|
assert.equal(mutableResult.status, 1);
|
|
assert.match(mutableResult.stderr, /full lowercase Git SHA/);
|
|
|
|
const groupResult = await runHelper(server.url, "configure-legacy-rollback-healthcheck", workerResourceUuid, deployRevision, "all");
|
|
assert.equal(groupResult.status, 1);
|
|
assert.match(groupResult.stderr, /valid worker group/);
|
|
assert.deepEqual(server.requests, []);
|
|
assertSecretSafe(mutableResult);
|
|
assertSecretSafe(groupResult);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("fails immediately for Coolify cancelled-by-user deployments", async () => {
|
|
const server = await startServer(() => ({ payload: { status: "cancelled-by-user" } }));
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "wait", deploymentUuid, "dependent");
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stdout, /cancelled-by-user/);
|
|
assert.match(result.stderr, /deployment failed/);
|
|
assert.equal(server.requests.length, 1);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("redacts API response bodies when Coolify returns an error", async () => {
|
|
const responseSentinel = "sensitive-coolify-response-sentinel";
|
|
const server = await startServer(() => ({
|
|
status: 401,
|
|
payload: { message: responseSentinel },
|
|
}));
|
|
|
|
try {
|
|
const result = await runHelper(server.url, "trigger", resourceUuid);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.stdout, "");
|
|
assert.match(result.stderr, /HTTP 401/);
|
|
assert.doesNotMatch(result.stderr, new RegExp(responseSentinel));
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it("bounds and redacts stalled Coolify API requests", async () => {
|
|
const server = await startServer(async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
return { payload: { deployments: [] } };
|
|
});
|
|
|
|
try {
|
|
const result = await runHelperWithEnv(server.url, { COOLIFY_API_TIMEOUT_MS: "10" }, "trigger", resourceUuid);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.stdout, "");
|
|
assert.match(result.stderr, /failed or timed out/);
|
|
assertSecretSafe(result);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
});
|