Files
OmniRoute/tests/unit/cli-redis-command.test.ts
Chedrian07 fd62cb1152 fix(docker): publish the Redis sidecar on loopback instead of 0.0.0.0 (#9286)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log
2026-08-05 21:45:56 -03:00

235 lines
8.7 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
// ─── T-12 (#3932 PR-3): `omniroute redis` CLI command ─────────────────────
test("registerRedis: exports a registerRedis function", async () => {
const mod = await import(`../../bin/cli/commands/redis.mjs?case=${Date.now()}-${Math.random()}`);
assert.equal(typeof mod.registerRedis, "function");
});
test("registerRedis: attaches a `redis` command with up/down/status subcommands", async () => {
const { registerRedis } = await import(
`../../bin/cli/commands/redis.mjs?case=${Date.now()}-${Math.random()}`
);
// Use a minimal stub of the commander program that records what was attached.
const recorded = { commands: new Map() };
const subCommands: Array<{ name: string; options: Set<string> }> = [];
const fakeProgram = {
command(name) {
const cmd = {
name,
description() {
return cmd;
},
option(flag) {
// Track every registered option so we can assert on them.
const optName = flag.split(/[ ,]/)[0].replace(/^-+/, "");
cmd.options = cmd.options || new Set();
cmd.options.add(optName);
return cmd;
},
action() {
return cmd;
},
};
recorded.commands.set(name, cmd);
return cmd;
},
};
// registerRedis calls .command("redis") first, then .command("up") etc.
// on the returned sub-command. We need a smarter stub that returns a
// separate object for each call.
const subStubs: Array<{ name: string; options: Set<string> }> = [];
const allCommands = new Map();
const program = {
command(name) {
if (name === "redis") {
// Return the parent-of-subcommands stub
const redisCmd = {
options: new Set<string>(),
command(subName) {
const sub = {
name: subName,
options: new Set<string>(),
description() {
return sub;
},
option(flag) {
const optName = flag.split(/[ ,]/)[0].replace(/^-+/, "");
sub.options.add(optName);
return sub;
},
action() {
return sub;
},
};
subStubs.push(sub);
return sub;
},
description() {
return redisCmd;
},
option(flag) {
const optName = flag.split(/[ ,]/)[0].replace(/^-+/, "");
redisCmd.options.add(optName);
return redisCmd;
},
};
allCommands.set(name, redisCmd);
return redisCmd;
}
return null;
},
};
registerRedis(program);
assert.equal(subStubs.length, 3, `expected 3 subcommands, got ${subStubs.length}`);
const names = subStubs.map((s) => s.name).sort();
assert.deepEqual(names, ["down", "status", "up"]);
});
test("registerRedis: `up` subcommand has the expected option flags", async () => {
const { registerRedis } = await import(
`../../bin/cli/commands/redis.mjs?case=${Date.now()}-${Math.random()}`
);
const subStubs: Array<{ name: string; options: Set<string> }> = [];
const program = {
command(_name: string) {
const redisCmd = {
options: new Set<string>(),
command(subName: string) {
const sub = {
name: subName,
options: new Set<string>(),
description() {
return sub;
},
option(flag: string) {
// Prefer the canonical long flag (`--port` from `-p, --port <port>`);
// fall back to the first token for short-only / `--no-x` flags.
const long = flag.match(/--([\w-]+)/);
const optName = long ? long[1] : flag.split(/[ ,]/)[0].replace(/^-+/, "");
sub.options.add(optName);
return sub;
},
action() {
return sub;
},
};
subStubs.push(sub);
return sub;
},
description() {
return redisCmd;
},
option(flag: string) {
// Prefer the canonical long flag (`--port` from `-p, --port <port>`);
// fall back to the first token for short-only / `--no-x` flags.
const long = flag.match(/--([\w-]+)/);
const optName = long ? long[1] : flag.split(/[ ,]/)[0].replace(/^-+/, "");
redisCmd.options.add(optName);
return redisCmd;
},
};
return redisCmd;
},
};
registerRedis(program);
const upCmd = subStubs.find((s) => s.name === "up")!;
assert.ok(upCmd.options.has("port"), "missing --port");
assert.ok(upCmd.options.has("bind"), "missing --bind");
assert.ok(upCmd.options.has("name"), "missing --name");
assert.ok(upCmd.options.has("image"), "missing --image");
assert.ok(upCmd.options.has("runtime"), "missing --runtime");
assert.ok(upCmd.options.has("password"), "missing --password");
assert.ok(upCmd.options.has("no-pull"), "missing --no-pull (boolean negation)");
});
test("runRedisUpCommand: returns 1 when no podman/docker is available (no PATH)", async () => {
// We force this by passing a --runtime that doesn't exist. execFile will
// throw ENOENT, the runner will print the error and return 1.
const { runRedisUpCommand } = await import(
`../../bin/cli/commands/redis.mjs?case=${Date.now()}-${Math.random()}`
);
// Capture stderr to keep the test output clean.
const origStderr = process.stderr.write.bind(process.stderr);
const captured: string[] = [];
process.stderr.write = ((chunk: string | Uint8Array) => {
captured.push(typeof chunk === "string" ? chunk : chunk.toString());
return true;
}) as typeof process.stderr.write;
try {
const code = await runRedisUpCommand({ runtime: "/nonexistent/runtime-binary" });
assert.equal(code, 1, "expected exit 1 when runtime is missing");
} finally {
process.stderr.write = origStderr;
}
});
test("runRedisStatusCommand: returns 1 when no podman/docker is available", async () => {
const { runRedisStatusCommand } = await import(
`../../bin/cli/commands/redis.mjs?case=${Date.now()}-${Math.random()}`
);
const origStderr = process.stderr.write.bind(process.stderr);
process.stderr.write = (() => true) as typeof process.stderr.write;
try {
const code = await runRedisStatusCommand({ runtime: "/nonexistent/runtime-binary" });
assert.equal(code, 1);
} finally {
process.stderr.write = origStderr;
}
});
test("runRedisDownCommand: returns 1 when no podman/docker is available", async () => {
const { runRedisDownCommand } = await import(
`../../bin/cli/commands/redis.mjs?case=${Date.now()}-${Math.random()}`
);
const origStderr = process.stderr.write.bind(process.stderr);
process.stderr.write = (() => true) as typeof process.stderr.write;
try {
const code = await runRedisDownCommand({ runtime: "/nonexistent/runtime-binary" });
assert.equal(code, 1);
} finally {
process.stderr.write = origStderr;
}
});
// ─── `omniroute redis up` must publish on loopback, not 0.0.0.0 ───────────
// The launcher starts Redis with no `requirepass` unless --password is given,
// so a bare "6379:6379" publish spec would hand the LAN an unauthenticated
// Redis (the container runtime defaults an unqualified spec to 0.0.0.0).
test("buildRedisPublishSpec: defaults to loopback, never 0.0.0.0", async () => {
const { buildRedisPublishSpec } = await import(
`../../bin/cli/commands/redis.mjs?case=${Date.now()}-${Math.random()}`
);
assert.equal(buildRedisPublishSpec(), "127.0.0.1:6379:6379");
assert.equal(buildRedisPublishSpec(undefined, "6380"), "127.0.0.1:6380:6379");
assert.equal(buildRedisPublishSpec("", ""), "127.0.0.1:6379:6379");
});
test("buildRedisPublishSpec: honours an explicit bind override and brackets IPv6", async () => {
const { buildRedisPublishSpec } = await import(
`../../bin/cli/commands/redis.mjs?case=${Date.now()}-${Math.random()}`
);
// Opt-in exposure stays possible — it just can never be the default.
assert.equal(buildRedisPublishSpec("0.0.0.0", "6379"), "0.0.0.0:6379:6379");
assert.equal(buildRedisPublishSpec("::1", "6379"), "[::1]:6379:6379");
});
test("`redis up` never builds an unqualified `-p <port>:6379` publish arg", async () => {
const source = await readFile(
new URL("../../bin/cli/commands/redis.mjs", import.meta.url),
"utf8"
);
assert.doesNotMatch(
source,
/"-p",\s*`\$\{port\}:6379`/,
"publish spec must be host-qualified via buildRedisPublishSpec()"
);
assert.match(source, /"-p",\s*buildRedisPublishSpec\(bind, port\)/);
});