aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/api_rooms_bulk_delete_test.go
blob: 66464a5d615c76b1dc9da4cce4a5e2a3af273aa9 (plain)
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)
	}
}