summaryrefslogtreecommitdiff
path: root/internal/handlers/claudomator_proxy_test.go
diff options
context:
space:
mode:
authorDoot Agent <agent@doot.terst.org>2026-03-25 04:03:13 +0000
committerDoot Agent <agent@doot.terst.org>2026-03-25 04:03:13 +0000
commit2db5020047640361066510f29f908ca9fd1c99aa (patch)
treed68b87204621ec8ab7bd7a7366a80357cd443366 /internal/handlers/claudomator_proxy_test.go
parent23c670442392af1c75b935b3296ae2fc4fd094ba (diff)
feat: gate Claudomator UI behind Doot session auth via reverse proxy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/handlers/claudomator_proxy_test.go')
-rw-r--r--internal/handlers/claudomator_proxy_test.go175
1 files changed, 175 insertions, 0 deletions
diff --git a/internal/handlers/claudomator_proxy_test.go b/internal/handlers/claudomator_proxy_test.go
new file mode 100644
index 0000000..bb2842a
--- /dev/null
+++ b/internal/handlers/claudomator_proxy_test.go
@@ -0,0 +1,175 @@
+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
+}