diff --git a/apps/dokploy/__test__/server/server-validate.test.ts b/apps/dokploy/__test__/server/server-validate.test.ts new file mode 100644 index 000000000..71e537116 --- /dev/null +++ b/apps/dokploy/__test__/server/server-validate.test.ts @@ -0,0 +1,64 @@ +import { execFileSync, execSync } from "node:child_process"; +import { chmodSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { validateSwarm } from "@dokploy/server"; +import { describe, expect, it } from "vitest"; + +const resolveBin = (name: string) => + execSync(`command -v ${name}`, { encoding: "utf8" }).trim(); + +/** + * Build a sandbox PATH so `command -v docker` only sees our fake docker + * binary, while `grep` passes through to the real system binary. Using the + * real `grep` is intentional: the bug was an unanchored `grep -q 'active'` + * substring match, and the fix anchors it with `grep -qx 'active'`, so we + * must assert the real grep line-anchoring behavior, not a shim's. + */ +const makeSandbox = (localNodeState: string) => { + const dir = mkdtempSync(path.join(tmpdir(), "dokploy-server-validate-")); + for (const tool of ["grep"]) { + const shim = path.join(dir, tool); + writeFileSync(shim, `#!/bin/sh\nexec ${resolveBin(tool)} "$@"\n`); + chmodSync(shim, 0o755); + } + const docker = path.join(dir, "docker"); + writeFileSync( + docker, + [ + "#!/bin/sh", + 'if [ "$1" = "info" ] && [ "$2" = "--format" ]; then', + ` echo "${localNodeState}"`, + " exit 0", + "fi", + "exit 1", + ].join("\n"), + ); + chmodSync(docker, 0o755); + return dir; +}; + +const runValidateSwarm = (sandboxPath: string) => + execFileSync(resolveBin("bash"), ["-c", validateSwarm()], { + encoding: "utf8", + env: { ...process.env, PATH: sandboxPath }, + }).trim(); + +describe("validateSwarm", () => { + it("reports true when the node is an active swarm member", () => { + expect(runValidateSwarm(makeSandbox("active"))).toBe("true"); + }); + + it("reports false when the node is not part of any swarm (LocalNodeState=inactive)", () => { + // Regression: the buggy `grep -q 'active'` matched `inactive` as a + // substring (in+active), falsely reporting isSwarmInstalled=true. + expect(runValidateSwarm(makeSandbox("inactive"))).toBe("false"); + }); + + it.each(["pending", "error", "locked"])( + "reports false for the non-active LocalNodeState=%s", + (state) => { + expect(runValidateSwarm(makeSandbox(state))).toBe("false"); + }, + ); +}); diff --git a/packages/server/src/setup/server-validate.ts b/packages/server/src/setup/server-validate.ts index f55bbe616..aad6c11e1 100644 --- a/packages/server/src/setup/server-validate.ts +++ b/packages/server/src/setup/server-validate.ts @@ -18,7 +18,7 @@ export const validateRClone = () => ` `; export const validateSwarm = () => ` - if docker info --format '{{.Swarm.LocalNodeState}}' | grep -q 'active'; then + if docker info --format '{{.Swarm.LocalNodeState}}' | grep -qx 'active'; then echo true else echo false