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:
Jason Woltje
2026-02-05 17:31:26 -06:00
parent 63a622cbef
commit dd46025d60
4 changed files with 194 additions and 2 deletions

View File

@@ -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();
});
});
});