diff --git a/src/gateway/openai-http.e2e.test.ts b/src/gateway/openai-http.e2e.test.ts index 154b771d683..e1a3af00dc5 100644 --- a/src/gateway/openai-http.e2e.test.ts +++ b/src/gateway/openai-http.e2e.test.ts @@ -2,7 +2,13 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { HISTORY_CONTEXT_MARKER } from "../auto-reply/reply/history.js"; import { CURRENT_MESSAGE_MARKER } from "../auto-reply/reply/mentions.js"; import { emitAgentEvent } from "../infra/agent-events.js"; -import { agentCommand, getFreePort, installGatewayTestHooks, testState } from "./test-helpers.js"; +import { + agentCommand, + getFreePort, + installGatewayTestHooks, + testState, + withGatewayServer, +} from "./test-helpers.js"; installGatewayTestHooks({ scope: "suite" }); @@ -345,46 +351,46 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { }); it("returns 429 for repeated failed auth when gateway.auth.rateLimit is configured", async () => { - const { startGatewayServer } = await import("./server.js"); testState.gatewayAuth = { mode: "token", token: "secret", rateLimit: { maxAttempts: 1, windowMs: 60_000, lockoutMs: 60_000, exemptLoopback: false }, // oxlint-disable-next-line typescript/no-explicit-any } as any; - const port = await getFreePort(); - const server = await startGatewayServer(port, { - host: "127.0.0.1", - controlUiEnabled: false, - openAiChatCompletionsEnabled: true, - }); - try { - const headers = { - "content-type": "application/json", - authorization: "Bearer wrong", - }; - const body = { - model: "openclaw", - messages: [{ role: "user", content: "hi" }], - }; + await withGatewayServer( + async ({ port }) => { + const headers = { + "content-type": "application/json", + authorization: "Bearer wrong", + }; + const body = { + model: "openclaw", + messages: [{ role: "user", content: "hi" }], + }; - const first = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { - method: "POST", - headers, - body: JSON.stringify(body), - }); - expect(first.status).toBe(401); + const first = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { + method: "POST", + headers, + body: JSON.stringify(body), + }); + expect(first.status).toBe(401); - const second = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { - method: "POST", - headers, - body: JSON.stringify(body), - }); - expect(second.status).toBe(429); - expect(second.headers.get("retry-after")).toBeTruthy(); - } finally { - await server.close({ reason: "rate-limit auth test done" }); - } + const second = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { + method: "POST", + headers, + body: JSON.stringify(body), + }); + expect(second.status).toBe(429); + expect(second.headers.get("retry-after")).toBeTruthy(); + }, + { + serverOptions: { + host: "127.0.0.1", + controlUiEnabled: false, + openAiChatCompletionsEnabled: true, + }, + }, + ); }); it("streams SSE chunks when stream=true", async () => { diff --git a/src/gateway/server.auth.e2e.test.ts b/src/gateway/server.auth.e2e.test.ts index 91fb4cefcd9..3f480f0dead 100644 --- a/src/gateway/server.auth.e2e.test.ts +++ b/src/gateway/server.auth.e2e.test.ts @@ -14,6 +14,7 @@ import { startServerWithClient, testTailscaleWhois, testState, + withGatewayServer, } from "./test-helpers.js"; installGatewayTestHooks({ scope: "suite" }); @@ -596,62 +597,64 @@ describe("gateway server auth/connect", () => { } as any); const prevToken = process.env.OPENCLAW_GATEWAY_TOKEN; process.env.OPENCLAW_GATEWAY_TOKEN = "secret"; - const port = await getFreePort(); - const server = await startGatewayServer(port); - const ws = new WebSocket(`ws://127.0.0.1:${port}`, { - headers: { - origin: "https://localhost", - "x-forwarded-for": "203.0.113.10", - }, - }); - const challengePromise = onceMessage<{ payload?: unknown }>( - ws, - (o) => o.type === "event" && o.event === "connect.challenge", - ); - await new Promise((resolve) => ws.once("open", resolve)); - const challenge = await challengePromise; - const nonce = (challenge.payload as { nonce?: unknown } | undefined)?.nonce; - expect(typeof nonce).toBe("string"); - const { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem, signDevicePayload } = - await import("../infra/device-identity.js"); - const identity = loadOrCreateDeviceIdentity(); - const scopes = ["operator.admin", "operator.approvals", "operator.pairing"]; - const signedAtMs = Date.now(); - const payload = buildDeviceAuthPayload({ - deviceId: identity.deviceId, - clientId: GATEWAY_CLIENT_NAMES.CONTROL_UI, - clientMode: GATEWAY_CLIENT_MODES.WEBCHAT, - role: "operator", - scopes, - signedAtMs, - token: "secret", - nonce: String(nonce), - }); - const device = { - id: identity.deviceId, - publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem), - signature: signDevicePayload(identity.privateKeyPem, payload), - signedAt: signedAtMs, - nonce: String(nonce), - }; - const res = await connectReq(ws, { - token: "secret", - scopes, - device, - client: { - id: GATEWAY_CLIENT_NAMES.CONTROL_UI, - version: "1.0.0", - platform: "web", - mode: GATEWAY_CLIENT_MODES.WEBCHAT, - }, - }); - expect(res.ok).toBe(true); - ws.close(); - await server.close(); - if (prevToken === undefined) { - delete process.env.OPENCLAW_GATEWAY_TOKEN; - } else { - process.env.OPENCLAW_GATEWAY_TOKEN = prevToken; + try { + await withGatewayServer(async ({ port }) => { + const ws = new WebSocket(`ws://127.0.0.1:${port}`, { + headers: { + origin: "https://localhost", + "x-forwarded-for": "203.0.113.10", + }, + }); + const challengePromise = onceMessage<{ payload?: unknown }>( + ws, + (o) => o.type === "event" && o.event === "connect.challenge", + ); + await new Promise((resolve) => ws.once("open", resolve)); + const challenge = await challengePromise; + const nonce = (challenge.payload as { nonce?: unknown } | undefined)?.nonce; + expect(typeof nonce).toBe("string"); + const { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem, signDevicePayload } = + await import("../infra/device-identity.js"); + const identity = loadOrCreateDeviceIdentity(); + const scopes = ["operator.admin", "operator.approvals", "operator.pairing"]; + const signedAtMs = Date.now(); + const payload = buildDeviceAuthPayload({ + deviceId: identity.deviceId, + clientId: GATEWAY_CLIENT_NAMES.CONTROL_UI, + clientMode: GATEWAY_CLIENT_MODES.WEBCHAT, + role: "operator", + scopes, + signedAtMs, + token: "secret", + nonce: String(nonce), + }); + const device = { + id: identity.deviceId, + publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem), + signature: signDevicePayload(identity.privateKeyPem, payload), + signedAt: signedAtMs, + nonce: String(nonce), + }; + const res = await connectReq(ws, { + token: "secret", + scopes, + device, + client: { + id: GATEWAY_CLIENT_NAMES.CONTROL_UI, + version: "1.0.0", + platform: "web", + mode: GATEWAY_CLIENT_MODES.WEBCHAT, + }, + }); + expect(res.ok).toBe(true); + ws.close(); + }); + } finally { + if (prevToken === undefined) { + delete process.env.OPENCLAW_GATEWAY_TOKEN; + } else { + process.env.OPENCLAW_GATEWAY_TOKEN = prevToken; + } } }); @@ -660,46 +663,48 @@ describe("gateway server auth/connect", () => { testState.gatewayAuth = { mode: "token", token: "secret" }; const prevToken = process.env.OPENCLAW_GATEWAY_TOKEN; process.env.OPENCLAW_GATEWAY_TOKEN = "secret"; - const port = await getFreePort(); - const server = await startGatewayServer(port); - const ws = await openWs(port, { origin: originForPort(port) }); - const { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem, signDevicePayload } = - await import("../infra/device-identity.js"); - const identity = loadOrCreateDeviceIdentity(); - const signedAtMs = Date.now() - 60 * 60 * 1000; - const payload = buildDeviceAuthPayload({ - deviceId: identity.deviceId, - clientId: GATEWAY_CLIENT_NAMES.CONTROL_UI, - clientMode: GATEWAY_CLIENT_MODES.WEBCHAT, - role: "operator", - scopes: [], - signedAtMs, - token: "secret", - }); - const device = { - id: identity.deviceId, - publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem), - signature: signDevicePayload(identity.privateKeyPem, payload), - signedAt: signedAtMs, - }; - const res = await connectReq(ws, { - token: "secret", - device, - client: { - id: GATEWAY_CLIENT_NAMES.CONTROL_UI, - version: "1.0.0", - platform: "web", - mode: GATEWAY_CLIENT_MODES.WEBCHAT, - }, - }); - expect(res.ok).toBe(true); - expect((res.payload as { auth?: unknown } | undefined)?.auth).toBeUndefined(); - ws.close(); - await server.close(); - if (prevToken === undefined) { - delete process.env.OPENCLAW_GATEWAY_TOKEN; - } else { - process.env.OPENCLAW_GATEWAY_TOKEN = prevToken; + try { + await withGatewayServer(async ({ port }) => { + const ws = await openWs(port, { origin: originForPort(port) }); + const { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem, signDevicePayload } = + await import("../infra/device-identity.js"); + const identity = loadOrCreateDeviceIdentity(); + const signedAtMs = Date.now() - 60 * 60 * 1000; + const payload = buildDeviceAuthPayload({ + deviceId: identity.deviceId, + clientId: GATEWAY_CLIENT_NAMES.CONTROL_UI, + clientMode: GATEWAY_CLIENT_MODES.WEBCHAT, + role: "operator", + scopes: [], + signedAtMs, + token: "secret", + }); + const device = { + id: identity.deviceId, + publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem), + signature: signDevicePayload(identity.privateKeyPem, payload), + signedAt: signedAtMs, + }; + const res = await connectReq(ws, { + token: "secret", + device, + client: { + id: GATEWAY_CLIENT_NAMES.CONTROL_UI, + version: "1.0.0", + platform: "web", + mode: GATEWAY_CLIENT_MODES.WEBCHAT, + }, + }); + expect(res.ok).toBe(true); + expect((res.payload as { auth?: unknown } | undefined)?.auth).toBeUndefined(); + ws.close(); + }); + } finally { + if (prevToken === undefined) { + delete process.env.OPENCLAW_GATEWAY_TOKEN; + } else { + process.env.OPENCLAW_GATEWAY_TOKEN = prevToken; + } } });