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(ServerDeps{ Config: config.Default(), Accounts: h.g.AccountStore, World: h.g.World, Items: h.g.ItemStore, Objects: h.g.ObjectStore, Mobs: h.g.MobStore, DataDir: h.dir, ReloadCourses: h.g.CourseStore.ReloadAll, RewriteRoomIDs: h.g.MigrateRoomIDsOnOwner, EvictRoom: h.g.EvictCasinoRoom, RebuildCraftIndex: h.g.RebuildCraftIndex, ConfigPath: filepath.Join(h.dir, "config.yaml"), RunOnOwner: 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") } }