package platform import ( "bufio" "context" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestProxyForwardsAPIWithBackendHost(t *testing.T) { var gotPath, gotHost, gotAuth string backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path gotHost = r.Host gotAuth = r.Header.Get("Authorization") w.WriteHeader(http.StatusOK) fmt.Fprint(w, "clip-body") })) defer backend.Close() proxy, err := NewAPIProxy(backend.URL) if err != nil { t.Fatalf("NewAPIProxy: %v", err) } req := httptest.NewRequest(http.MethodGet, "/api/clipboard", nil) req.Header.Set("Authorization", "Bearer tok-123") rec := httptest.NewRecorder() proxy.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status: got %d, want 200", rec.Code) } if gotPath != "/api/clipboard" { t.Errorf("backend path: got %q", gotPath) } // Host must be rewritten to the backend (TLS SNI + vhost routing), not the // WebView's synthetic origin. if gotHost != backendHost(backend.URL) { t.Errorf("backend Host header: got %q, want %q", gotHost, backendHost(backend.URL)) } if gotAuth != "Bearer tok-123" { t.Errorf("Authorization not forwarded: got %q", gotAuth) } if body := rec.Body.String(); body != "clip-body" { t.Errorf("body: got %q", body) } } func TestProxyNonAPIReturns404(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { t.Error("backend should not be hit for non-/api path") w.WriteHeader(http.StatusOK) })) defer backend.Close() proxy, err := NewAPIProxy(backend.URL) if err != nil { t.Fatalf("NewAPIProxy: %v", err) } rec := httptest.NewRecorder() proxy.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/settings", nil)) if rec.Code != http.StatusNotFound { t.Errorf("non-/api status: got %d, want 404", rec.Code) } } func TestProxyForwardsPUT(t *testing.T) { var gotMethod, gotBody string backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotMethod = r.Method buf := make([]byte, r.ContentLength) _, _ = r.Body.Read(buf) gotBody = string(buf) w.WriteHeader(http.StatusNoContent) })) defer backend.Close() proxy, _ := NewAPIProxy(backend.URL) req := httptest.NewRequest(http.MethodPut, "/api/clipboard", strings.NewReader("hello")) rec := httptest.NewRecorder() proxy.ServeHTTP(rec, req) if gotMethod != http.MethodPut { t.Errorf("method: got %q", gotMethod) } if gotBody != "hello" { t.Errorf("body: got %q", gotBody) } } // TestProxyStreamsSSE checks that the proxy relays an event-stream incrementally // rather than buffering until the backend closes the connection. The backend // holds the connection open after the first frame; the client must see that // frame before the deadline. func TestProxyStreamsSSE(t *testing.T) { release := make(chan struct{}) backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(http.StatusOK) fmt.Fprint(w, "event: clipboard:update\ndata: {}\n\n") w.(http.Flusher).Flush() <-release // keep the stream open like a real SSE channel })) defer backend.Close() defer close(release) proxy, _ := NewAPIProxy(backend.URL) srv := httptest.NewServer(proxy) defer srv.Close() ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() req, _ := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/api/hub/events", nil) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("GET sse: %v", err) } defer resp.Body.Close() done := make(chan string, 1) go func() { line, _ := bufio.NewReader(resp.Body).ReadString('\n') done <- line }() select { case line := <-done: if !strings.Contains(line, "clipboard:update") { t.Errorf("first streamed frame: got %q", line) } case <-time.After(1500 * time.Millisecond): t.Fatal("SSE frame did not arrive incrementally (proxy buffered the stream)") } } func backendHost(rawURL string) string { // httptest URLs are http://host:port — strip the scheme. return strings.TrimPrefix(rawURL, "http://") }