Files
homelab-frontend/internal/proxy/bodysize_test.go
T

337 lines
8.9 KiB
Go
Raw Normal View History

2026-08-19 20:52:13 -07:00
package proxy
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
2026-08-19 20:52:13 -07:00
)
// 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)
}
2026-08-19 20:52:13 -07:00
resp.Body.Close()
// Verify rejection status
if resp.StatusCode != http.StatusRequestEntityTooLarge {
t.Errorf("expected 413, got %d", resp.StatusCode)
}
}