chore: initial commit of Go API gateway
CI / Test (push) Canceled after 0s
CI / Vet (push) Canceled after 0s
CI / Build (push) Canceled after 0s
CI / Security (govulncheck) (push) Canceled after 0s

Baseline for the Kong replacement on api.riotpiao.com. Brings the working
tree under version control for the first time: gateway source, the task
board that drives the agent runs, test fixtures, and K8s manifests.

Anchor the gateway ignore rule to the repo root. Unanchored, "gateway"
also matched the cmd/gateway/ source directory, so the program entrypoint
was excluded from every commit.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Story Crater Bot
2026-08-19 20:54:34 -07:00
co-authored by Claude Opus 5
commit 058f11cf2b
109 changed files with 8992 additions and 0 deletions
+333
View File
@@ -0,0 +1,333 @@
package proxy
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/Riotpiaole/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, _ := http.Post(server.URL+"/test", "text/plain", strings.NewReader(body))
resp.Body.Close()
// Verify rejection status
if resp.StatusCode != http.StatusRequestEntityTooLarge {
t.Errorf("expected 413, got %d", resp.StatusCode)
}
}