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) } }