openclaw/src/commands/doctor-security.test.ts
Peter Steinberger 3b0e62d5bf fix(doctor): warn that approvals.exec.enabled only disables forwarding
Co-authored-by: nomadonwheels196 <nomadonwheels196@users.noreply.github.com>
2026-02-22 23:33:15 +01:00

123 lines
3.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
const note = vi.hoisted(() => vi.fn());
const pluginRegistry = vi.hoisted(() => ({ list: [] as unknown[] }));
vi.mock("../terminal/note.js", () => ({
note,
}));
vi.mock("../channels/plugins/index.js", () => ({
listChannelPlugins: () => pluginRegistry.list,
}));
import { noteSecurityWarnings } from "./doctor-security.js";
describe("noteSecurityWarnings gateway exposure", () => {
let prevToken: string | undefined;
let prevPassword: string | undefined;
beforeEach(() => {
note.mockClear();
pluginRegistry.list = [];
prevToken = process.env.OPENCLAW_GATEWAY_TOKEN;
prevPassword = process.env.OPENCLAW_GATEWAY_PASSWORD;
delete process.env.OPENCLAW_GATEWAY_TOKEN;
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
});
afterEach(() => {
if (prevToken === undefined) {
delete process.env.OPENCLAW_GATEWAY_TOKEN;
} else {
process.env.OPENCLAW_GATEWAY_TOKEN = prevToken;
}
if (prevPassword === undefined) {
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
} else {
process.env.OPENCLAW_GATEWAY_PASSWORD = prevPassword;
}
});
const lastMessage = () => String(note.mock.calls.at(-1)?.[0] ?? "");
it("warns when exposed without auth", async () => {
const cfg = { gateway: { bind: "lan" } } as OpenClawConfig;
await noteSecurityWarnings(cfg);
const message = lastMessage();
expect(message).toContain("CRITICAL");
expect(message).toContain("without authentication");
expect(message).toContain("Safer remote access");
expect(message).toContain("ssh -N -L 18789:127.0.0.1:18789");
});
it("uses env token to avoid critical warning", async () => {
process.env.OPENCLAW_GATEWAY_TOKEN = "token-123";
const cfg = { gateway: { bind: "lan" } } as OpenClawConfig;
await noteSecurityWarnings(cfg);
const message = lastMessage();
expect(message).toContain("WARNING");
expect(message).not.toContain("CRITICAL");
});
it("treats whitespace token as missing", async () => {
const cfg = {
gateway: { bind: "lan", auth: { mode: "token", token: " " } },
} as OpenClawConfig;
await noteSecurityWarnings(cfg);
const message = lastMessage();
expect(message).toContain("CRITICAL");
});
it("skips warning for loopback bind", async () => {
const cfg = { gateway: { bind: "loopback" } } as OpenClawConfig;
await noteSecurityWarnings(cfg);
const message = lastMessage();
expect(message).toContain("No channel security warnings detected");
expect(message).not.toContain("Gateway bound");
});
it("shows explicit dmScope config command for multi-user DMs", async () => {
pluginRegistry.list = [
{
id: "whatsapp",
meta: { label: "WhatsApp" },
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({}),
isEnabled: () => true,
isConfigured: () => true,
},
security: {
resolveDmPolicy: () => ({
policy: "allowlist",
allowFrom: ["alice", "bob"],
allowFromPath: "channels.whatsapp.",
approveHint: "approve",
}),
},
},
];
const cfg = { session: { dmScope: "main" } } as OpenClawConfig;
await noteSecurityWarnings(cfg);
const message = lastMessage();
expect(message).toContain('config set session.dmScope "per-channel-peer"');
});
it("clarifies approvals.exec forwarding-only behavior", async () => {
const cfg = {
approvals: {
exec: {
enabled: false,
},
},
} as OpenClawConfig;
await noteSecurityWarnings(cfg);
const message = lastMessage();
expect(message).toContain("disables approval forwarding only");
expect(message).toContain("exec-approvals.json");
expect(message).toContain("openclaw approvals get --gateway");
});
});