119 lines
4.1 KiB
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) != 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)
|
|
}
|
|
}
|