mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
test: share gateway shared auth ws helpers
This commit is contained in:
@@ -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<WebSocket> {
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
||||
trackConnectChallengeNonce(ws);
|
||||
await new Promise<void>((resolve) => ws.once("open", resolve));
|
||||
await connectOk(ws, { token });
|
||||
return ws;
|
||||
}
|
||||
|
||||
async function openDeviceTokenWs(): Promise<WebSocket> {
|
||||
const identityPath = path.join(os.tmpdir(), `openclaw-shared-auth-${process.pid}-${port}.json`);
|
||||
const { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem } =
|
||||
@@ -79,14 +76,6 @@ async function openDeviceTokenWs(): Promise<WebSocket> {
|
||||
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<void> {
|
||||
if (ws.readyState === WebSocket.CLOSED) {
|
||||
return;
|
||||
@@ -113,24 +102,8 @@ async function closeWsAndWait(ws: WebSocket, timeoutMs = 2_000): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCurrentConfig(ws: WebSocket): Promise<{
|
||||
hash: string;
|
||||
config: Record<string, unknown>;
|
||||
}> {
|
||||
const current = await rpcReq<{
|
||||
hash?: string;
|
||||
config?: Record<string, unknown>;
|
||||
}>(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<WebSocket> {
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${secretRefPort}`);
|
||||
trackConnectChallengeNonce(ws);
|
||||
await new Promise<void>((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({
|
||||
|
||||
@@ -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<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
async function openAuthenticatedWs(token: string): Promise<WebSocket> {
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
||||
trackConnectChallengeNonce(ws);
|
||||
await new Promise<void>((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();
|
||||
|
||||
@@ -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<string, unknown>): Record
|
||||
return next;
|
||||
}
|
||||
|
||||
async function openAuthenticatedWs(token: string): Promise<WebSocket> {
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
||||
trackConnectChallengeNonce(ws);
|
||||
await new Promise<void>((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<string, unknown>;
|
||||
}> {
|
||||
const current = await rpcReq<{
|
||||
hash?: string;
|
||||
config?: Record<string, unknown>;
|
||||
}>(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),
|
||||
|
||||
37
src/gateway/shared-auth.test-helpers.ts
Normal file
37
src/gateway/shared-auth.test-helpers.ts
Normal file
@@ -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<WebSocket> {
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
||||
trackConnectChallengeNonce(ws);
|
||||
await new Promise<void>((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<string, unknown>;
|
||||
}> {
|
||||
const current = await rpcReq<{
|
||||
hash?: string;
|
||||
config?: Record<string, unknown>;
|
||||
}>(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 ?? {}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user