aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/owner_routes_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_routes_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_routes_test.go')
-rw-r--r--internal/admin/owner_routes_test.go68
1 files changed, 68 insertions, 0 deletions
diff --git a/internal/admin/owner_routes_test.go b/internal/admin/owner_routes_test.go
new file mode 100644
index 0000000..a061b0e
--- /dev/null
+++ b/internal/admin/owner_routes_test.go
@@ -0,0 +1,68 @@
+package admin
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "thehouseoficarus/internal/config"
+)
+
+// The production router must not leave file-only mutations or undo/redo on
+// the HTTP goroutine. A rejecting dispatcher prevents ALL API handlers from
+// touching files, including future routes and read endpoints.
+func TestAPIRoutesRequireOwnerAdmission(t *testing.T) {
+ h, _ := newEditorHarness(t)
+ tasks := 0
+ s, err := NewServer(config.Default(), false, h.g.AccountStore, h.g.World, h.g.ItemStore, h.g.ObjectStore, h.g.MobStore, h.dir,
+ h.g.CourseStore.ReloadAll, h.g.MigrateRoomIDsOnOwner, h.g.EvictCasinoRoom, h.g.RebuildCraftIndex, filepath.Join(h.dir, "config.yaml"),
+ func(func()) bool { tasks++; return false })
+ if err != nil {
+ t.Fatal(err)
+ }
+ cookieRec := httptest.NewRecorder()
+ s.setAuthCookie(cookieRec, "alice")
+ cookie := cookieRec.Result().Cookies()[0]
+
+ routes := []string{
+ "GET /api/map", "GET /api/dashboard", "GET /api/undo/state",
+ "POST /api/rooms", "PUT /api/rooms/2", "DELETE /api/rooms/2",
+ "POST /api/rooms/new-empty", "POST /api/rooms/link", "DELETE /api/rooms/link",
+ "POST /api/rooms/insert", "POST /api/rooms/remove", "POST /api/rooms/move",
+ "POST /api/rooms/rename", "POST /api/rooms/swap", "POST /api/rooms/bulk-edit",
+ "POST /api/rooms/bulk-delete", "POST /api/rooms/resolve-duplicate",
+ "POST /api/room-dirs/create", "POST /api/room-dirs/rename",
+ "POST /api/courses", "PUT /api/courses/test", "DELETE /api/courses/test",
+ "POST /api/triggers", "PUT /api/triggers/test", "DELETE /api/triggers/test",
+ "PUT /api/files?path=test.yaml", "POST /api/files?path=test.yaml",
+ "DELETE /api/files?path=test.yaml", "POST /api/files/rename",
+ "POST /api/colors", "POST /api/colors/reset",
+ "POST /api/undo/undo", "POST /api/undo/redo",
+ }
+ for _, entity := range []string{"objects", "items", "mobs", "drops", "hazards", "techs", "modules"} {
+ for _, route := range []string{"POST /api/%s", "PUT /api/%s/test", "DELETE /api/%s/test", "POST /api/%s/rename", "POST /api/%s/move", "POST /api/%s/dirs"} {
+ routes = append(routes, strings.ReplaceAll(route, "%s", entity))
+ }
+ }
+ for _, route := range routes {
+ t.Run(route, func(t *testing.T) {
+ method, path, _ := strings.Cut(route, " ")
+ r := httptest.NewRequest(method, path, strings.NewReader(`{}`))
+ r.AddCookie(cookie)
+ rec := httptest.NewRecorder()
+ before := tasks
+ s.ServeHTTP(rec, r)
+ if tasks != before+1 || rec.Code != http.StatusServiceUnavailable {
+ t.Fatalf("dispatcher calls=%d status=%d", tasks-before, rec.Code)
+ }
+ })
+ }
+ before := tasks
+ rec := httptest.NewRecorder()
+ s.ServeHTTP(rec, httptest.NewRequest("POST", "/api/rooms", nil))
+ if tasks != before || rec.Code != http.StatusUnauthorized {
+ t.Fatal("unauthenticated request reached owner")
+ }
+}