diff --git a/src/gateway/credentials.test.ts b/src/gateway/credentials.test.ts index ae54c68e487..1ba7ae5c80a 100644 --- a/src/gateway/credentials.test.ts +++ b/src/gateway/credentials.test.ts @@ -11,6 +11,7 @@ function cfg(input: Partial): OpenClawConfig { type ResolveFromConfigInput = Parameters[0]; type GatewayConfig = NonNullable; +type ResolveFromConfigOverrides = Partial>; const DEFAULT_GATEWAY_AUTH = { token: "config-token", password: "config-password" }; // pragma: allowlist secret const DEFAULT_REMOTE_AUTH = { token: "remote-token", password: "remote-password" }; // pragma: allowlist secret @@ -18,10 +19,37 @@ const DEFAULT_GATEWAY_ENV = { OPENCLAW_GATEWAY_TOKEN: "env-token", OPENCLAW_GATEWAY_PASSWORD: "env-password", // pragma: allowlist secret } as NodeJS.ProcessEnv; +const EMPTY_GATEWAY_ENV = {} as NodeJS.ProcessEnv; + +function envSecretRef(id: string) { + return { source: "env", provider: "default", id } as const; +} + +function cfgWithDefaultEnvSecretProvider(gateway: GatewayConfig): OpenClawConfig { + return { + gateway, + secrets: { + providers: { + default: { source: "env" }, + }, + }, + } as unknown as OpenClawConfig; +} + +function resolveGatewayCredentialsWithEmptyEnv( + config: OpenClawConfig, + overrides: ResolveFromConfigOverrides = {}, +) { + return resolveGatewayCredentialsFromConfig({ + cfg: config, + env: EMPTY_GATEWAY_ENV, + ...overrides, + }); +} function resolveGatewayCredentialsFor( gateway: GatewayConfig, - overrides: Partial> = {}, + overrides: ResolveFromConfigOverrides = {}, ) { return resolveGatewayCredentialsFromConfig({ cfg: cfg({ gateway }), @@ -30,6 +58,10 @@ function resolveGatewayCredentialsFor( }); } +function resolveLocalGatewayCredentials(gateway: GatewayConfig) { + return resolveGatewayCredentialsWithEmptyEnv(cfg({ gateway: { mode: "local", ...gateway } })); +} + function expectEnvGatewayCredentials(resolved: { token?: string; password?: string }) { expect(resolved).toEqual({ token: "env-token", @@ -37,9 +69,32 @@ function expectEnvGatewayCredentials(resolved: { token?: string; password?: stri }); } -function resolveRemoteModeWithRemoteCredentials( - overrides: Partial> = {}, +function expectNoGatewayCredentials(resolved: { token?: string; password?: string }) { + expect(resolved).toEqual({ + token: undefined, + password: undefined, + }); +} + +function expectRemoteGatewayCredentials(resolved: { token?: string; password?: string }) { + expect(resolved).toEqual({ + token: "remote-token", + password: "remote-password", // pragma: allowlist secret + }); +} + +function resolveGatewayCredentialsFromDefaultValues( + overrides: Partial[0]> = {}, ) { + return resolveGatewayCredentialsFromValues({ + configToken: "config-token", + configPassword: "config-password", // pragma: allowlist secret + env: DEFAULT_GATEWAY_ENV, + ...overrides, + }); +} + +function resolveRemoteModeWithRemoteCredentials(overrides: ResolveFromConfigOverrides = {}) { return resolveGatewayCredentialsFor( { mode: "remote", @@ -51,23 +106,15 @@ function resolveRemoteModeWithRemoteCredentials( } function resolveLocalModeWithUnresolvedPassword(mode: "none" | "trusted-proxy") { - return resolveGatewayCredentialsFromConfig({ - cfg: { - gateway: { - mode: "local", - auth: { - mode, - password: { source: "env", provider: "default", id: "MISSING_GATEWAY_PASSWORD" }, - }, + return resolveGatewayCredentialsWithEmptyEnv( + cfgWithDefaultEnvSecretProvider({ + mode: "local", + auth: { + mode, + password: envSecretRef("MISSING_GATEWAY_PASSWORD"), }, - secrets: { - providers: { - default: { source: "env" }, - }, - }, - } as unknown as OpenClawConfig, - env: {} as NodeJS.ProcessEnv, - }); + }), + ); } function expectUnresolvedLocalAuthSecretRefFailure(params: { @@ -80,29 +127,21 @@ function expectUnresolvedLocalAuthSecretRefFailure(params: { params.authMode === "token" ? { mode: "token" as const, - token: { source: "env", provider: "default", id: params.secretId }, + token: envSecretRef(params.secretId), } : { mode: "password" as const, - password: { source: "env", provider: "default", id: params.secretId }, + password: envSecretRef(params.secretId), }; expect(() => - resolveGatewayCredentialsFromConfig({ - cfg: { - gateway: { - mode: "local", - auth: localAuth, - remote: params.remote, - }, - secrets: { - providers: { - default: { source: "env" }, - }, - }, - } as unknown as OpenClawConfig, - env: {} as NodeJS.ProcessEnv, - }), + resolveGatewayCredentialsWithEmptyEnv( + cfgWithDefaultEnvSecretProvider({ + mode: "local", + auth: localAuth, + remote: params.remote, + }), + ), ).toThrow(params.errorPath); } @@ -176,20 +215,11 @@ describe("resolveGatewayCredentialsFromConfig", () => { }); it("falls back to remote credentials in local mode when local auth is missing", () => { - const resolved = resolveGatewayCredentialsFromConfig({ - cfg: cfg({ - gateway: { - mode: "local", - remote: { token: "remote-token", password: "remote-password" }, // pragma: allowlist secret - auth: {}, - }, - }), - env: {} as NodeJS.ProcessEnv, - }); - expect(resolved).toEqual({ - token: "remote-token", - password: "remote-password", // pragma: allowlist secret + const resolved = resolveLocalGatewayCredentials({ + remote: DEFAULT_REMOTE_AUTH, + auth: {}, }); + expectRemoteGatewayCredentials(resolved); }); it("fails closed when local token SecretRef is unresolved and remote token fallback exists", () => { @@ -257,30 +287,9 @@ describe("resolveGatewayCredentialsFromConfig", () => { ).toThrow("gateway.auth.token"); }); - it("throws when unresolved local token SecretRef would otherwise fall back to remote token", () => { - expectUnresolvedLocalAuthSecretRefFailure({ - authMode: "token", - secretId: "MISSING_LOCAL_TOKEN", - errorPath: "gateway.auth.token", - remote: { token: "remote-token" }, - }); - }); - - it("throws when unresolved local password SecretRef would otherwise fall back to remote password", () => { - expectUnresolvedLocalAuthSecretRefFailure({ - authMode: "password", - secretId: "MISSING_LOCAL_PASSWORD", - errorPath: "gateway.auth.password", - remote: { password: "remote-password" }, // pragma: allowlist secret - }); - }); - it("ignores unresolved local password ref when local auth mode is none", () => { const resolved = resolveLocalModeWithUnresolvedPassword("none"); - expect(resolved).toEqual({ - token: undefined, - password: undefined, - }); + expectNoGatewayCredentials(resolved); }); it("throws when trusted-proxy local password SecretRef cannot resolve", () => { @@ -290,17 +299,11 @@ describe("resolveGatewayCredentialsFromConfig", () => { }); it("resolves trusted-proxy local password credentials", () => { - const resolved = resolveGatewayCredentialsFromConfig({ - cfg: cfg({ - gateway: { - mode: "local", - auth: { - mode: "trusted-proxy", - password: "local-trusted-proxy-password", // pragma: allowlist secret - }, - }, - }), - env: {} as NodeJS.ProcessEnv, + const resolved = resolveLocalGatewayCredentials({ + auth: { + mode: "trusted-proxy", + password: "local-trusted-proxy-password", // pragma: allowlist secret + }, }); expect(resolved).toEqual({ @@ -310,37 +313,22 @@ describe("resolveGatewayCredentialsFromConfig", () => { }); it("does not use remote password as trusted-proxy local fallback", () => { - const resolved = resolveGatewayCredentialsFromConfig({ - cfg: cfg({ - gateway: { - mode: "local", - auth: { - mode: "trusted-proxy", - }, - remote: { - password: "remote-password", // pragma: allowlist secret - }, - }, - }), - env: {} as NodeJS.ProcessEnv, + const resolved = resolveLocalGatewayCredentials({ + auth: { + mode: "trusted-proxy", + }, + remote: { + password: "remote-password", // pragma: allowlist secret + }, }); - expect(resolved).toEqual({ - token: undefined, - password: undefined, - }); + expectNoGatewayCredentials(resolved); }); it("keeps local credentials ahead of remote fallback in local mode", () => { - const resolved = resolveGatewayCredentialsFromConfig({ - cfg: cfg({ - gateway: { - mode: "local", - remote: { token: "remote-token", password: "remote-password" }, // pragma: allowlist secret - auth: { token: "local-token", password: "local-password" }, // pragma: allowlist secret - }, - }), - env: {} as NodeJS.ProcessEnv, + const resolved = resolveLocalGatewayCredentials({ + remote: DEFAULT_REMOTE_AUTH, + auth: { token: "local-token", password: "local-password" }, // pragma: allowlist secret }); expect(resolved).toEqual({ token: "local-token", @@ -422,25 +410,17 @@ describe("resolveGatewayCredentialsFromConfig", () => { it("throws when remote token auth relies on an unresolved SecretRef", () => { expect(() => - resolveGatewayCredentialsFromConfig({ - cfg: { - gateway: { - mode: "remote", - remote: { - url: "wss://gateway.example", - token: { source: "env", provider: "default", id: "MISSING_REMOTE_TOKEN" }, - }, - auth: {}, + resolveGatewayCredentialsWithEmptyEnv( + cfgWithDefaultEnvSecretProvider({ + mode: "remote", + remote: { + url: "wss://gateway.example", + token: envSecretRef("MISSING_REMOTE_TOKEN"), }, - secrets: { - providers: { - default: { source: "env" }, - }, - }, - } as unknown as OpenClawConfig, - env: {} as NodeJS.ProcessEnv, - remoteTokenFallback: "remote-only", - }), + auth: {}, + }), + { remoteTokenFallback: "remote-only" }, + ), ).toThrow("gateway.remote.token"); }); @@ -453,7 +433,7 @@ describe("resolveGatewayCredentialsFromConfig", () => { }, auth: { mode: "token", - token: { source: "env", provider: "default", id: "MISSING_LOCAL_TOKEN" }, + token: envSecretRef("MISSING_LOCAL_TOKEN"), }, }, secrets: { @@ -465,12 +445,13 @@ describe("resolveGatewayCredentialsFromConfig", () => { } it("ignores unresolved local token ref in remote-only mode when local auth mode is token", () => { - const resolved = resolveGatewayCredentialsFromConfig({ - cfg: createRemoteConfigWithMissingLocalTokenRef(), - env: {} as NodeJS.ProcessEnv, - remoteTokenFallback: "remote-only", - remotePasswordFallback: "remote-only", // pragma: allowlist secret - }); + const resolved = resolveGatewayCredentialsWithEmptyEnv( + createRemoteConfigWithMissingLocalTokenRef(), + { + remoteTokenFallback: "remote-only", + remotePasswordFallback: "remote-only", // pragma: allowlist secret + }, + ); expect(resolved).toEqual({ token: undefined, password: undefined, @@ -479,9 +460,7 @@ describe("resolveGatewayCredentialsFromConfig", () => { it("throws for unresolved local token ref in remote mode when local fallback is enabled", () => { expect(() => - resolveGatewayCredentialsFromConfig({ - cfg: createRemoteConfigWithMissingLocalTokenRef(), - env: {} as NodeJS.ProcessEnv, + resolveGatewayCredentialsWithEmptyEnv(createRemoteConfigWithMissingLocalTokenRef(), { remoteTokenFallback: "remote-env-local", remotePasswordFallback: "remote-only", // pragma: allowlist secret }), @@ -489,25 +468,17 @@ describe("resolveGatewayCredentialsFromConfig", () => { }); it("uses remote password when remote token ref is unresolved", () => { - const resolved = resolveGatewayCredentialsFromConfig({ - cfg: { - gateway: { - mode: "remote", - remote: { - url: "wss://gateway.example", - token: { source: "env", provider: "default", id: "MISSING_REMOTE_TOKEN" }, - password: "remote-password", // pragma: allowlist secret - }, - auth: {}, + const resolved = resolveGatewayCredentialsWithEmptyEnv( + cfgWithDefaultEnvSecretProvider({ + mode: "remote", + remote: { + url: "wss://gateway.example", + token: envSecretRef("MISSING_REMOTE_TOKEN"), + password: "remote-password", // pragma: allowlist secret }, - secrets: { - providers: { - default: { source: "env" }, - }, - }, - } as unknown as OpenClawConfig, - env: {} as NodeJS.ProcessEnv, - }); + auth: {}, + }), + ); expect(resolved).toEqual({ token: undefined, password: "remote-password", // pragma: allowlist secret @@ -516,38 +487,24 @@ describe("resolveGatewayCredentialsFromConfig", () => { it("throws when remote password auth relies on an unresolved SecretRef", () => { expect(() => - resolveGatewayCredentialsFromConfig({ - cfg: { - gateway: { - mode: "remote", - remote: { - url: "wss://gateway.example", - password: { source: "env", provider: "default", id: "MISSING_REMOTE_PASSWORD" }, - }, - auth: {}, + resolveGatewayCredentialsWithEmptyEnv( + cfgWithDefaultEnvSecretProvider({ + mode: "remote", + remote: { + url: "wss://gateway.example", + password: envSecretRef("MISSING_REMOTE_PASSWORD"), }, - secrets: { - providers: { - default: { source: "env" }, - }, - }, - } as unknown as OpenClawConfig, - env: {} as NodeJS.ProcessEnv, - remotePasswordFallback: "remote-only", // pragma: allowlist secret - }), + auth: {}, + }), + { remotePasswordFallback: "remote-only" }, // pragma: allowlist secret + ), ).toThrow("gateway.remote.password"); }); }); describe("resolveGatewayCredentialsFromValues", () => { it("supports config-first precedence for token/password", () => { - const resolved = resolveGatewayCredentialsFromValues({ - configToken: "config-token", - configPassword: "config-password", // pragma: allowlist secret - env: { - OPENCLAW_GATEWAY_TOKEN: "env-token", - OPENCLAW_GATEWAY_PASSWORD: "env-password", // pragma: allowlist secret - } as NodeJS.ProcessEnv, + const resolved = resolveGatewayCredentialsFromDefaultValues({ tokenPrecedence: "config-first", passwordPrecedence: "config-first", // pragma: allowlist secret }); @@ -558,18 +515,8 @@ describe("resolveGatewayCredentialsFromValues", () => { }); it("uses env-first precedence by default", () => { - const resolved = resolveGatewayCredentialsFromValues({ - configToken: "config-token", - configPassword: "config-password", // pragma: allowlist secret - env: { - OPENCLAW_GATEWAY_TOKEN: "env-token", - OPENCLAW_GATEWAY_PASSWORD: "env-password", // pragma: allowlist secret - } as NodeJS.ProcessEnv, - }); - expect(resolved).toEqual({ - token: "env-token", - password: "env-password", // pragma: allowlist secret - }); + const resolved = resolveGatewayCredentialsFromDefaultValues(); + expectEnvGatewayCredentials(resolved); }); it("rejects unresolved env var placeholders in config credentials", () => {