Julius Marmingeharden transport320f2498 months agoHistory
175 lines · 4.5 KB · TypeScript
RawDownload
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { WsTransport } from "./wsTransport"; type WsEventType = "open" | "message" | "close" | "error";type WsListener = (event?: { data?: unknown }) => void; const sockets: MockWebSocket[] = []; class MockWebSocket {  static readonly CONNECTING = 0;  static readonly OPEN = 1;  static readonly CLOSING = 2;  static readonly CLOSED = 3;   readyState = MockWebSocket.CONNECTING;  readonly sent: string[] = [];  private readonly listeners = new Map<WsEventType, Set<WsListener>>();   constructor(_url: string) {    sockets.push(this);  }   addEventListener(type: WsEventType, listener: WsListener) {    const listeners = this.listeners.get(type) ?? new Set<WsListener>();    listeners.add(listener);    this.listeners.set(type, listeners);  }   send(data: string) {    this.sent.push(data);  }   close() {    this.readyState = MockWebSocket.CLOSED;    this.emit("close");  }   open() {    this.readyState = MockWebSocket.OPEN;    this.emit("open");  }   serverMessage(data: unknown) {    this.emit("message", { data });  }   private emit(type: WsEventType, event?: { data?: unknown }) {    const listeners = this.listeners.get(type);    if (!listeners) return;    for (const listener of listeners) {      listener(event);    }  }} const originalWebSocket = globalThis.WebSocket; function getSocket(): MockWebSocket {  const socket = sockets.at(-1);  if (!socket) {    throw new Error("Expected a websocket instance");  }  return socket;} beforeEach(() => {  sockets.length = 0;   Object.defineProperty(globalThis, "window", {    configurable: true,    value: {      location: { hostname: "localhost", port: "3020" },      desktopBridge: undefined,    },  });   globalThis.WebSocket = MockWebSocket as unknown as typeof WebSocket;}); afterEach(() => {  globalThis.WebSocket = originalWebSocket;  vi.restoreAllMocks();}); describe("WsTransport", () => {  it("routes valid push envelopes to channel listeners", () => {    const transport = new WsTransport("ws://localhost:3020");    const socket = getSocket();    socket.open();     const listener = vi.fn();    transport.subscribe("providers.event", listener);     socket.serverMessage(      JSON.stringify({        type: "push",        channel: "providers.event",        data: { status: "ok" },      }),    );     expect(listener).toHaveBeenCalledTimes(1);    expect(listener).toHaveBeenCalledWith({ status: "ok" });     transport.dispose();  });   it("resolves pending requests for valid response envelopes", async () => {    const transport = new WsTransport("ws://localhost:3020");    const socket = getSocket();    socket.open();     const requestPromise = transport.request("projects.list");    const sent = socket.sent.at(-1);    if (!sent) {      throw new Error("Expected request envelope to be sent");    }     const requestEnvelope = JSON.parse(sent) as { id: string };    socket.serverMessage(      JSON.stringify({        id: requestEnvelope.id,        result: { projects: [] },      }),    );     await expect(requestPromise).resolves.toEqual({ projects: [] });     transport.dispose();  });   it("drops malformed envelopes without crashing transport", () => {    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});    const transport = new WsTransport("ws://localhost:3020");    const socket = getSocket();    socket.open();     const listener = vi.fn();    transport.subscribe("providers.event", listener);     socket.serverMessage("{ invalid-json");    socket.serverMessage(      JSON.stringify({        type: "push",        channel: 42,        data: { bad: true },      }),    );    socket.serverMessage(      JSON.stringify({        type: "push",        channel: "providers.event",        data: { ok: true },      }),    );     expect(listener).toHaveBeenCalledTimes(1);    expect(listener).toHaveBeenCalledWith({ ok: true });    expect(warnSpy).toHaveBeenCalledTimes(2);    expect(warnSpy).toHaveBeenNthCalledWith(1, "Dropped inbound WebSocket envelope", {      reason: "decode-failed",      issue:        "SchemaError: SyntaxError: Expected property name or '}' in JSON at position 2 (line 1 column 3)",      raw: "{ invalid-json",    });    expect(warnSpy).toHaveBeenNthCalledWith(2, "Dropped inbound WebSocket envelope", {      reason: "decode-failed",      issue: expect.stringContaining("SchemaError: Expected string, got 42"),      raw: '{"type":"push","channel":42,"data":{"bad":true}}',    });     transport.dispose();  });});