feat: complete remaining Go backend modules
This commit is contained in:
118
backend/internal/httpapi/method_compat_test.go
Normal file
118
backend/internal/httpapi/method_compat_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRouteMethodCompatibilityDerivesEverySurfacePath(t *testing.T) {
|
||||
patterns := routeMethodPatterns(GoRouteSurface())
|
||||
if len(patterns) != 47 {
|
||||
t.Fatalf("route patterns=%d want 47", 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user