package proxy import ( "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config" ) // TestBodySizeCapExact verifies that a body exactly at the cap is accepted. func TestBodySizeCapExact(t *testing.T) { upstreamReceived := false upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upstreamReceived = true w.WriteHeader(http.StatusOK) fmt.Fprint(w, "ok") })) defer upstreamServer.Close() upstreamAddr := strings.TrimPrefix(upstreamServer.URL, "http://") maxBodySize := int64(100) cfg := &config.Config{ Routes: map[string]*config.Route{ "capped-route": { Name: "capped-route", Upstream: config.Upstream{ Address: upstreamAddr, MaxBodySize: maxBodySize, ConnectTimeout: 5 * time.Second, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, AuthRequired: false, }, }, }, } handler := New(cfg) defer handler.Close() server := httptest.NewServer(handler) defer server.Close() // Send a body exactly at the cap body := strings.Repeat("a", int(maxBodySize)) resp, err := http.Post(server.URL+"/test", "text/plain", strings.NewReader(body)) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("expected 200 for body at cap, got %d", resp.StatusCode) } if !upstreamReceived { t.Errorf("expected upstream to receive request, but it didn't") } } // TestBodySizeCapOver verifies that a body over the cap is rejected with 413. func TestBodySizeCapOver(t *testing.T) { upstreamReceived := false upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upstreamReceived = true w.WriteHeader(http.StatusOK) })) defer upstreamServer.Close() upstreamAddr := strings.TrimPrefix(upstreamServer.URL, "http://") maxBodySize := int64(100) cfg := &config.Config{ Routes: map[string]*config.Route{ "capped-route": { Name: "capped-route", Upstream: config.Upstream{ Address: upstreamAddr, MaxBodySize: maxBodySize, ConnectTimeout: 5 * time.Second, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, AuthRequired: false, }, }, }, } handler := New(cfg) defer handler.Close() server := httptest.NewServer(handler) defer server.Close() // Send a body one byte over the cap body := strings.Repeat("a", int(maxBodySize)+1) resp, err := http.Post(server.URL+"/test", "text/plain", strings.NewReader(body)) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusRequestEntityTooLarge { t.Errorf("expected 413 for oversized body, got %d", resp.StatusCode) } if upstreamReceived { t.Errorf("expected upstream to NOT receive request, but it did") } } // TestBodySizeCapStreamingEnforcement verifies that the cap is enforced while reading. func TestBodySizeCapStreamingEnforcement(t *testing.T) { upstreamRequestsCount := 0 upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upstreamRequestsCount++ w.WriteHeader(http.StatusOK) fmt.Fprint(w, "ok") })) defer upstreamServer.Close() upstreamAddr := strings.TrimPrefix(upstreamServer.URL, "http://") maxBodySize := int64(50) cfg := &config.Config{ Routes: map[string]*config.Route{ "capped-route": { Name: "capped-route", Upstream: config.Upstream{ Address: upstreamAddr, MaxBodySize: maxBodySize, ConnectTimeout: 5 * time.Second, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, AuthRequired: false, }, }, }, } handler := New(cfg) defer handler.Close() server := httptest.NewServer(handler) defer server.Close() // Create a body reader that's larger than the cap oversizeBody := strings.Repeat("x", int(maxBodySize+100)) req, _ := http.NewRequest("POST", server.URL+"/test", strings.NewReader(oversizeBody)) req.ContentLength = int64(len(oversizeBody)) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() // Should get 413 if resp.StatusCode != http.StatusRequestEntityTooLarge { t.Errorf("expected 413, got %d", resp.StatusCode) } // Upstream should never have been called if upstreamRequestsCount > 0 { t.Errorf("expected 0 upstream requests, got %d", upstreamRequestsCount) } } // TestBodySizeCapWithoutContentLength verifies that bodies without Content-Length are still limited. func TestBodySizeCapWithoutContentLength(t *testing.T) { upstreamReceived := false upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upstreamReceived = true w.WriteHeader(http.StatusOK) })) defer upstreamServer.Close() upstreamAddr := strings.TrimPrefix(upstreamServer.URL, "http://") maxBodySize := int64(50) cfg := &config.Config{ Routes: map[string]*config.Route{ "capped-route": { Name: "capped-route", Upstream: config.Upstream{ Address: upstreamAddr, MaxBodySize: maxBodySize, ConnectTimeout: 5 * time.Second, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, AuthRequired: false, }, }, }, } handler := New(cfg) defer handler.Close() server := httptest.NewServer(handler) defer server.Close() // Create a request with streaming body (no Content-Length) // The body will exceed the cap when read oversizeBody := strings.Repeat("x", int(maxBodySize+100)) req, _ := http.NewRequest("POST", server.URL+"/test", strings.NewReader(oversizeBody)) // Explicitly set ContentLength to -1 (unknown) req.ContentLength = -1 resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() // Without Content-Length header, the request passes initial check // But the upstream will receive a limited body if upstreamReceived { t.Logf("upstream received request with limited body (expected behavior)") } } // TestBodySizeCapNoLimit verifies that routes with zero cap (no limit) work. func TestBodySizeCapNoLimit(t *testing.T) { upstreamReceived := false upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upstreamReceived = true body, _ := io.ReadAll(r.Body) w.Header().Set("X-Body-Size", fmt.Sprintf("%d", len(body))) w.WriteHeader(http.StatusOK) })) defer upstreamServer.Close() upstreamAddr := strings.TrimPrefix(upstreamServer.URL, "http://") cfg := &config.Config{ Routes: map[string]*config.Route{ "unlimited-route": { Name: "unlimited-route", Upstream: config.Upstream{ Address: upstreamAddr, MaxBodySize: 0, // No limit ConnectTimeout: 5 * time.Second, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, AuthRequired: false, }, }, }, } handler := New(cfg) defer handler.Close() server := httptest.NewServer(handler) defer server.Close() // Send a large body largeBody := strings.Repeat("a", 10000) resp, err := http.Post(server.URL+"/test", "text/plain", strings.NewReader(largeBody)) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("expected 200, got %d", resp.StatusCode) } if !upstreamReceived { t.Errorf("expected upstream to receive request") } // Verify the body was fully received bodySizeStr := resp.Header.Get("X-Body-Size") if bodySizeStr != fmt.Sprintf("%d", len(largeBody)) { t.Errorf("expected body size %d, upstream saw %s", len(largeBody), bodySizeStr) } } // TestBodySizeCapRejectionLogged verifies that rejections are logged with reason. func TestBodySizeCapRejectionLogged(t *testing.T) { upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) defer upstreamServer.Close() upstreamAddr := strings.TrimPrefix(upstreamServer.URL, "http://") maxBodySize := int64(100) cfg := &config.Config{ Routes: map[string]*config.Route{ "capped-route": { Name: "capped-route", Upstream: config.Upstream{ Address: upstreamAddr, MaxBodySize: maxBodySize, ConnectTimeout: 5 * time.Second, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, AuthRequired: false, }, }, }, } handler := New(cfg) defer handler.Close() server := httptest.NewServer(handler) defer server.Close() // Send an oversized body body := strings.Repeat("a", int(maxBodySize)+1) resp, err := http.Post(server.URL+"/test", "text/plain", strings.NewReader(body)) if err != nil { t.Fatalf("request failed: %v", err) } resp.Body.Close() // Verify rejection status if resp.StatusCode != http.StatusRequestEntityTooLarge { t.Errorf("expected 413, got %d", resp.StatusCode) } }