From 61881fd0c1a7066293f552be1a747143f70d03ff Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 15 Sep 2026 20:44:31 -0400 Subject: fix: registration overwrite, recovery chip/payout edge cases, bulk-delete rollback --- internal/admin/api_rooms_bulk_delete_test.go | 70 ++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 internal/admin/api_rooms_bulk_delete_test.go (limited to 'internal/admin/api_rooms_bulk_delete_test.go') diff --git a/internal/admin/api_rooms_bulk_delete_test.go b/internal/admin/api_rooms_bulk_delete_test.go new file mode 100644 index 0000000..083b890 --- /dev/null +++ b/internal/admin/api_rooms_bulk_delete_test.go @@ -0,0 +1,70 @@ +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, int) { + 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, rec.Code +} + +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) + } +} -- cgit v1.2.3