import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; const mocks = vi.hoisted(() => ({ writeConfigFile: vi.fn(async (_cfg: OpenClawConfig) => {}), })); vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, writeConfigFile: mocks.writeConfigFile, }; }); import { ensureGatewayStartupAuth } from "./startup-auth.js"; describe("ensureGatewayStartupAuth", () => { beforeEach(() => { vi.restoreAllMocks(); mocks.writeConfigFile.mockReset(); }); it("generates and persists a token when startup auth is missing", async () => { const result = await ensureGatewayStartupAuth({ cfg: {}, env: {} as NodeJS.ProcessEnv, persist: true, }); expect(result.generatedToken).toMatch(/^[0-9a-f]{48}$/); expect(result.persistedGeneratedToken).toBe(true); expect(result.auth.mode).toBe("token"); expect(result.auth.token).toBe(result.generatedToken); expect(mocks.writeConfigFile).toHaveBeenCalledTimes(1); const persisted = mocks.writeConfigFile.mock.calls[0]?.[0]; expect(persisted?.gateway?.auth?.mode).toBe("token"); expect(persisted?.gateway?.auth?.token).toBe(result.generatedToken); }); it("does not generate when token already exists", async () => { const cfg: OpenClawConfig = { gateway: { auth: { mode: "token", token: "configured-token", }, }, }; const result = await ensureGatewayStartupAuth({ cfg, env: {} as NodeJS.ProcessEnv, persist: true, }); expect(result.generatedToken).toBeUndefined(); expect(result.persistedGeneratedToken).toBe(false); expect(result.auth.mode).toBe("token"); expect(result.auth.token).toBe("configured-token"); expect(mocks.writeConfigFile).not.toHaveBeenCalled(); }); it("does not generate in password mode", async () => { const cfg: OpenClawConfig = { gateway: { auth: { mode: "password", }, }, }; const result = await ensureGatewayStartupAuth({ cfg, env: {} as NodeJS.ProcessEnv, persist: true, }); expect(result.generatedToken).toBeUndefined(); expect(result.persistedGeneratedToken).toBe(false); expect(result.auth.mode).toBe("password"); expect(mocks.writeConfigFile).not.toHaveBeenCalled(); }); it("does not generate in trusted-proxy mode", async () => { const cfg: OpenClawConfig = { gateway: { auth: { mode: "trusted-proxy", trustedProxy: { userHeader: "x-forwarded-user" }, }, }, }; const result = await ensureGatewayStartupAuth({ cfg, env: {} as NodeJS.ProcessEnv, persist: true, }); expect(result.generatedToken).toBeUndefined(); expect(result.persistedGeneratedToken).toBe(false); expect(result.auth.mode).toBe("trusted-proxy"); expect(mocks.writeConfigFile).not.toHaveBeenCalled(); }); it("does not generate in explicit none mode", async () => { const cfg: OpenClawConfig = { gateway: { auth: { mode: "none", }, }, }; const result = await ensureGatewayStartupAuth({ cfg, env: {} as NodeJS.ProcessEnv, persist: true, }); expect(result.generatedToken).toBeUndefined(); expect(result.persistedGeneratedToken).toBe(false); expect(result.auth.mode).toBe("none"); expect(mocks.writeConfigFile).not.toHaveBeenCalled(); }); it("treats undefined token override as no override", async () => { const cfg: OpenClawConfig = { gateway: { auth: { mode: "token", token: "from-config", }, }, }; const result = await ensureGatewayStartupAuth({ cfg, env: {} as NodeJS.ProcessEnv, authOverride: { mode: "token", token: undefined }, persist: true, }); expect(result.generatedToken).toBeUndefined(); expect(result.persistedGeneratedToken).toBe(false); expect(result.auth.mode).toBe("token"); expect(result.auth.token).toBe("from-config"); expect(mocks.writeConfigFile).not.toHaveBeenCalled(); }); it("keeps generated token ephemeral when runtime override flips explicit non-token mode", async () => { const cfg: OpenClawConfig = { gateway: { auth: { mode: "password", }, }, }; const result = await ensureGatewayStartupAuth({ cfg, env: {} as NodeJS.ProcessEnv, authOverride: { mode: "token" }, persist: true, }); expect(result.generatedToken).toMatch(/^[0-9a-f]{48}$/); expect(result.persistedGeneratedToken).toBe(false); expect(result.auth.mode).toBe("token"); expect(result.auth.token).toBe(result.generatedToken); expect(mocks.writeConfigFile).not.toHaveBeenCalled(); }); it("keeps generated token ephemeral when runtime override flips explicit none mode", async () => { const cfg: OpenClawConfig = { gateway: { auth: { mode: "none", }, }, }; const result = await ensureGatewayStartupAuth({ cfg, env: {} as NodeJS.ProcessEnv, authOverride: { mode: "token" }, persist: true, }); expect(result.generatedToken).toMatch(/^[0-9a-f]{48}$/); expect(result.persistedGeneratedToken).toBe(false); expect(result.auth.mode).toBe("token"); expect(result.auth.token).toBe(result.generatedToken); expect(mocks.writeConfigFile).not.toHaveBeenCalled(); }); it("keeps generated token ephemeral when runtime override flips implicit password mode", async () => { const cfg: OpenClawConfig = { gateway: { auth: { password: "configured-password", }, }, }; const result = await ensureGatewayStartupAuth({ cfg, env: {} as NodeJS.ProcessEnv, authOverride: { mode: "token" }, persist: true, }); expect(result.generatedToken).toMatch(/^[0-9a-f]{48}$/); expect(result.persistedGeneratedToken).toBe(false); expect(result.auth.mode).toBe("token"); expect(result.auth.token).toBe(result.generatedToken); expect(mocks.writeConfigFile).not.toHaveBeenCalled(); }); });