1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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 {
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)
}
}
|