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"); }); 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"'); }); });