package admin // Bulk-delete regression tests: duplicate IDs in one request are rejected // before any file is touched, and a mid-plan delete failure rolls back // every room it already removed (no rooms stranded without an undo record). import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) func postBulkDelete(t *testing.T, s *AdminServer, body string) map[string]any { t.Helper() req := httptest.NewRequest(http.MethodPost, "/api/rooms/bulk-delete", strings.NewReader(body)) rec := httptest.NewRecorder() s.handleBulkDelete(rec, req) var resp map[string]any _ = json.Unmarshal(rec.Body.Bytes(), &resp) return resp } func TestBulkDeleteRejectsDuplicateIDs(t *testing.T) { s := newTestAdminServer(t, map[int]string{1: "id: 1\nname: Room\n"}) path, _ := s.world.GetRoomPath(1) resp := postBulkDelete(t, s, `{"ids":[1,1]}`) if errMsg, _ := resp["error"].(string); !strings.Contains(errMsg, "duplicate room id 1") { t.Fatalf("duplicate IDs not rejected: %v", resp) } if _, err := os.Stat(path); err != nil { t.Fatalf("rejected bulk-delete removed the room: %v", err) } } // TestBulkDeleteFailureRollsBackDeletions: a delete failure mid-plan // restores the rooms already removed, so a failed plan leaves the world // untouched. The failure is staged at the DELETE phase (not the earlier // snapshot phase) by making room 3's parent directory unwritable after its // file snapshot was taken. func TestBulkDeleteFailureRollsBackDeletions(t *testing.T) { s := newTestAdminServer(t, map[int]string{1: "id: 1\nname: One\n"}) sub := filepath.Join(s.dataDir, "rooms", "sub") if err := os.MkdirAll(sub, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(sub, "3.yaml"), []byte("id: 3\nname: Three\n"), 0o644); err != nil { t.Fatal(err) } s.world.RebuildRoomIndex(s.dataDir) // Room 3's snapshot succeeds (the file is readable), but its removal // fails because the parent directory is not writable. if err := os.Chmod(sub, 0o555); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.Chmod(sub, 0o755); os.RemoveAll(sub) }) path1, _ := s.world.GetRoomPath(1) resp := postBulkDelete(t, s, `{"ids":[1,3]}`) if errMsg, _ := resp["error"].(string); !strings.Contains(errMsg, "delete room 3") { t.Fatalf("expected room-3 delete failure: %v", resp) } if _, err := os.Stat(path1); err != nil { t.Fatalf("failed bulk delete stranded room 1 without an undo record: %v", err) } }