import { fetchWebSocketTicket } from "@/websocket/api/fetchWebSocketTicket"; import { createWebSocketConnection } from "@/websocket/connection/createConnection"; import { WS_INBOUND_EVENTS } from "@/websocket/protocols"; import { waitFor } from "@testing-library/dom"; import { setupWebsocket } from "./utils/setupWebsocket"; jest.mock("@/websocket/api/fetchWebSocketTicket"); describe("createWebSocketConnection", () => { let mockWebSocket: jest.Mock; let cleanup: () => void; let webSocketInstances: any[] = []; const mockTicket = { value: "test-ticket-123", clientAddress: "127.0.0.1", generatedOn: "2025-01-12T10:00:00Z", validUntil: "2025-01-12T11:00:00Z", username: "testuser", }; /** ---------- Helpers ---------- */ const getWs = () => webSocketInstances[0]; const triggerEvent = (ws: any, event: string, payload?: any) => { ws._listeners[event]?.[0]?.(payload); }; const createAndOpenConnection = async () => { const mockCallbacks = { onMessage: jest.fn(), onClose: jest.fn(), onError: jest.fn(), }; const promise = createWebSocketConnection(mockCallbacks); await waitFor(() => { expect(webSocketInstances.length).toBe(1); }); triggerEvent(getWs(), WS_INBOUND_EVENTS.CONNECTION_OPENED); const socket = await promise; return { socket, ws: getWs(), promise, mockCallbacks }; }; /** ---------- Setup ---------- */ beforeEach(() => { ({ webSocketInstances, mockWebSocket, cleanup } = setupWebsocket()); (window as any).WEBSOCKET_URL = "wss://calendar.example.com"; (fetchWebSocketTicket as jest.Mock).mockResolvedValue(mockTicket); }); afterEach(() => { jest.clearAllMocks(); delete (window as any).WEBSOCKET_URL; delete (window as any).CALENDAR_BASE_URL; cleanup(); }); /** ---------- Tests ---------- */ it("throws when WEBSOCKET_URL is not defined", async () => { delete (window as any).WEBSOCKET_URL; const mockCallbacks = { onMessage: jest.fn(), }; await expect(createWebSocketConnection(mockCallbacks)).rejects.toThrow( "WEBSOCKET_URL is not defined" ); }); it("fetches WebSocket ticket", async () => { await createAndOpenConnection(); expect(fetchWebSocketTicket).toHaveBeenCalledTimes(1); }); it("creates WebSocket with correct URL and ticket", async () => { await createAndOpenConnection(); expect(mockWebSocket).toHaveBeenCalledWith( "wss://calendar.example.com/ws?ticket=test-ticket-123" ); }); it("creates WebSocket with correct URL and ticket without the WEBSOCKET_URL", async () => { delete (window as any).WEBSOCKET_URL; (window as any).CALENDAR_BASE_URL = "https://calendar.example.com"; await createAndOpenConnection(); expect(mockWebSocket).toHaveBeenCalledWith( "wss://calendar.example.com/ws?ticket=test-ticket-123" ); }); it("resolves with socket when connection opens", async () => { const { socket, ws } = await createAndOpenConnection(); expect(socket).toBe(ws); }); it("rejects when connection fails", async () => { const mockCallbacks = { onMessage: jest.fn(), }; const promise = createWebSocketConnection(mockCallbacks); await waitFor(() => { expect(webSocketInstances.length).toBe(1); }); triggerEvent(getWs(), WS_INBOUND_EVENTS.ERROR, new Event("error")); await expect(promise).rejects.toThrow("WebSocket connection failed"); }); it("attaches message event listener", async () => { const { ws } = await createAndOpenConnection(); expect(ws._listeners[WS_INBOUND_EVENTS.MESSAGE]).toBeDefined(); expect(ws._listeners[WS_INBOUND_EVENTS.MESSAGE].length).toBeGreaterThan(0); }); it("attaches close event listener", async () => { const { ws } = await createAndOpenConnection(); expect(ws._listeners[WS_INBOUND_EVENTS.CONNECTION_CLOSED]).toBeDefined(); }); it("handles invalid JSON messages", async () => { const errorSpy = jest.spyOn(console, "error").mockImplementation(); const { ws } = await createAndOpenConnection(); triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, { data: "invalid json" }); expect(errorSpy).toHaveBeenCalledWith( "Failed to parse WebSocket message:", expect.any(Error) ); errorSpy.mockRestore(); }); it("rejects on timeout", async () => { jest.useFakeTimers(); const mockCallbacks = { onMessage: jest.fn(), }; const promise = createWebSocketConnection(mockCallbacks); await waitFor(() => { expect(webSocketInstances.length).toBe(1); }); jest.advanceTimersByTime(10000); await expect(promise).rejects.toThrow("WebSocket connection timed out"); jest.useRealTimers(); }); it("calls onMessage callback when message received", async () => { const { ws, mockCallbacks } = await createAndOpenConnection(); const testMessage = { type: "test", payload: "data" }; triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, { data: JSON.stringify(testMessage), }); expect(mockCallbacks.onMessage).toHaveBeenCalledWith(testMessage); }); it("does not call onMessage when JSON parsing fails", async () => { const errorSpy = jest.spyOn(console, "error").mockImplementation(); const { ws, mockCallbacks } = await createAndOpenConnection(); triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, { data: "invalid json" }); expect(mockCallbacks.onMessage).not.toHaveBeenCalled(); expect(errorSpy).toHaveBeenCalledWith( "Failed to parse WebSocket message:", expect.any(Error) ); errorSpy.mockRestore(); }); it("calls onClose callback when connection closes", async () => { const { ws, mockCallbacks } = await createAndOpenConnection(); const closeEvent = new CloseEvent("close", { code: 1000, reason: "Normal closure", }); triggerEvent(ws, WS_INBOUND_EVENTS.CONNECTION_CLOSED, closeEvent); expect(mockCallbacks.onClose).toHaveBeenCalledWith(closeEvent); }); it("calls onError callback when error occurs", async () => { const { ws, mockCallbacks } = await createAndOpenConnection(); const errorEvent = new Event("error"); triggerEvent(ws, WS_INBOUND_EVENTS.ERROR, errorEvent); expect(mockCallbacks.onError).toHaveBeenCalledWith(errorEvent); }); });