package api import ( "context" "io" "net/http" "net/http/httptest" "net/http/httputil" "net/url" "strings" "sync/atomic" "testing" "time" ) func officeTestProxy(t *testing.T, upstream string, officeTimeout time.Duration) *httputil.ReverseProxy { t.Helper() target, err := url.Parse(upstream) if err != nil { t.Fatal(err) } normal := runtimeTransport.Clone() office := runtimeOfficeTransport.Clone() normal.ResponseHeaderTimeout = 30 * time.Millisecond office.ResponseHeaderTimeout = officeTimeout t.Cleanup(normal.CloseIdleConnections) t.Cleanup(office.CloseIdleConnections) proxy := runtimeReverseProxy(target, "/dsh/office-test", "panel.test", "runtime=secret") proxy.Transport = runtimeProxyTransport{normal: normal, office: office} return proxy } func TestRuntimeOfficeTransportDefaults(t *testing.T) { if got := runtimeTransport.ResponseHeaderTimeout; got != 20*time.Second { t.Errorf("normal timeout = %s, want 20s", got) } if got := runtimeOfficeTransport.ResponseHeaderTimeout; got != 120*time.Second { t.Errorf("office timeout = %s, want 120s", got) } if runtimeTransport == runtimeOfficeTransport { t.Fatal("office transport must be separate") } target, _ := url.Parse("http://panel.test") proxy := runtimeReverseProxy(target, "/dsh/office-test", "panel.test", "") transport, ok := proxy.Transport.(runtimeProxyTransport) if !ok || transport.normal != runtimeTransport || transport.office != runtimeOfficeTransport { t.Fatalf("unexpected default transport: %#v", proxy.Transport) } } func TestRuntimeOfficeProxyRouting(t *testing.T) { for _, path := range []string{ "/api/officeToPdf/render", "/api/status", "/api/officeToPdf/render/", "/api/officeToPdf/render/child", "/api/officeToPdf/renderOther", "/api/officetopdf/render", "/api/officeToPdf/status", } { t.Run(path, func(t *testing.T) { const body = "document payload" const query = "file=report%20one.docx&mode=pdf&mode=preview" var calls atomic.Int32 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) data, err := io.ReadAll(r.Body) if err != nil || string(data) != body || r.Method != http.MethodPost { t.Errorf("upstream method/body = %s %q, error %v", r.Method, data, err) } if r.URL.Path != path || r.URL.RawQuery != query { t.Errorf("upstream URL = %s", r.URL) } if r.Host != "panel.test" || r.Header.Get("X-Forwarded-Prefix") != "/dsh/office-test" || r.Header.Get("Cookie") != "runtime=secret" { t.Errorf("upstream host/headers = %s %v", r.Host, r.Header) } time.Sleep(100 * time.Millisecond) w.Header().Set("Content-Type", "application/pdf") _, _ = io.WriteString(w, "rendered PDF") })) defer upstream.Close() proxy := officeTestProxy(t, upstream.URL, 500*time.Millisecond) req := httptest.NewRequest(http.MethodPost, "/dsh/office-test"+path+"?"+query+"&token=removed", strings.NewReader(body)) req.Header.Set("Cookie", "panel=private") originalURL := req.URL.String() w := httptest.NewRecorder() proxy.ServeHTTP(w, req) want := http.StatusBadGateway if path == "/api/officeToPdf/render" { want = http.StatusOK if w.Body.String() != "rendered PDF" { t.Errorf("response body = %q", w.Body.String()) } } if w.Code != want { t.Errorf("status = %d, want %d: %s", w.Code, want, w.Body.String()) } if req.URL.String() != originalURL || req.Header.Get("Cookie") != "panel=private" { t.Error("proxy mutated incoming URL or cookies") } upstream.Close() if got := calls.Load(); got != 1 { t.Errorf("mutating request sent %d times, want exactly once", got) } }) } } func TestRuntimeOfficeProxyFiniteTimeoutAndCancellation(t *testing.T) { for _, name := range []string{"header timeout", "caller cancellation"} { t.Run(name, func(t *testing.T) { started := make(chan struct{}, 2) canceled := make(chan struct{}, 2) var calls atomic.Int32 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) _, _ = io.Copy(io.Discard, r.Body) started <- struct{}{} select { case <-r.Context().Done(): canceled <- struct{}{} case <-time.After(2 * time.Second): } })) defer upstream.Close() officeTimeout := 100 * time.Millisecond if name == "caller cancellation" { officeTimeout = 5 * time.Second } proxy := officeTestProxy(t, upstream.URL, officeTimeout) ctx, cancel := context.WithCancel(context.Background()) defer cancel() req := httptest.NewRequest(http.MethodPost, "/dsh/office-test/api/officeToPdf/render?mode=pdf", strings.NewReader("document")).WithContext(ctx) w := httptest.NewRecorder() done := make(chan struct{}) go func() { proxy.ServeHTTP(w, req) close(done) }() select { case <-started: case <-time.After(time.Second): t.Fatal("request never reached upstream") } if name == "caller cancellation" { cancel() } select { case <-done: case <-time.After(500 * time.Millisecond): t.Fatal("proxy did not promptly finish") } if w.Code != http.StatusBadGateway { t.Errorf("status = %d, want 502", w.Code) } select { case <-canceled: case <-time.After(500 * time.Millisecond): t.Fatal("upstream context was not canceled") } upstream.Close() if got := calls.Load(); got != 1 { t.Errorf("mutating request sent %d times, want exactly once", got) } }) } }