package proxy import ( "context" "net/http" "net/http/httptest" "net/url" "testing" ) // captureTransport records the outgoing request and returns 200 without dialing. 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 TestForwardDoubleSlashPathMatchesStripPrefix(t *testing.T) { target, err := url.Parse("http://127.0.0.1:9") if err != nil { t.Fatal(err) } cap := &captureTransport{} h := NewAliasForward(target, "/api/mtg", "/v1", "", cap, nil) 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" h.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 TestForwardRewritesPath(t *testing.T) { target, err := url.Parse("http://127.0.0.1:9") if err != nil { t.Fatal(err) } cap := &captureTransport{} h := NewAliasForward(target, "/api/main_srv", "/v1", "", cap, nil) 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) } h.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 TestForwardRewritesPathWithAPIBasePath(t *testing.T) { target, err := url.Parse("http://127.0.0.1:9/api/") if err != nil { t.Fatal(err) } cap := &captureTransport{} h := NewAliasForward(target, "/api/main_srv", "/v1", "", cap, nil) 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) } h.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 TestForwardGETUsersListUsesStatsUsers(t *testing.T) { target, err := url.Parse("http://127.0.0.1:9") if err != nil { t.Fatal(err) } cap := &captureTransport{} h := NewAliasForward(target, "/api/mtg", "/v1", "", cap, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://127.0.0.1:9/api/mtg/users", nil) if err != nil { t.Fatal(err) } h.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 TestForwardPOSTUsersCreateNotRewritten(t *testing.T) { target, err := url.Parse("http://127.0.0.1:9") if err != nil { t.Fatal(err) } cap := &captureTransport{} h := NewAliasForward(target, "/api/mtg", "/v1", "", cap, nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "http://127.0.0.1:9/api/mtg/users", nil) if err != nil { t.Fatal(err) } h.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 TestForwardGETUsersByNameNotRewritten(t *testing.T) { target, err := url.Parse("http://127.0.0.1:9") if err != nil { t.Fatal(err) } cap := &captureTransport{} h := NewAliasForward(target, "/api/mtg", "/v1", "", cap, nil) 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) } h.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 TestForwardNestedStatsUsers(t *testing.T) { target, err := url.Parse("http://127.0.0.1:9/api/") if err != nil { t.Fatal(err) } cap := &captureTransport{} h := NewAliasForward(target, "/api/gt2", "/v1", "", cap, nil) 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) } h.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) } func TestForwardOutgoingHostIsUpstreamAuthority(t *testing.T) { target, err := url.Parse("http://172.20.0.3:9091") if err != nil { t.Fatal(err) } cap := &captureTransport{} h := NewAliasForward(target, "/api/mtg", "/v1", "", cap, nil) req := httptest.NewRequest(http.MethodGet, "http://public.example/api/mtg/health", nil) req.Host = "public.example:8888" req.Header.Set("Host", "public.example:8888") h.ServeHTTP(httptest.NewRecorder(), req) if cap.got == nil { t.Fatal("no outgoing request captured") } if cap.got.URL.Host != "172.20.0.3:9091" { t.Fatalf("URL.Host=%q", cap.got.URL.Host) } if cap.got.Host != "172.20.0.3:9091" { t.Fatalf("Request.Host=%q want 172.20.0.3:9091", cap.got.Host) } if h := cap.got.Header.Get("Host"); h != "" { t.Fatalf("Header Host should be empty (use Request.Host); got %q", h) } } func TestJoinPathPrefixRoundTrip(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 := JoinPathPrefix(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) } 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()) }