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_routes_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_routes_test.go')
| -rw-r--r-- | internal/admin/owner_routes_test.go | 68 |
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") + } +} |
