Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions test/unit/selfhost-deploy-common.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,86 @@ printf 'REACHED_END args=[%s]\\n' "\${compose_args[*]}"
expect(result.stderr).toContain("compose file not found: missing.yml");
});
});

// Generic seam for the remaining library functions (#7769): source the lib in a scratch dir, invoke one
// function with the given args, and return its status/stdout/stderr -- the same spawn-a-real-bash approach as
// createHarness above, but parameterized so env_get/require_cmd can each be driven directly.
// (compose_file_args is already covered by the #7765 suite above.)
function runLibFn(call: string, options: { env?: Record<string, string>; files?: Record<string, string> } = {}) {
const dir = mkdtempSync(join(tmpdir(), "loopover-selfhost-deploy-common-fn-"));
try {
for (const [name, contents] of Object.entries(options.files ?? {})) {
writeFileSync(join(dir, name), contents);
}
const scriptPath = join(dir, "run.sh");
// set -u is deliberately NOT enabled: env_get reads optional ENV_FILE with `${VAR:-default}` guards,
// exactly as the real deploy scripts do.
writeFileSync(scriptPath, `#!/usr/bin/env bash\nset -eo pipefail\n. "${libPath.replace(/\\/g, "/")}"\n${call}\n`);
chmodSync(scriptPath, 0o755);
return spawnSync("bash", [scriptPath], {
cwd: dir,
encoding: "utf8",
env: { ...process.env, ...(options.env ?? {}) },
});
} finally {
rmSync(dir, { recursive: true, force: true });
}
}

describe("require_cmd (#7769)", () => {
it("succeeds silently when the command exists", () => {
const result = runLibFn("require_cmd bash");
expect(result.status, result.stderr).toBe(0);
expect(result.stderr).toBe("");
});

it("fails with exit 1 and a clear error when the command is missing", () => {
const result = runLibFn("require_cmd loopover-definitely-not-a-real-command");
expect(result.status).toBe(1);
expect(result.stderr).toContain("required command not found: loopover-definitely-not-a-real-command");
});
});

describe("env_get (#7769)", () => {
it("returns a plain unquoted value for a matching key", () => {
const result = runLibFn('env_get FOO "$PWD/.env"', { files: { ".env": "FOO=bar\n" } });
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toBe("bar\n");
});

it("strips surrounding double and single quotes from the value", () => {
const dq = runLibFn('env_get FOO "$PWD/.env"', { files: { ".env": 'FOO="quoted value"\n' } });
expect(dq.status, dq.stderr).toBe(0);
expect(dq.stdout).toBe("quoted value\n");

const sq = runLibFn('env_get FOO "$PWD/.env"', { files: { ".env": "FOO='single quoted'\n" } });
expect(sq.status, sq.stderr).toBe(0);
expect(sq.stdout).toBe("single quoted\n");
});

it("skips comment and blank lines and returns the first matching key", () => {
const result = runLibFn('env_get FOO "$PWD/.env"', { files: { ".env": "# a comment\n\n FOO = spaced\nFOO=second\n" } });
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toBe("spaced\n");
});

it("returns exit 1 when the key is absent from the file", () => {
const result = runLibFn('env_get MISSING "$PWD/.env"', { files: { ".env": "FOO=bar\n" } });
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
});

it("returns exit 1 when the file does not exist", () => {
const result = runLibFn('env_get FOO "$PWD/does-not-exist.env"');
expect(result.status).toBe(1);
});

it("falls back to $ENV_FILE when no file argument is given", () => {
const result = runLibFn("env_get FOO", {
files: { ".env": "FOO=from-env-file\n" },
env: { ENV_FILE: ".env" },
});
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toBe("from-env-file\n");
});
});
Loading