aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/owner_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-15 20:14:02 -0400
committerhistoria <[not public]>2026-09-15 20:14:02 -0400
commit69235cac5b3da5ff0dc20310e7474cabd8e63ba3 (patch)
treece96c9506531e219c157f29ac6a6dc53de1df15b /internal/admin/owner_test.go
parent6f3c43aca62a7d33a19f14f6ad7c16891952673a (diff)
downloadthehouseoficarus-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.go152
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")
+}