import { setSelectedCalendars } from "@/utils/storage/setSelectedCalendars"; import { createWebSocketConnection } from "@/websocket/connection/createConnection"; import { registerToCalendars } from "@/websocket/operations/registerToCalendars"; import { unregisterToCalendars } from "@/websocket/operations/unregisterToCalendars"; import { WebSocketGate } from "@/websocket/WebSocketGate"; import { configureStore } from "@reduxjs/toolkit"; import { act, cleanup, render, waitFor } from "@testing-library/react"; import { Provider } from "react-redux"; jest.mock("@/websocket/connection/createConnection"); jest.mock("@/websocket/operations/registerToCalendars"); jest.mock("@/websocket/operations/unregisterToCalendars"); describe("WebSocketGate", () => { let store: any; let mockSocket: any; const createMockStore = ( userData: any = { id: "1" }, tokens: any = { access: "token" } ) => { return configureStore({ reducer: { user: (state = { userData, tokens }) => state, calendars: (state = { list: {} }) => state, }, }); }; const createMockSocket = (readyState = WebSocket.OPEN) => ({ readyState, close: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn(), cleanup: jest.fn(), }); beforeEach(() => { store = createMockStore(); mockSocket = createMockSocket(); localStorage.clear(); }); afterEach(() => { cleanup(); jest.clearAllMocks(); localStorage.clear(); }); describe("Authentication", () => { it("should not create connection when user is not authenticated", () => { const unauthStore = createMockStore(null, null); render( ); expect(createWebSocketConnection).not.toHaveBeenCalled(); }); it("should create connection when user is authenticated", async () => { (createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket); render( ); await waitFor(() => { expect(createWebSocketConnection).toHaveBeenCalledTimes(1); }); }); it("should close existing socket when user logs out", async () => { (createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket); const { rerender } = render( ); await waitFor(() => { expect(createWebSocketConnection).toHaveBeenCalled(); }); const unauthStore = createMockStore(null, null); rerender( ); await waitFor(() => { expect(mockSocket.close).toHaveBeenCalled(); }); }); }); describe("Socket Connection Management", () => { it("should create connection with callbacks", async () => { (createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket); render( ); await waitFor(() => { expect(createWebSocketConnection).toHaveBeenCalledWith( expect.objectContaining({ onMessage: expect.any(Function), onClose: expect.any(Function), onError: expect.any(Function), }) ); }); }); it("should handle socket close via callback", async () => { let onCloseCallback: Function | undefined; (createWebSocketConnection as jest.Mock).mockImplementation( (callbacks) => { onCloseCallback = callbacks.onClose; return Promise.resolve(mockSocket); } ); render( ); await waitFor(() => { expect(createWebSocketConnection).toHaveBeenCalled(); }); // Simulate close event await act(async () => { onCloseCallback?.(new CloseEvent("close")); }); // Verify that subsequent calendar changes don't try to register await act(async () => { setSelectedCalendars(["cal1"]); }); await waitFor(() => { expect(registerToCalendars).not.toHaveBeenCalled(); }); }); it("should handle connection errors gracefully", async () => { const consoleError = jest.spyOn(console, "error").mockImplementation(); (createWebSocketConnection as jest.Mock).mockRejectedValue( new Error("Connection failed") ); render( ); await waitFor(() => { expect(consoleError).toHaveBeenCalledWith( "Failed to create WebSocket connection:", expect.any(Error) ); }); consoleError.mockRestore(); }); it("should close socket on component unmount", async () => { (createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket); const { unmount } = render( ); await waitFor(() => { expect(createWebSocketConnection).toHaveBeenCalled(); }); unmount(); expect(mockSocket.close).toHaveBeenCalled(); }); }); describe("Calendar Registration", () => { it("should not register if socket is not open", async () => { localStorage.setItem( "selectedCalendars", JSON.stringify(["cal1", "cal2"]) ); const connectingSocket = createMockSocket(WebSocket.CONNECTING); (createWebSocketConnection as jest.Mock).mockResolvedValue( connectingSocket ); render( ); await waitFor(() => { expect(createWebSocketConnection).toHaveBeenCalled(); }); expect(registerToCalendars).not.toHaveBeenCalled(); }); it("should register to calendars when socket opens and calendars are selected", async () => { localStorage.setItem( "selectedCalendars", JSON.stringify(["cal1", "cal2"]) ); (createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket); render( ); await waitFor(() => { expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [ "/calendars/cal1", "/calendars/cal2", ]); }); }); it("should register only new calendars when calendar list changes", async () => { localStorage.setItem("selectedCalendars", JSON.stringify(["cal1"])); (createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket); render( ); await waitFor(() => { expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [ "/calendars/cal1", ]); }); jest.clearAllMocks(); await act(async () => { setSelectedCalendars(["cal1", "cal2", "cal3"]); }); await waitFor(() => { expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [ "/calendars/cal2", "/calendars/cal3", ]); }); }); it("should unregister removed calendars when calendar list changes", async () => { localStorage.setItem( "selectedCalendars", JSON.stringify(["cal1", "cal2", "cal3"]) ); (createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket); render( ); await waitFor(() => { expect(registerToCalendars).toHaveBeenCalled(); }); jest.clearAllMocks(); await act(async () => { setSelectedCalendars(["cal1"]); }); await waitFor(() => { expect(unregisterToCalendars).toHaveBeenCalledWith(mockSocket, [ "/calendars/cal2", "/calendars/cal3", ]); }); }); it("should both register and unregister when calendar list partially changes", async () => { localStorage.setItem( "selectedCalendars", JSON.stringify(["cal1", "cal2"]) ); (createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket); render( ); await waitFor(() => { expect(registerToCalendars).toHaveBeenCalled(); }); jest.clearAllMocks(); await act(async () => { setSelectedCalendars(["cal1", "cal3"]); }); await waitFor(() => { expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [ "/calendars/cal3", ]); expect(unregisterToCalendars).toHaveBeenCalledWith(mockSocket, [ "/calendars/cal2", ]); }); }); it("should handle registration errors and not update previous calendar list", async () => { const consoleError = jest.spyOn(console, "error").mockImplementation(); localStorage.setItem("selectedCalendars", JSON.stringify(["cal1"])); (createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket); (registerToCalendars as jest.Mock).mockImplementation(() => { throw new Error("Registration failed"); }); render( ); await waitFor(() => { expect(consoleError).toHaveBeenCalledWith( "Failed to register calendar:", expect.any(Error) ); }); await waitFor(() => { expect(mockSocket.close).not.toHaveBeenCalled(); }); jest.clearAllMocks(); (registerToCalendars as jest.Mock).mockImplementation(() => {}); await act(async () => { setSelectedCalendars(["cal1", "cal2"]); }); await waitFor(() => { // Should register both calendars since previous update failed and didn't update the ref expect(registerToCalendars).toHaveBeenCalledWith(mockSocket, [ "/calendars/cal1", "/calendars/cal2", ]); }); consoleError.mockRestore(); }); it("should not attempt registration when no calendars are selected", async () => { localStorage.setItem("selectedCalendars", JSON.stringify([])); (createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket); render( ); await waitFor(() => { expect(createWebSocketConnection).toHaveBeenCalled(); }); expect(registerToCalendars).not.toHaveBeenCalled(); expect(unregisterToCalendars).not.toHaveBeenCalled(); }); }); describe("Edge Cases", () => { it("should handle rapid calendar changes correctly", async () => { localStorage.setItem("selectedCalendars", JSON.stringify(["cal1"])); (createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket); render( ); await waitFor(() => { expect(registerToCalendars).toHaveBeenCalledTimes(1); }); // Rapid changes await act(async () => { setSelectedCalendars(["cal1", "cal2"]); setSelectedCalendars(["cal1", "cal2", "cal3"]); }); await waitFor(() => { expect(registerToCalendars).toHaveBeenCalled(); }); }); it("should handle socket closed during calendar update", async () => { localStorage.setItem("selectedCalendars", JSON.stringify(["cal1"])); (createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket); render( ); await waitFor(() => { expect(registerToCalendars).toHaveBeenCalled(); }); jest.clearAllMocks(); await act(async () => { mockSocket.readyState = WebSocket.CLOSED; setSelectedCalendars(["cal1", "cal2"]); }); // Wait a bit to ensure the effect would have run if it was going to jest.useFakeTimers(); jest.advanceTimersByTime(100); jest.useRealTimers(); expect(registerToCalendars).not.toHaveBeenCalled(); }); it("should handle unregistration errors gracefully", async () => { const consoleError = jest.spyOn(console, "error").mockImplementation(); localStorage.setItem( "selectedCalendars", JSON.stringify(["cal1", "cal2"]) ); (createWebSocketConnection as jest.Mock).mockResolvedValue(mockSocket); render( ); await waitFor(() => { expect(registerToCalendars).toHaveBeenCalled(); }); jest.clearAllMocks(); (unregisterToCalendars as jest.Mock).mockImplementation(() => { throw new Error("Unregistration failed"); }); await act(async () => { setSelectedCalendars(["cal1"]); }); await waitFor(() => { expect(consoleError).toHaveBeenCalledWith( "Failed to unregister calendar:", expect.any(Error) ); }); consoleError.mockRestore(); }); it("should handle socket that becomes open after initial connection", async () => { const closedSocket = createMockSocket(WebSocket.CONNECTING); localStorage.setItem("selectedCalendars", JSON.stringify(["cal1"])); (createWebSocketConnection as jest.Mock).mockResolvedValue(closedSocket); render( ); await waitFor(() => { expect(createWebSocketConnection).toHaveBeenCalled(); }); // Socket is not open yet, should not register expect(registerToCalendars).not.toHaveBeenCalled(); // Simulate socket becoming open await act(async () => { closedSocket.readyState = WebSocket.OPEN; // Trigger a calendar change to re-run the effect setSelectedCalendars(["cal1", "cal2"]); }); // Still should not register because isSocketOpen state is false // The component only sets isSocketOpen to true when socket.readyState is OPEN during connection expect(registerToCalendars).not.toHaveBeenCalled(); }); }); });