package embeddednats_test import ( "fmt" "net" "net/http" "strings" "testing" "time" "github.com/enmanuel/unibus/pkg/embeddednats" ) // wsFreePort returns an OS-assigned free TCP port on loopback. Kept local to this // file so the WebSocket tests do not depend on the cluster test helpers. func wsFreePort(t *testing.T) int { t.Helper() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("reserve free port: %v", err) } defer l.Close() return l.Addr().(*net.TCPAddr).Port } // TestWebsocketListenerOpens verifies that when a ServerConfig carries a // WebsocketConfig the embedded nats-server opens the additional WebSocket port and // accepts a connection there, while the regular TCP client port keeps working. A // browser cannot speak raw TCP, so this WebSocket listener is the only path the SPA // has to the data plane (issue uniweb/0001). func TestWebsocketListenerOpens(t *testing.T) { clientPort := wsFreePort(t) wsPort := wsFreePort(t) ns, err := embeddednats.StartServer(embeddednats.ServerConfig{ StoreDir: t.TempDir(), Host: "127.0.0.1", Port: clientPort, Websocket: &embeddednats.WebsocketConfig{ Host: "127.0.0.1", Port: wsPort, NoTLS: true, // loopback dev: plain ws:// }, }) if err != nil { t.Fatalf("StartServer with websocket: %v", err) } t.Cleanup(func() { ns.Shutdown(); ns.WaitForShutdown() }) // The WebSocket listener must accept a TCP connection on its dedicated port. addr := fmt.Sprintf("127.0.0.1:%d", wsPort) conn, err := net.DialTimeout("tcp", addr, 2*time.Second) if err != nil { t.Fatalf("websocket port %d not accepting connections: %v", wsPort, err) } conn.Close() // And it must speak the WebSocket upgrade handshake: a GET with the upgrade // headers should get a 101 Switching Protocols (nats-server's ws endpoint), // proving it is a real WebSocket listener, not just an open socket. req, err := http.NewRequest(http.MethodGet, "http://"+addr+"/", nil) if err != nil { t.Fatalf("build upgrade request: %v", err) } req.Header.Set("Upgrade", "websocket") req.Header.Set("Connection", "Upgrade") req.Header.Set("Sec-WebSocket-Version", "13") req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==") client := &http.Client{Timeout: 2 * time.Second} resp, err := client.Do(req) if err != nil { t.Fatalf("websocket upgrade request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusSwitchingProtocols { t.Fatalf("websocket upgrade: got status %d, want 101 Switching Protocols", resp.StatusCode) } } // TestNoWebsocketByDefault verifies the listener stays TCP-only when WebsocketConfig // is nil: opening the browser transport must be an explicit opt-in so existing // single-node and cluster deployments are unchanged. func TestNoWebsocketByDefault(t *testing.T) { clientPort := wsFreePort(t) // Reserve a port, then free it, so we can assert nothing is listening there. maybeWSPort := wsFreePort(t) ns, err := embeddednats.StartServer(embeddednats.ServerConfig{ StoreDir: t.TempDir(), Host: "127.0.0.1", Port: clientPort, // Websocket intentionally nil. }) if err != nil { t.Fatalf("StartServer: %v", err) } t.Cleanup(func() { ns.Shutdown(); ns.WaitForShutdown() }) conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", maybeWSPort), 300*time.Millisecond) if err == nil { conn.Close() t.Fatalf("a listener is unexpectedly open on %d with no WebsocketConfig", maybeWSPort) } if !strings.Contains(err.Error(), "refused") && !strings.Contains(err.Error(), "timeout") { t.Logf("dial error (acceptable, port closed): %v", err) } }