mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-26 21:58:38 +00:00
* fix(configure): reject literal "undefined" and "null" gateway auth tokens * fix(configure): reject literal "undefined" and "null" gateway auth tokens * fix(configure): validate gateway password prompt and harden token coercion (#13767) (thanks @omair445) * test: remove unused vitest imports in baseline lint fixtures (#13767) --------- Co-authored-by: Luna AI <luna@coredirection.ai> Co-authored-by: Peter Steinberger <steipete@gmail.com>
154 lines
4.6 KiB
TypeScript
154 lines
4.6 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
normalizeGatewayTokenInput,
|
|
openUrl,
|
|
resolveBrowserOpenCommand,
|
|
resolveControlUiLinks,
|
|
validateGatewayPasswordInput,
|
|
} from "./onboard-helpers.js";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
runCommandWithTimeout: vi.fn(async () => ({
|
|
stdout: "",
|
|
stderr: "",
|
|
code: 0,
|
|
signal: null,
|
|
killed: false,
|
|
})),
|
|
pickPrimaryTailnetIPv4: vi.fn(() => undefined),
|
|
}));
|
|
|
|
vi.mock("../process/exec.js", () => ({
|
|
runCommandWithTimeout: mocks.runCommandWithTimeout,
|
|
}));
|
|
|
|
vi.mock("../infra/tailnet.js", () => ({
|
|
pickPrimaryTailnetIPv4: mocks.pickPrimaryTailnetIPv4,
|
|
}));
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
describe("openUrl", () => {
|
|
it("quotes URLs on win32 so '&' is not treated as cmd separator", async () => {
|
|
vi.stubEnv("VITEST", "");
|
|
vi.stubEnv("NODE_ENV", "");
|
|
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
|
vi.stubEnv("VITEST", "");
|
|
vi.stubEnv("NODE_ENV", "development");
|
|
|
|
const url =
|
|
"https://accounts.google.com/o/oauth2/v2/auth?client_id=abc&response_type=code&redirect_uri=http%3A%2F%2Flocalhost";
|
|
|
|
const ok = await openUrl(url);
|
|
expect(ok).toBe(true);
|
|
|
|
expect(mocks.runCommandWithTimeout).toHaveBeenCalledTimes(1);
|
|
const [argv, options] = mocks.runCommandWithTimeout.mock.calls[0] ?? [];
|
|
expect(argv?.slice(0, 4)).toEqual(["cmd", "/c", "start", '""']);
|
|
expect(argv?.at(-1)).toBe(`"${url}"`);
|
|
expect(options).toMatchObject({
|
|
timeoutMs: 5_000,
|
|
windowsVerbatimArguments: true,
|
|
});
|
|
|
|
platformSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("resolveBrowserOpenCommand", () => {
|
|
it("marks win32 commands as quoteUrl=true", async () => {
|
|
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
|
const resolved = await resolveBrowserOpenCommand();
|
|
expect(resolved.argv).toEqual(["cmd", "/c", "start", ""]);
|
|
expect(resolved.quoteUrl).toBe(true);
|
|
platformSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("resolveControlUiLinks", () => {
|
|
it("uses customBindHost for custom bind", () => {
|
|
const links = resolveControlUiLinks({
|
|
port: 18789,
|
|
bind: "custom",
|
|
customBindHost: "192.168.1.100",
|
|
});
|
|
expect(links.httpUrl).toBe("http://192.168.1.100:18789/");
|
|
expect(links.wsUrl).toBe("ws://192.168.1.100:18789");
|
|
});
|
|
|
|
it("falls back to loopback for invalid customBindHost", () => {
|
|
const links = resolveControlUiLinks({
|
|
port: 18789,
|
|
bind: "custom",
|
|
customBindHost: "192.168.001.100",
|
|
});
|
|
expect(links.httpUrl).toBe("http://127.0.0.1:18789/");
|
|
expect(links.wsUrl).toBe("ws://127.0.0.1:18789");
|
|
});
|
|
|
|
it("uses tailnet IP for tailnet bind", () => {
|
|
mocks.pickPrimaryTailnetIPv4.mockReturnValueOnce("100.64.0.9");
|
|
const links = resolveControlUiLinks({
|
|
port: 18789,
|
|
bind: "tailnet",
|
|
});
|
|
expect(links.httpUrl).toBe("http://100.64.0.9:18789/");
|
|
expect(links.wsUrl).toBe("ws://100.64.0.9:18789");
|
|
});
|
|
|
|
it("keeps loopback for auto even when tailnet is present", () => {
|
|
mocks.pickPrimaryTailnetIPv4.mockReturnValueOnce("100.64.0.9");
|
|
const links = resolveControlUiLinks({
|
|
port: 18789,
|
|
bind: "auto",
|
|
});
|
|
expect(links.httpUrl).toBe("http://127.0.0.1:18789/");
|
|
expect(links.wsUrl).toBe("ws://127.0.0.1:18789");
|
|
});
|
|
});
|
|
|
|
describe("normalizeGatewayTokenInput", () => {
|
|
it("returns empty string for undefined or null", () => {
|
|
expect(normalizeGatewayTokenInput(undefined)).toBe("");
|
|
expect(normalizeGatewayTokenInput(null)).toBe("");
|
|
});
|
|
|
|
it("trims string input", () => {
|
|
expect(normalizeGatewayTokenInput(" token ")).toBe("token");
|
|
});
|
|
|
|
it("returns empty string for non-string input", () => {
|
|
expect(normalizeGatewayTokenInput(123)).toBe("");
|
|
});
|
|
|
|
it('rejects the literal string "undefined"', () => {
|
|
expect(normalizeGatewayTokenInput("undefined")).toBe("");
|
|
});
|
|
|
|
it('rejects the literal string "null"', () => {
|
|
expect(normalizeGatewayTokenInput("null")).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("validateGatewayPasswordInput", () => {
|
|
it("requires a non-empty password", () => {
|
|
expect(validateGatewayPasswordInput("")).toBe("Required");
|
|
expect(validateGatewayPasswordInput(" ")).toBe("Required");
|
|
});
|
|
|
|
it("rejects literal string coercion artifacts", () => {
|
|
expect(validateGatewayPasswordInput("undefined")).toBe(
|
|
'Cannot be the literal string "undefined" or "null"',
|
|
);
|
|
expect(validateGatewayPasswordInput("null")).toBe(
|
|
'Cannot be the literal string "undefined" or "null"',
|
|
);
|
|
});
|
|
|
|
it("accepts a normal password", () => {
|
|
expect(validateGatewayPasswordInput(" secret ")).toBeUndefined();
|
|
});
|
|
});
|