import assert from "node:assert/strict"; import { test } from "node:test"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync, type SpawnSyncReturns } from "node:child_process"; import { redact, responseDetails } from "../scripts/dokploy-diagnostics.mjs"; test("Dokploy diagnostics redact credentials and strip terminal controls", () => { const key = "secret/key?value"; const message = redact( `Key ${key} encoded ${encodeURIComponent(key)}\nx-api-key: ${key}\nAuthorization: Bearer token\nhttps://user:password@proxy.test\n\u001b[31m`, [key], ); for (const value of [ key, encodeURIComponent(key), "Bearer token", "user:password", "\u001b", ]) assert.ok(!message.includes(value)); assert.ok(message.includes("[REDACTED]")); }); test("API failure details include useful error fields without dumping deployments, configuration or cookies", () => { const result = responseDetails( "HTTP/2 403\r\nserver: cloudflare\r\ncf-ray: abc\r\nset-cookie: private\r\n", JSON.stringify({ error: { message: "Permission denied", code: "FORBIDDEN", config: "private config", }, source: "private source", }), ).join("\n"); assert.ok(result.includes("HTTP/2 403")); assert.ok(result.includes("Permission denied")); assert.ok(result.includes("cf-ray: abc")); for (const value of ["private config", "private source", "set-cookie"]) assert.ok(!result.includes(value)); const html = responseDetails( "content-type: text/html", "Bad GatewayPrivate proxy details", ).join("\n"); assert.ok(html.includes("Bad Gateway")); assert.ok(!html.includes("Private proxy details")); }); test("HTTP 000 diagnostics preserve curl DNS/TLS errors, timings and exit codes while masking the key", () => { const directory = fs.mkdtempSync( path.join(os.tmpdir(), "minegeo-dokploy-diagnostics-"), ); try { const file = (name: string, value: string) => { const target = path.join(directory, name); fs.writeFileSync(target, value); return target; }; const key = "test-dokploy-secret-123"; const metrics = file( "metrics", "000\nremote_ip= dns=0.002s connect=0s tls=0s total=0.003s\nssl_verify_result=20\n", ); const headers = file("headers", ""); const body = file("body", ""); const auth = file("auth", `x-api-key: ${key}\n`); for (const [code, error, expected] of [ [ "6", "curl: (6) Could not resolve host: dokploy.minegeo.site", "Target DNS failed", ], [ "60", `curl: (60) SSL certificate problem: unable to get local issuer certificate ${key}`, "TLS certificate verification failed", ], ]) { const stderr = file("error", error); const result = spawnSync( process.execPath, [ "scripts/dokploy-diagnostics.mjs", "request", "API preflight", code, metrics, stderr, headers, body, auth, ], { encoding: "utf8" }, ); assert.equal(result.status, 0, result.stderr); assert.ok(result.stderr.includes(`curl exit=${code}`)); assert.ok(result.stderr.includes(expected)); assert.ok(result.stderr.includes("HTTP 000 means no HTTP response")); assert.ok(result.stderr.includes("dns=0.002s")); assert.ok(result.stderr.includes("Response body: empty")); assert.ok(!result.stderr.includes(key)); assert.equal(result.stdout, ""); } } finally { fs.rmSync(directory, { recursive: true }); } }); test("deployment workflow has valid Bash syntax and captures failures without disabling certificate checks", () => { const workflow = fs .readFileSync(".gitea/workflows/deploy-dokploy.yml", "utf8") .replace(/\r\n/g, "\n"); const script = workflow .split(" run: |\n")[1] ?.split("\n") .map((line) => line.replace(/^ /, "")) .join("\n"); assert.ok(script, "workflow script was not found"); assert.ok( script.includes("readonly DOKPLOY_URL='https://dokploy.minegeo.site'"), ); assert.ok(script.includes("curl_exit=$?")); assert.ok(script.includes("--silent --show-error")); assert.ok(!script.includes("2>/dev/null")); assert.ok(!script.includes("--insecure")); const bash = process.platform === "win32" ? "C:/Program Files/Git/bin/bash.exe" : "/bin/bash"; const result = spawnSync(bash, ["--noprofile", "--norc", "-n"], { input: script, encoding: "utf8", }); assert.equal(result.status, 0, result.stderr || String(result.error)); }); test("the workflow request wrapper fails on HTTP 000 and reports the original curl exit code", () => { const workflow = fs .readFileSync(".gitea/workflows/deploy-dokploy.yml", "utf8") .replace(/\r\n/g, "\n"); const script = workflow .split(" run: |\n")[1] .split("\n") .map((line) => line.replace(/^ /, "")) .join("\n"); const wrapper = script.match(/dokploy_request\(\) \{[\s\S]*?\n\}/)?.[0]; assert.ok(wrapper); const directory = fs.mkdtempSync( path.join(os.tmpdir(), "minegeo-dokploy-wrapper-"), ); const bash = process.platform === "win32" ? "C:/Program Files/Git/bin/bash.exe" : "/bin/bash"; try { fs.writeFileSync( path.join(directory, "auth"), "x-api-key: fake-secret-for-wrapper-test\n", ); for (const [code, http, expected] of [ [6, "000", 1], [60, "000", 1], [0, "403", 1], [0, "200", 0], ] as const) { const result: SpawnSyncReturns = spawnSync( bash, ["--noprofile", "--norc", "-s"], { encoding: "utf8", env: { ...process.env, FIXTURE_DIR: directory.replace(/\\/g, "/"), DOKPLOY_APPLICATION_ID: "test-app", }, input: `set -eu auth_file="$FIXTURE_DIR/auth" metrics_file="$FIXTURE_DIR/metrics" error_file="$FIXTURE_DIR/error" headers_file="$FIXTURE_DIR/headers" ${wrapper} curl() { printf '${http}\\ndns=0.001s total=0.002s\\n'; if [ ${code} -ne 0 ]; then printf 'curl: (${code}) simulated network failure\\n' >&2; fi; return ${code}; } dokploy_request 'Mock API preflight' "$FIXTURE_DIR/body" --get https://dokploy.minegeo.site/api/deployment.all `, }, ); assert.equal( result.status, expected, result.stderr || String(result.error), ); assert.ok(result.stderr.includes(`curl exit=${code}`)); assert.ok(result.stderr.includes(http)); assert.ok(!result.stderr.includes("fake-secret-for-wrapper-test")); if (code) assert.ok( result.stderr.includes(`curl: (${code}) simulated network failure`), ); } } finally { fs.rmSync(directory, { recursive: true }); } });