- Introduced a new function to normalize request URL paths, collapsing duplicate slashes and clearing raw paths to ensure correct routing in the reverse proxy. - Updated the gateway to utilize the normalization function for API requests, improving routing consistency. - Trimmed whitespace from server configuration fields in the validation process to prevent potential issues with malformed URLs. - Enhanced documentation in GATEWAY_RUN.md to clarify the importance of proper URL formatting and configuration.
249 lines
6.5 KiB
Go
249 lines
6.5 KiB
Go
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())
|
|
}
|