package handlers import ( "bufio" "fmt" "net" "net/http" "net/http/httptest" "strings" "testing" ) func TestClaudomatorProxy_HTTP_Forward(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "hello") })) defer backend.Close() proxy := NewClaudomatorProxy(backend.URL) req := httptest.NewRequest("GET", "/claudomator/", nil) rr := httptest.NewRecorder() proxy.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) } if body := rr.Body.String(); body != "hello" { t.Errorf("expected 'hello', got %q", body) } } func TestClaudomatorProxy_StripPrefix(t *testing.T) { var seenPath string backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { seenPath = r.URL.Path w.WriteHeader(http.StatusOK) })) defer backend.Close() proxy := NewClaudomatorProxy(backend.URL) req := httptest.NewRequest("GET", "/claudomator/api/tasks", nil) rr := httptest.NewRecorder() proxy.ServeHTTP(rr, req) if seenPath != "/api/tasks" { t.Errorf("expected upstream to see /api/tasks, got %q", seenPath) } } func TestClaudomatorProxy_RawPathStrip(t *testing.T) { var seenRawPath string backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { seenRawPath = r.URL.RawPath w.WriteHeader(http.StatusOK) })) defer backend.Close() proxy := NewClaudomatorProxy(backend.URL) req := httptest.NewRequest("GET", "/claudomator/api/tasks%2Ffoo", nil) rr := httptest.NewRecorder() proxy.ServeHTTP(rr, req) // RawPath should have /claudomator stripped if strings.HasPrefix(seenRawPath, "/claudomator") { t.Errorf("upstream still sees /claudomator prefix in RawPath: %q", seenRawPath) } } func TestClaudomatorProxy_ServiceWorkerHeader(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Service-Worker-Allowed", "/") w.WriteHeader(http.StatusOK) })) defer backend.Close() proxy := NewClaudomatorProxy(backend.URL) req := httptest.NewRequest("GET", "/claudomator/sw.js", nil) rr := httptest.NewRecorder() proxy.ServeHTTP(rr, req) if got := rr.Header().Get("Service-Worker-Allowed"); got != "/" { t.Errorf("expected Service-Worker-Allowed '/', got %q", got) } } // responseHijacker wraps httptest.ResponseRecorder and implements http.Hijacker // so WebSocket hijack tests work without a real TCP connection. type responseHijacker struct { *httptest.ResponseRecorder serverConn net.Conn } func (h *responseHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) { rw := bufio.NewReadWriter(bufio.NewReader(h.serverConn), bufio.NewWriter(h.serverConn)) return h.serverConn, rw, nil } func TestClaudomatorProxy_WebSocket(t *testing.T) { // Stand up a minimal WebSocket echo backend that speaks raw HTTP upgrade listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer listener.Close() done := make(chan struct{}) go func() { defer close(done) conn, err := listener.Accept() if err != nil { return } defer conn.Close() // Read the HTTP upgrade request from the proxy buf := make([]byte, 4096) n, err := conn.Read(buf) if err != nil || n == 0 { return } // Send a minimal HTTP 101 response so the proxy considers this a WS resp := "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n" _, _ = conn.Write([]byte(resp)) // Echo one message: read "ping", write "pong" msgBuf := make([]byte, 64) n, _ = conn.Read(msgBuf) if n > 0 { _, _ = conn.Write(msgBuf[:n]) } }() targetURL := "http://" + listener.Addr().String() proxy := NewClaudomatorProxy(targetURL) // Build a client-side TCP pipe to simulate the browser side clientSide, serverSide := net.Pipe() defer clientSide.Close() rr := httptest.NewRecorder() rh := &responseHijacker{ResponseRecorder: rr, serverConn: serverSide} req := httptest.NewRequest("GET", "/claudomator/ws", nil) req.Header.Set("Upgrade", "websocket") req.Header.Set("Connection", "Upgrade") proxyDone := make(chan struct{}) go func() { defer close(proxyDone) proxy.ServeHTTP(rh, req) }() // Write a message from the "browser" side and read the echo _, err = clientSide.Write([]byte("ping")) if err != nil { t.Fatal(err) } // Read back — we expect "pong" from the echo server (or at least some data) got := make([]byte, 64) n, _ := clientSide.Read(got) if n == 0 { t.Error("expected data back from WebSocket echo") } clientSide.Close() <-proxyDone <-done }