package handlers import ( "bufio" "fmt" "net" "net/http" "net/http/httptest" "strings" "testing" ) func TestServiceProxy_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 := NewServiceProxy(backend.URL, "", "/claudomator") 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 TestServiceProxy_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 := NewServiceProxy(backend.URL, "", "/claudomator") 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 TestServiceProxy_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 := NewServiceProxy(backend.URL, "", "/claudomator") req := httptest.NewRequest("GET", "/claudomator/api/tasks%2Ffoo", nil) rr := httptest.NewRecorder() proxy.ServeHTTP(rr, req) if strings.HasPrefix(seenRawPath, "/claudomator") { t.Errorf("upstream still sees /claudomator prefix in RawPath: %q", seenRawPath) } } func TestServiceProxy_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 := NewServiceProxy(backend.URL, "", "/claudomator") 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) } } func TestServiceProxy_DifferentPrefix(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 := NewServiceProxy(backend.URL, "", "/playground") req := httptest.NewRequest("GET", "/playground/status", nil) rr := httptest.NewRecorder() proxy.ServeHTTP(rr, req) if seenPath != "/status" { t.Errorf("expected upstream to see /status, got %q", seenPath) } } // 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 TestServiceProxy_WebSocket(t *testing.T) { 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() buf := make([]byte, 4096) n, err := conn.Read(buf) if err != nil || n == 0 { return } resp := "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n" _, _ = conn.Write([]byte(resp)) msgBuf := make([]byte, 64) n, _ = conn.Read(msgBuf) if n > 0 { _, _ = conn.Write(msgBuf[:n]) } }() targetURL := "http://" + listener.Addr().String() proxy := NewServiceProxy(targetURL, "", "/claudomator") 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) }() _, err = clientSide.Write([]byte("ping")) if err != nil { t.Fatal(err) } got := make([]byte, 64) n, _ := clientSide.Read(got) if n == 0 { t.Error("expected data back from WebSocket echo") } clientSide.Close() <-proxyDone <-done }