Files
NianAIGC/backend/internal/httpapi/method_compat_test.go

119 lines
4.1 KiB
Go

package httpapi
import (
"io"
"net/http"
"net/http/httptest"
"reflect"
"sort"
"strings"
"testing"
)
func TestRouteMethodCompatibilityDerivesEverySurfacePath(t *testing.T) {
patterns := routeMethodPatterns(GoRouteSurface())
if len(patterns) != 48 {
t.Fatalf("route patterns=%d want 48", len(patterns))
}
for _, pattern := range patterns {
if _, ok := pattern.methods[http.MethodOptions]; !ok {
t.Fatalf("%s omits OPTIONS", pattern.path)
}
if _, hasGet := pattern.methods[http.MethodGet]; hasGet {
if _, hasHead := pattern.methods[http.MethodHead]; !hasHead {
t.Fatalf("%s omits derived HEAD", pattern.path)
}
}
if !sort.StringsAreSorted(pattern.allow) {
t.Fatalf("%s Allow is not sorted: %v", pattern.path, pattern.allow)
}
}
}
func TestRouteMethodCompatibilityHandlesOptionsBeforeApplicationAuth(t *testing.T) {
calls := 0
next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { calls++ })
handler := WithRouteMethodCompatibility(next)
for _, test := range []struct {
path string
allow string
}{
{path: "/api/health", allow: "GET, HEAD, OPTIONS"},
{path: "/api/admin/accounts", allow: "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT"},
{path: "/api/v1/jobs", allow: "GET, HEAD, OPTIONS, POST"},
{path: "/api/v1/jobs/job-1/cancel", allow: "OPTIONS, POST"},
{path: "/uploads/2026/08/file.png", allow: "GET, HEAD, OPTIONS"},
} {
t.Run(test.path, func(t *testing.T) {
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodOptions, test.path, nil))
if response.Code != http.StatusNoContent || response.Body.Len() != 0 {
t.Fatalf("status=%d body=%q", response.Code, response.Body.String())
}
if got := response.Header().Get("Allow"); got != test.allow {
t.Fatalf("Allow=%q want %q", got, test.allow)
}
})
}
if calls != 0 {
t.Fatalf("OPTIONS reached application %d times", calls)
}
}
func TestRouteMethodCompatibilityDerivesHeadAndSuppressesBody(t *testing.T) {
var methods []string
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
methods = append(methods, r.Method)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_, _ = io.WriteString(w, `{"secret":"must-not-be-written"}`)
})
handler := WithRouteMethodCompatibility(next)
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodHead, "/api/v1/assets/asset-1/download", nil))
if response.Code != http.StatusCreated || response.Body.Len() != 0 {
t.Fatalf("status=%d body=%q", response.Code, response.Body.String())
}
if response.Header().Get("Content-Type") != "application/json" {
t.Fatalf("content-type=%q", response.Header().Get("Content-Type"))
}
if !reflect.DeepEqual(methods, []string{http.MethodGet}) {
t.Fatalf("downstream methods=%v", methods)
}
}
func TestRouteMethodCompatibilityReturnsFrameworkStyle405AndPassesUnknownPaths(t *testing.T) {
calls := 0
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
http.NotFound(w, r)
})
handler := WithRouteMethodCompatibility(next)
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodDelete, "/api/health", strings.NewReader("ignored")))
if response.Code != http.StatusMethodNotAllowed || response.Body.Len() != 0 {
t.Fatalf("status=%d body=%q", response.Code, response.Body.String())
}
if got := response.Header().Get("Allow"); got != "" {
t.Fatalf("unsupported method unexpectedly exposes Allow=%q", got)
}
if calls != 0 {
t.Fatalf("unsupported method reached application")
}
response = httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodOptions, "/not-a-route", nil))
if response.Code != http.StatusNotFound || calls != 1 {
t.Fatalf("unknown status=%d calls=%d", response.Code, calls)
}
response = httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodOptions, "/uploads/", nil))
if response.Code != http.StatusNotFound || calls != 2 {
t.Fatalf("empty catch-all status=%d calls=%d", response.Code, calls)
}
}