package proxy import ( "context" "net/http" "net/http/httptest" "net/url" "testing" ) // captureTransport records the outgoing request and returns 200 without dialing. // httptest.ResponseRecorder + ReverseProxy + real httptest.Server can yield flaky // or environment-dependent failures (proxy env, request-line quirks); we assert // Director output directly instead. type captureTransport struct { got *http.Request } func (c *captureTransport) RoundTrip(req *http.Request) (*http.Response, error) { c.got = req.Clone(req.Context()) return &http.Response{ StatusCode: http.StatusOK, Body: http.NoBody, Header: make(http.Header), }, nil } func TestDirectorDoubleSlashPathMatchesStripPrefix(t *testing.T) { target, err := url.Parse("http://127.0.0.1:9") if err != nil { t.Fatal(err) } cap := &captureTransport{} rp := NewReverseProxy(target, "/api/mtg", "/v1", "") rp.Transport = cap req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://127.0.0.1:9/", nil) if err != nil { t.Fatal(err) } req.URL.Path = "/api//mtg/health" rp.ServeHTTP(httptest.NewRecorder(), req) if cap.got == nil { t.Fatal("no outgoing request captured") } want, err := url.Parse("http://127.0.0.1:9/v1/health") if err != nil { t.Fatal(err) } assertSameURL(t, cap.got.URL, want) } func TestDirectorRewritesPath(t *testing.T) { target, err := url.Parse("http://127.0.0.1:9") if err != nil { t.Fatal(err) } cap := &captureTransport{} rp := NewReverseProxy(target, "/api/main_srv", "/v1", "") rp.Transport = cap req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://127.0.0.1:9/api/main_srv/health", nil) if err != nil { t.Fatal(err) } rp.ServeHTTP(httptest.NewRecorder(), req) if cap.got == nil { t.Fatal("no outgoing request captured") } want, err := url.Parse("http://127.0.0.1:9/v1/health") if err != nil { t.Fatal(err) } assertSameURL(t, cap.got.URL, want) } func TestDirectorRewritesPathWithAPIBasePath(t *testing.T) { target, err := url.Parse("http://127.0.0.1:9/api/") if err != nil { t.Fatal(err) } cap := &captureTransport{} rp := NewReverseProxy(target, "/api/main_srv", "/v1", "") rp.Transport = cap req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://127.0.0.1:9/api/main_srv/health", nil) if err != nil { t.Fatal(err) } rp.ServeHTTP(httptest.NewRecorder(), req) if cap.got == nil { t.Fatal("no outgoing request captured") } want, err := url.Parse("http://127.0.0.1:9/api/v1/health") if err != nil { t.Fatal(err) } assertSameURL(t, cap.got.URL, want) } func TestDirectorGETUsersListUsesStatsUsers(t *testing.T) { target, err := url.Parse("http://127.0.0.1:9") if err != nil { t.Fatal(err) } cap := &captureTransport{} rp := NewReverseProxy(target, "/api/mtg", "/v1", "") rp.Transport = cap req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://127.0.0.1:9/api/mtg/users", nil) if err != nil { t.Fatal(err) } rp.ServeHTTP(httptest.NewRecorder(), req) if cap.got == nil { t.Fatal("no outgoing request captured") } want, err := url.Parse("http://127.0.0.1:9/v1/stats/users") if err != nil { t.Fatal(err) } assertSameURL(t, cap.got.URL, want) } func TestDirectorPOSTUsersCreateNotRewritten(t *testing.T) { target, err := url.Parse("http://127.0.0.1:9") if err != nil { t.Fatal(err) } cap := &captureTransport{} rp := NewReverseProxy(target, "/api/mtg", "/v1", "") rp.Transport = cap req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "http://127.0.0.1:9/api/mtg/users", nil) if err != nil { t.Fatal(err) } rp.ServeHTTP(httptest.NewRecorder(), req) if cap.got == nil { t.Fatal("no outgoing request captured") } want, err := url.Parse("http://127.0.0.1:9/v1/users") if err != nil { t.Fatal(err) } assertSameURL(t, cap.got.URL, want) } func TestDirectorGETUsersByNameNotRewritten(t *testing.T) { target, err := url.Parse("http://127.0.0.1:9") if err != nil { t.Fatal(err) } cap := &captureTransport{} rp := NewReverseProxy(target, "/api/mtg", "/v1", "") rp.Transport = cap req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://127.0.0.1:9/api/mtg/users/alice", nil) if err != nil { t.Fatal(err) } rp.ServeHTTP(httptest.NewRecorder(), req) if cap.got == nil { t.Fatal("no outgoing request captured") } want, err := url.Parse("http://127.0.0.1:9/v1/users/alice") if err != nil { t.Fatal(err) } assertSameURL(t, cap.got.URL, want) } func TestDirectorRewritesNestedStatsUsers(t *testing.T) { target, err := url.Parse("http://127.0.0.1:9/api/") if err != nil { t.Fatal(err) } cap := &captureTransport{} rp := NewReverseProxy(target, "/api/gt2", "/v1", "") rp.Transport = cap req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://127.0.0.1:9/api/gt2/stats/users", nil) if err != nil { t.Fatal(err) } rp.ServeHTTP(httptest.NewRecorder(), req) if cap.got == nil { t.Fatal("no outgoing request captured") } want, err := url.Parse("http://127.0.0.1:9/api/v1/stats/users") if err != nil { t.Fatal(err) } assertSameURL(t, cap.got.URL, want) } // TestBuildUpstreamURLRoundTrip checks that a URL built like the Director does is // accepted by net/http against httptest (no ReverseProxy). Isolates JoinPath + server. func TestBuildUpstreamURLRoundTrip(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/health" { http.Error(w, "bad path", http.StatusBadRequest) return } w.WriteHeader(http.StatusOK) })) defer srv.Close() base, err := url.Parse(srv.URL) if err != nil { t.Fatal(err) } joined := buildUpstreamURL(base, "/v1", "health") req, err := http.NewRequest(http.MethodGet, joined.String(), nil) if err != nil { t.Fatal(err) } resp, err := DirectTransport().RoundTrip(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status %d", resp.StatusCode) } } func assertSameURL(t *testing.T, got, want *url.URL) { t.Helper() if got == nil || want == nil { t.Fatalf("nil URL: got=%v want=%v", got, want) } // JoinPath vs url.Parse can differ in Path vs RawPath while String() is identical. if got.String() == want.String() { return } if got.Scheme == want.Scheme && got.Host == want.Host && got.RawQuery == want.RawQuery && got.EscapedPath() == want.EscapedPath() { return } t.Fatalf("URL mismatch\ngot %q\nwant %q", got.String(), want.String()) }