aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/bulk_edit_regression_test.go
blob: 4828c93cd0202a21a4ce05d88838886500fd2997 (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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package admin

// Bulk-edit mutation-plan regressions: a same-directory "move" must never
// delete the room, a failure anywhere must leave every room untouched (no
// partial changes without an undo record), and a successful plan applies as
// one unit with a single undo record.

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func postBulkEdit(t *testing.T, s *AdminServer, body string) (map[string]any, int) {
	t.Helper()
	req := httptest.NewRequest(http.MethodPost, "/api/rooms/bulk-edit", strings.NewReader(body))
	rec := httptest.NewRecorder()
	s.handleBulkEdit(rec, req)
	var resp map[string]any
	_ = json.Unmarshal(rec.Body.Bytes(), &resp)
	return resp, rec.Code
}

// TestBulkEditSameDirectoryMovePreservesRoom: moving a room to the
// directory it already lives in is an in-place edit, not a delete. The old
// implementation removed the file and reported success.
func TestBulkEditSameDirectoryMovePreservesRoom(t *testing.T) {
	s := newTestAdminServer(t, map[int]string{1: "id: 1\nname: Room\n"})
	oldPath, _ := s.world.GetRoomPath(1)
	dir := filepath.Join(s.dataDir, "rooms", "area")
	if err := os.MkdirAll(dir, 0o755); err != nil {
		t.Fatal(err)
	}
	dest := filepath.Join(dir, "1.yaml")
	if err := os.Rename(oldPath, dest); err != nil {
		t.Fatal(err)
	}
	s.world.RebuildRoomIndex(s.dataDir)

	resp, code := postBulkEdit(t, s, `{"ids":[1],"patch":{"dir":"area"}}`)
	if code != http.StatusOK {
		t.Fatalf("bulk edit failed: %d %v", code, resp)
	}
	if _, err := os.Stat(dest); err != nil {
		t.Fatalf("same-directory move deleted the room: %v", err)
	}
	if _, err := os.Stat(oldPath); !os.IsNotExist(err) {
		t.Fatal("old path should not reappear for an in-place edit")
	}
}

// TestBulkEditFailureLeavesNoPartialChanges: a parse failure on any selected
// room aborts the whole plan before any file changes, so no room is mutated
// without an undo record.
func TestBulkEditFailureLeavesNoPartialChanges(t *testing.T) {
	s := newTestAdminServer(t, map[int]string{
		1: "id: 1\nname: Room\n",
		2: "invalid: [\n",
	})
	path, _ := s.world.GetRoomPath(1)
	before, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}

	resp, code := postBulkEdit(t, s, `{"ids":[1,2],"patch":{"color":"red"}}`)
	if code == http.StatusOK && resp["error"] == nil {
		t.Fatalf("bulk edit unexpectedly succeeded: %v", resp)
	}
	if resp["error"] == nil {
		t.Fatalf("expected an error response, got: %v", resp)
	}
	after, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	if string(after) != string(before) {
		t.Fatalf("failed bulk edit mutated room 1: before=%q after=%q", before, after)
	}
	if s.undoStack.Info().StackSize != 0 {
		t.Fatal("failed bulk edit pushed an undo record")
	}
}

// TestBulkEditSuccessSingleUndoRecord: a valid plan applies every patch and
// pushes exactly one undo record whose undo restores every file.
func TestBulkEditSuccessSingleUndoRecord(t *testing.T) {
	s := newTestAdminServer(t, map[int]string{
		1: "id: 1\nname: Room\n",
		2: "id: 2\nname: Other\n",
	})

	resp, code := postBulkEdit(t, s, `{"ids":[1,2],"patch":{"color":"red"}}`)
	if code != http.StatusOK {
		t.Fatalf("bulk edit failed: %d %v", code, resp)
	}
	for _, id := range []int{1, 2} {
		path, _ := s.world.GetRoomPath(id)
		data, err := os.ReadFile(path)
		if err != nil {
			t.Fatal(err)
		}
		if !strings.Contains(string(data), "color: red") {
			t.Fatalf("room %d not patched: %q", id, data)
		}
	}
	if info := s.undoStack.Info(); info.StackSize != 1 {
		t.Fatalf("undo stack size = %d, want exactly 1 plan", info.StackSize)
	}
	if s.undoStack.Undo() == nil {
		t.Fatal("undo failed")
	}
	path, _ := s.world.GetRoomPath(1)
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(string(data), "color: red") {
		t.Fatalf("undo did not restore room 1: %q", data)
	}
}