package admin import ( "context" "errors" "io" "net/http" "net/http/httptest" "strings" "testing" ) type checkedBody struct { t *testing.T onOwner *bool io.Reader } func (b checkedBody) Read(p []byte) (int, error) { if *b.onOwner { b.t.Error("network request read on owner") } return b.Reader.Read(p) } func (b checkedBody) Close() error { return nil } type checkedWriter struct { t *testing.T onOwner *bool *httptest.ResponseRecorder } func (w checkedWriter) Write(p []byte) (int, error) { if *w.onOwner { w.t.Error("network response write on owner") } return w.ResponseRecorder.Write(p) } func (w checkedWriter) WriteHeader(status int) { if *w.onOwner { w.t.Error("network response headers written on owner") } w.ResponseRecorder.WriteHeader(status) } func TestOwnerMiddlewareBuffersNetworkIO(t *testing.T) { for _, method := range []string{"GET", "POST", "PUT", "DELETE", "PATCH"} { t.Run(method, func(t *testing.T) { onOwner, tasks := false, 0 s := &AdminServer{runOnOwner: func(fn func()) bool { tasks++ onOwner = true fn() onOwner = false return true }} handler := s.ownerMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !onOwner { t.Error("handler outside owner task") } body, err := io.ReadAll(r.Body) if err != nil || string(body) != "request" { t.Errorf("body = %q, %v", body, err) } w.Header().Set("X-Test", "buffered") w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusBadRequest) // first status wins _, _ = w.Write([]byte("response")) })) r := httptest.NewRequest(method, "/api/test", nil) r.Body = checkedBody{t, &onOwner, strings.NewReader("request")} rec := httptest.NewRecorder() handler.ServeHTTP(checkedWriter{t, &onOwner, rec}, r) if tasks != 1 || rec.Code != http.StatusCreated || rec.Body.String() != "response" || rec.Header().Get("X-Test") != "buffered" { t.Fatalf("tasks=%d response=%+v", tasks, rec) } }) } } type failingBody struct{} func (failingBody) Read([]byte) (int, error) { return 0, errors.New("broken upload") } func (failingBody) Close() error { return nil } func TestOwnerMiddlewareRejectsBeforeDispatch(t *testing.T) { for _, tc := range []struct { name string body io.ReadCloser status int }{ {"oversize", io.NopCloser(strings.NewReader(strings.Repeat("x", maxAdminRequestBytes+1))), http.StatusRequestEntityTooLarge}, {"read error", failingBody{}, http.StatusBadRequest}, } { t.Run(tc.name, func(t *testing.T) { s := &AdminServer{runOnOwner: func(func()) bool { t.Error("invalid request dispatched") return false }} r := httptest.NewRequest("POST", "/api/test", nil) r.Body = tc.body rec := httptest.NewRecorder() s.ownerMiddleware(nil).ServeHTTP(rec, r) if rec.Code != tc.status { t.Fatalf("status=%d, want %d", rec.Code, tc.status) } }) } } func TestOwnerMiddlewareShutdownAndCancellation(t *testing.T) { t.Run("shutdown", func(t *testing.T) { s := &AdminServer{runOnOwner: func(func()) bool { return false }} rec := httptest.NewRecorder() s.ownerMiddleware(nil).ServeHTTP(rec, httptest.NewRequest("POST", "/api/test", nil)) if rec.Code != http.StatusServiceUnavailable { t.Fatalf("status=%d", rec.Code) } }) t.Run("cancelled while queued", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() s := &AdminServer{runOnOwner: func(fn func()) bool { cancel() fn() return true }} s.ownerMiddleware(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { t.Error("cancelled request executed") })).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("POST", "/api/test", nil).WithContext(ctx)) }) } func TestOwnerMiddlewarePanicReturnsToHTTPGoroutine(t *testing.T) { ownerReturned := false s := &AdminServer{runOnOwner: func(fn func()) bool { fn() ownerReturned = true return true }} defer func() { if value := recover(); value != "handler panic" || !ownerReturned { t.Errorf("panic=%v, owner returned=%v", value, ownerReturned) } }() s.ownerMiddleware(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { panic("handler panic") })).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("POST", "/api/test", nil)) t.Fatal("panic was swallowed") }