fix(#338): Enforce WSS in production and add connect_error handling
- Add validateWebSocketSecurity() to warn when using ws:// in production - Add connect_error event handler to capture connection failures - Expose connectionError state to consumers via hook and provider - Add comprehensive tests for WSS enforcement and error handling Refs #338 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -232,4 +232,142 @@ describe("useWebSocket", (): void => {
|
||||
expect(mockSocket.off).toHaveBeenCalledWith("task:updated", expect.any(Function));
|
||||
expect(mockSocket.off).toHaveBeenCalledWith("task:deleted", expect.any(Function));
|
||||
});
|
||||
|
||||
describe("connect_error handling", (): void => {
|
||||
it("should handle connect_error events and expose error state", async (): Promise<void> => {
|
||||
const { result } = renderHook(() => useWebSocket("workspace-123", "token"));
|
||||
|
||||
expect(result.current.connectionError).toBeNull();
|
||||
|
||||
const error = new Error("Connection refused");
|
||||
|
||||
act(() => {
|
||||
eventHandlers.connect_error?.(error);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.connectionError).toEqual({
|
||||
message: "Connection refused",
|
||||
type: "connect_error",
|
||||
description: "Failed to establish WebSocket connection",
|
||||
});
|
||||
expect(result.current.isConnected).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle connect_error with missing message", async (): Promise<void> => {
|
||||
const { result } = renderHook(() => useWebSocket("workspace-123", "token"));
|
||||
|
||||
const error = new Error();
|
||||
|
||||
act(() => {
|
||||
eventHandlers.connect_error?.(error);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.connectionError).toEqual({
|
||||
message: "Connection failed",
|
||||
type: "connect_error",
|
||||
description: "Failed to establish WebSocket connection",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should clear connection error on reconnect", async (): Promise<void> => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ workspaceId }: { workspaceId: string }) => useWebSocket(workspaceId, "token"),
|
||||
{ initialProps: { workspaceId: "workspace-1" } }
|
||||
);
|
||||
|
||||
// Simulate connect error
|
||||
act(() => {
|
||||
eventHandlers.connect_error?.(new Error("Connection failed"));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.connectionError).not.toBeNull();
|
||||
});
|
||||
|
||||
// Rerender with new workspace to trigger reconnect
|
||||
rerender({ workspaceId: "workspace-2" });
|
||||
|
||||
// Connection error should be cleared when attempting new connection
|
||||
await waitFor(() => {
|
||||
expect(result.current.connectionError).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("should register connect_error handler on socket", (): void => {
|
||||
renderHook(() => useWebSocket("workspace-123", "token"));
|
||||
|
||||
expect(mockSocket.on).toHaveBeenCalledWith("connect_error", expect.any(Function));
|
||||
});
|
||||
|
||||
it("should clean up connect_error handler on unmount", (): void => {
|
||||
const { unmount } = renderHook(() => useWebSocket("workspace-123", "token"));
|
||||
|
||||
unmount();
|
||||
|
||||
expect(mockSocket.off).toHaveBeenCalledWith("connect_error", expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe("WSS enforcement", (): void => {
|
||||
afterEach((): void => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("should warn when using ws:// in production", (): void => {
|
||||
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
|
||||
vi.stubEnv("NODE_ENV", "production");
|
||||
vi.stubEnv("NEXT_PUBLIC_API_URL", "http://insecure-server.com");
|
||||
|
||||
renderHook(() => useWebSocket("workspace-123", "token"));
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("[Security Warning]"));
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("insecure protocol"));
|
||||
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should not warn when using https:// in production", (): void => {
|
||||
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
|
||||
vi.stubEnv("NODE_ENV", "production");
|
||||
vi.stubEnv("NEXT_PUBLIC_API_URL", "https://secure-server.com");
|
||||
|
||||
renderHook(() => useWebSocket("workspace-123", "token"));
|
||||
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should not warn when using wss:// in production", (): void => {
|
||||
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
|
||||
vi.stubEnv("NODE_ENV", "production");
|
||||
vi.stubEnv("NEXT_PUBLIC_API_URL", "wss://secure-server.com");
|
||||
|
||||
renderHook(() => useWebSocket("workspace-123", "token"));
|
||||
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should not warn in development mode even with http://", (): void => {
|
||||
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
|
||||
vi.stubEnv("NODE_ENV", "development");
|
||||
vi.stubEnv("NEXT_PUBLIC_API_URL", "http://localhost:3001");
|
||||
|
||||
renderHook(() => useWebSocket("workspace-123", "token"));
|
||||
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user