Archived
test: cover session websocket cwd proxying
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import Fastify, { type FastifyInstance } from "fastify";
|
import Fastify, { type FastifyInstance } from "fastify";
|
||||||
import fastifyWebsocket from "@fastify/websocket";
|
import fastifyWebsocket from "@fastify/websocket";
|
||||||
|
import { WebSocket, WebSocketServer } from "ws";
|
||||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import { registerSessionProxyRoutes } from "./sessionProxyRoutes";
|
import { registerSessionProxyRoutes } from "./sessionProxyRoutes";
|
||||||
|
|
||||||
@@ -9,12 +10,13 @@ let daemon: FakeSessionDaemon;
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
app = Fastify({ logger: false });
|
app = Fastify({ logger: false });
|
||||||
await app.register(fastifyWebsocket);
|
await app.register(fastifyWebsocket);
|
||||||
daemon = new FakeSessionDaemon();
|
daemon = await FakeSessionDaemon.create();
|
||||||
registerSessionProxyRoutes(app, daemon, "/api/machines/local");
|
registerSessionProxyRoutes(app, daemon, "/api/machines/local");
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await app.close();
|
await app.close();
|
||||||
|
await daemon.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("machine-scoped session proxy routes", () => {
|
describe("machine-scoped session proxy routes", () => {
|
||||||
@@ -33,17 +35,86 @@ describe("machine-scoped session proxy routes", () => {
|
|||||||
expect(response.json()).toEqual({ ok: true });
|
expect(response.json()).toEqual({ ok: true });
|
||||||
expect(daemon.requests).toEqual([{ method: "POST", path: "/auth/api-key", body: { providerId: "p", key: "k" } }]);
|
expect(daemon.requests).toEqual([{ method: "POST", path: "/auth/api-key", body: { providerId: "p", key: "k" } }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves cwd query context when forwarding session event websockets", async () => {
|
||||||
|
await app.listen({ host: "127.0.0.1", port: 0 });
|
||||||
|
const socket = new WebSocket(`${serverUrl(app)}/api/machines/local/sessions/session-1/events?cwd=${encodeURIComponent("/repo")}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await waitForOpen(socket);
|
||||||
|
expect(daemon.websocketPaths).toEqual(["/sessions/session-1/events?cwd=%2Frepo"]);
|
||||||
|
} finally {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
class FakeSessionDaemon {
|
class FakeSessionDaemon {
|
||||||
readonly requests: { method: string; path: string; body: unknown }[] = [];
|
readonly requests: { method: string; path: string; body: unknown }[] = [];
|
||||||
|
readonly websocketPaths: string[] = [];
|
||||||
|
private readonly sockets = new Set<WebSocket>();
|
||||||
|
|
||||||
|
private constructor(private readonly upstream: WebSocketServer) {
|
||||||
|
this.upstream.on("connection", (socket) => {
|
||||||
|
this.sockets.add(socket);
|
||||||
|
socket.on("close", () => { this.sockets.delete(socket); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(): Promise<FakeSessionDaemon> {
|
||||||
|
const upstream = new WebSocketServer({ host: "127.0.0.1", port: 0 });
|
||||||
|
await waitForListening(upstream);
|
||||||
|
return new FakeSessionDaemon(upstream);
|
||||||
|
}
|
||||||
|
|
||||||
request(method: string, path: string, body?: unknown): Promise<{ statusCode: number; headers: Record<string, string>; body: string }> {
|
request(method: string, path: string, body?: unknown): Promise<{ statusCode: number; headers: Record<string, string>; body: string }> {
|
||||||
this.requests.push({ method, path, body });
|
this.requests.push({ method, path, body });
|
||||||
return Promise.resolve({ statusCode: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true }) });
|
return Promise.resolve({ statusCode: 200, headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
connectWebSocket(): never {
|
connectWebSocket(path: string): WebSocket {
|
||||||
throw new Error("not implemented");
|
this.websocketPaths.push(path);
|
||||||
|
return new WebSocket(`${webSocketServerUrl(this.upstream)}${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
for (const socket of this.sockets) socket.terminate();
|
||||||
|
await closeWebSocketServer(this.upstream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serverUrl(instance: FastifyInstance): string {
|
||||||
|
const address = instance.server.address();
|
||||||
|
if (address === null || typeof address === "string") throw new Error("Expected TCP server address");
|
||||||
|
return `ws://127.0.0.1:${String(address.port)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function webSocketServerUrl(server: WebSocketServer): string {
|
||||||
|
const address = server.address();
|
||||||
|
if (address === null || typeof address === "string") throw new Error("Expected TCP server address");
|
||||||
|
return `ws://127.0.0.1:${String(address.port)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForListening(server: WebSocketServer): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
server.once("listening", () => { resolve(); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWebSocketServer(server: WebSocketServer): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
server.close((error) => {
|
||||||
|
if (error !== undefined) reject(error);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForOpen(socket: WebSocket): Promise<void> {
|
||||||
|
if (socket.readyState === WebSocket.OPEN) return Promise.resolve();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
socket.once("open", () => { resolve(); });
|
||||||
|
socket.once("error", reject);
|
||||||
|
socket.once("close", () => { reject(new Error("WebSocket closed before opening")); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user