diff options
| author | historia <[not public]> | 2026-09-15 20:14:02 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-15 20:14:02 -0400 |
| commit | 69235cac5b3da5ff0dc20310e7474cabd8e63ba3 (patch) | |
| tree | ce96c9506531e219c157f29ac6a6dc53de1df15b /internal/admin/owner_test.go | |
| parent | 6f3c43aca62a7d33a19f14f6ad7c16891952673a (diff) | |
| download | thehouseoficarus-69235cac5b3da5ff0dc20310e7474cabd8e63ba3.tar.gz | |
feat: route all html editor API mutations through single owner task
Diffstat (limited to 'internal/admin/owner_test.go')
| -rw-r--r-- | internal/admin/owner_test.go | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/internal/admin/owner_test.go b/internal/admin/owner_test.go new file mode 100644 index 0000000..35538bc --- /dev/null +++ b/internal/admin/owner_test.go @@ -0,0 +1,152 @@ +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") +} |
