From ee54a8d298ce2f78412438ce4165392e03b5d4ab Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 19:44:47 +0100 Subject: [PATCH] test: share gateway shared auth ws helpers --- .../server.shared-auth-rotation.test.ts | 53 ++++--------------- .../server.shared-token-hot-reload.test.ts | 26 ++------- ...rver.shared-token-session-rotation.test.ts | 46 +++------------- src/gateway/shared-auth.test-helpers.ts | 37 +++++++++++++ 4 files changed, 60 insertions(+), 102 deletions(-) create mode 100644 src/gateway/shared-auth.test-helpers.ts diff --git a/src/gateway/server.shared-auth-rotation.test.ts b/src/gateway/server.shared-auth-rotation.test.ts index 8cdb2682678..8ee2334f398 100644 --- a/src/gateway/server.shared-auth-rotation.test.ts +++ b/src/gateway/server.shared-auth-rotation.test.ts @@ -3,6 +3,11 @@ import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { WebSocket } from "ws"; +import { + loadGatewayConfig, + openAuthenticatedGatewayWs, + waitForGatewayWsClose, +} from "./shared-auth.test-helpers.js"; import { connectOk, getFreePort, @@ -33,14 +38,6 @@ afterAll(() => { } }); -async function openAuthenticatedWs(token: string): Promise { - const ws = new WebSocket(`ws://127.0.0.1:${port}`); - trackConnectChallengeNonce(ws); - await new Promise((resolve) => ws.once("open", resolve)); - await connectOk(ws, { token }); - return ws; -} - async function openDeviceTokenWs(): Promise { const identityPath = path.join(os.tmpdir(), `openclaw-shared-auth-${process.pid}-${port}.json`); const { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem } = @@ -79,14 +76,6 @@ async function openDeviceTokenWs(): Promise { return ws; } -async function waitForClose(ws: WebSocket): Promise<{ code: number; reason: string }> { - return await new Promise((resolve) => { - ws.once("close", (code, reason) => { - resolve({ code, reason: reason.toString() }); - }); - }); -} - async function closeWsAndWait(ws: WebSocket, timeoutMs = 2_000): Promise { if (ws.readyState === WebSocket.CLOSED) { return; @@ -113,24 +102,8 @@ async function closeWsAndWait(ws: WebSocket, timeoutMs = 2_000): Promise { }); } -async function loadCurrentConfig(ws: WebSocket): Promise<{ - hash: string; - config: Record; -}> { - const current = await rpcReq<{ - hash?: string; - config?: Record; - }>(ws, "config.get", {}); - expect(current.ok).toBe(true); - expect(typeof current.payload?.hash).toBe("string"); - return { - hash: String(current.payload?.hash), - config: structuredClone(current.payload?.config ?? {}), - }; -} - async function sendSharedTokenRotationPatch(ws: WebSocket): Promise<{ ok: boolean }> { - const current = await loadCurrentConfig(ws); + const current = await loadGatewayConfig(ws); return await rpcReq(ws, "config.patch", { baseHash: current.hash, raw: JSON.stringify({ gateway: { auth: { token: NEW_TOKEN } } }), @@ -139,7 +112,7 @@ async function sendSharedTokenRotationPatch(ws: WebSocket): Promise<{ ok: boolea } async function applyCurrentConfig(ws: WebSocket) { - const current = await loadCurrentConfig(ws); + const current = await loadGatewayConfig(ws); return await rpcReq(ws, "config.apply", { baseHash: current.hash, raw: JSON.stringify(current.config, null, 2), @@ -164,9 +137,9 @@ describe("gateway shared auth rotation", () => { }); it("disconnects existing shared-token websocket sessions after config.patch rotates auth", async () => { - const ws = await openAuthenticatedWs(OLD_TOKEN); + const ws = await openAuthenticatedGatewayWs(port, OLD_TOKEN); try { - const closed = waitForClose(ws); + const closed = waitForGatewayWsClose(ws); const res = await sendSharedTokenRotationPatch(ws); expect(res.ok).toBe(true); @@ -238,17 +211,13 @@ describe("gateway shared auth rotation with unchanged SecretRefs", () => { }); async function openSecretRefAuthenticatedWs(): Promise { - const ws = new WebSocket(`ws://127.0.0.1:${secretRefPort}`); - trackConnectChallengeNonce(ws); - await new Promise((resolve) => ws.once("open", resolve)); - await connectOk(ws, { token: OLD_TOKEN }); - return ws; + return openAuthenticatedGatewayWs(secretRefPort, OLD_TOKEN); } it("disconnects shared-auth websocket sessions when config.apply rewrites a SecretRef token", async () => { const ws = await openSecretRefAuthenticatedWs(); try { - const closed = waitForClose(ws); + const closed = waitForGatewayWsClose(ws); const res = await applyCurrentConfig(ws); expect(res.ok).toBe(true); await expect(closed).resolves.toEqual({ diff --git a/src/gateway/server.shared-token-hot-reload.test.ts b/src/gateway/server.shared-token-hot-reload.test.ts index a4174040c99..1727cef003d 100644 --- a/src/gateway/server.shared-token-hot-reload.test.ts +++ b/src/gateway/server.shared-token-hot-reload.test.ts @@ -1,14 +1,12 @@ import fs from "node:fs/promises"; import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { WebSocket } from "ws"; +import { openAuthenticatedGatewayWs, waitForGatewayWsClose } from "./shared-auth.test-helpers.js"; import { - connectOk, getFreePort, installGatewayTestHooks, rpcReq, startGatewayServer, testState, - trackConnectChallengeNonce, } from "./test-helpers.js"; installGatewayTestHooks({ scope: "suite" }); @@ -35,22 +33,6 @@ function buildSharedTokenReloadConfig(): Record { }; } -async function openAuthenticatedWs(token: string): Promise { - const ws = new WebSocket(`ws://127.0.0.1:${port}`); - trackConnectChallengeNonce(ws); - await new Promise((resolve) => ws.once("open", resolve)); - await connectOk(ws, { token }); - return ws; -} - -async function waitForClose(ws: WebSocket): Promise<{ code: number; reason: string }> { - return await new Promise((resolve) => { - ws.once("close", (code, reason) => { - resolve({ code, reason: reason.toString() }); - }); - }); -} - beforeAll(async () => { const configPath = process.env.OPENCLAW_CONFIG_PATH; if (!configPath) { @@ -79,9 +61,9 @@ afterAll(async () => { describe("gateway shared token hot reload rotation", () => { it("disconnects existing shared-token websocket sessions after hot reload picks up a rotated SecretRef value", async () => { - const ws = await openAuthenticatedWs(OLD_TOKEN); + const ws = await openAuthenticatedGatewayWs(port, OLD_TOKEN); try { - const closed = waitForClose(ws); + const closed = waitForGatewayWsClose(ws); process.env[SECRET_REF_TOKEN_ID] = NEW_TOKEN; const reload = await rpcReq<{ warningCount?: number }>(ws, "secrets.reload", {}).catch( (err: unknown) => (err instanceof Error ? err : new Error(String(err))), @@ -95,7 +77,7 @@ describe("gateway shared token hot reload rotation", () => { expect(reload.ok).toBe(true); } - const freshWs = await openAuthenticatedWs(NEW_TOKEN); + const freshWs = await openAuthenticatedGatewayWs(port, NEW_TOKEN); freshWs.close(); } finally { ws.close(); diff --git a/src/gateway/server.shared-token-session-rotation.test.ts b/src/gateway/server.shared-token-session-rotation.test.ts index 7d5dfd1f5fc..3375fd4fd6c 100644 --- a/src/gateway/server.shared-token-session-rotation.test.ts +++ b/src/gateway/server.shared-token-session-rotation.test.ts @@ -1,14 +1,16 @@ import fs from "node:fs/promises"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { WebSocket } from "ws"; import { - connectOk, + loadGatewayConfig, + openAuthenticatedGatewayWs, + waitForGatewayWsClose, +} from "./shared-auth.test-helpers.js"; +import { getFreePort, installGatewayTestHooks, rpcReq, startGatewayServer, testState, - trackConnectChallengeNonce, } from "./test-helpers.js"; installGatewayTestHooks({ scope: "suite" }); @@ -71,45 +73,13 @@ function buildConfigSetWithRotatedToken(config: Record): Record return next; } -async function openAuthenticatedWs(token: string): Promise { - const ws = new WebSocket(`ws://127.0.0.1:${port}`); - trackConnectChallengeNonce(ws); - await new Promise((resolve) => ws.once("open", resolve)); - await connectOk(ws, { token }); - return ws; -} - -async function waitForClose(ws: WebSocket): Promise<{ code: number; reason: string }> { - return await new Promise((resolve) => { - ws.once("close", (code, reason) => { - resolve({ code, reason: reason.toString() }); - }); - }); -} - -async function loadCurrentConfig(ws: WebSocket): Promise<{ - hash: string; - config: Record; -}> { - const current = await rpcReq<{ - hash?: string; - config?: Record; - }>(ws, "config.get", {}); - expect(current.ok).toBe(true); - expect(typeof current.payload?.hash).toBe("string"); - return { - hash: String(current.payload?.hash), - config: structuredClone(current.payload?.config ?? {}), - }; -} - describe("gateway shared token session rotation", () => { it("invalidates shared-token websocket sessions after config.set rotation even with reload mode off", async () => { - const ws = await openAuthenticatedWs(OLD_TOKEN); + const ws = await openAuthenticatedGatewayWs(port, OLD_TOKEN); try { - const current = await loadCurrentConfig(ws); + const current = await loadGatewayConfig(ws); const nextConfig = buildConfigSetWithRotatedToken(current.config); - const closed = waitForClose(ws); + const closed = waitForGatewayWsClose(ws); const setRes = await rpcReq(ws, "config.set", { baseHash: current.hash, raw: JSON.stringify(nextConfig, null, 2), diff --git a/src/gateway/shared-auth.test-helpers.ts b/src/gateway/shared-auth.test-helpers.ts new file mode 100644 index 00000000000..69f3007e1f1 --- /dev/null +++ b/src/gateway/shared-auth.test-helpers.ts @@ -0,0 +1,37 @@ +import { expect } from "vitest"; +import { WebSocket } from "ws"; +import { connectOk, rpcReq, trackConnectChallengeNonce } from "./test-helpers.js"; + +export async function openAuthenticatedGatewayWs(port: number, token: string): Promise { + const ws = new WebSocket(`ws://127.0.0.1:${port}`); + trackConnectChallengeNonce(ws); + await new Promise((resolve) => ws.once("open", resolve)); + await connectOk(ws, { token }); + return ws; +} + +export async function waitForGatewayWsClose( + ws: WebSocket, +): Promise<{ code: number; reason: string }> { + return await new Promise((resolve) => { + ws.once("close", (code, reason) => { + resolve({ code, reason: reason.toString() }); + }); + }); +} + +export async function loadGatewayConfig(ws: WebSocket): Promise<{ + hash: string; + config: Record; +}> { + const current = await rpcReq<{ + hash?: string; + config?: Record; + }>(ws, "config.get", {}); + expect(current.ok).toBe(true); + expect(typeof current.payload?.hash).toBe("string"); + return { + hash: String(current.payload?.hash), + config: structuredClone(current.payload?.config ?? {}), + }; +}