diff options
Diffstat (limited to 'internal/admin/api_items_undo_test.go')
| -rw-r--r-- | internal/admin/api_items_undo_test.go | 82 |
1 files changed, 82 insertions, 0 deletions
diff --git a/internal/admin/api_items_undo_test.go b/internal/admin/api_items_undo_test.go new file mode 100644 index 0000000..3ccbe10 --- /dev/null +++ b/internal/admin/api_items_undo_test.go @@ -0,0 +1,82 @@ +package admin + +import ( + "encoding/json" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "thehouseoficarus/internal/item" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +func newTestWorld(dir string) *world.World { + return world.New(dir) +} + +// Regression: _raw must not leak the password hash even when it is a +// multiline YAML block scalar, which line-based masking misses. +func TestAccountAPIRedactsMultilinePasswordHash(t *testing.T) { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "players", "accounts"), 0o755); err != nil { + t.Fatal(err) + } + body := "name: alice\npassword_hash: |-\n sensitive-hash-sentinel\n" + if err := os.WriteFile(filepath.Join(dir, "players", "accounts", "alice.yaml"), []byte(body), 0o644); err != nil { + t.Fatal(err) + } + s := &AdminServer{accountStore: player.NewAccountStore(dir)} + + rr := httptest.NewRecorder() + s.getAccount(rr, httptest.NewRequest("GET", "/api/players/account/alice", nil), "alice") + + if strings.Contains(rr.Body.String(), "sensitive-hash-sentinel") { + t.Fatalf("password hash leaked in response: %s", rr.Body.String()) + } + var resp map[string]any + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("response is not valid json: %v", err) + } +} + +// Regression: undo/redo of an item edit must invalidate the runtime item +// cache (and rebuild dependent indexes), not leave gameplay on the stale +// definition. +func TestUndoRefreshesRuntimeItemCache(t *testing.T) { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "items"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(dir, "rooms"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "items", "coin.yaml"), []byte("name: Coin\nvalue: 1\n"), 0o644); err != nil { + t.Fatal(err) + } + s := &AdminServer{ + dataDir: dir, + itemStore: item.NewItemStore(dir), + world: newTestWorld(dir), + undoStack: NewUndoStack(dir), + } + + // Warm the cache with the original definition. + if _, err := s.itemStore.Load("coin"); err != nil { + t.Fatal(err) + } + s.updateItem(httptest.NewRecorder(), httptest.NewRequest("PUT", "/api/items/coin", + strings.NewReader(`{"name":"Coin","value":9}`)), "coin") + if updated, err := s.itemStore.Load("coin"); err != nil || updated.Value != 9 { + t.Fatalf("item edit did not reach the runtime cache: %v %+v", err, updated) + } + + s.doUndo(httptest.NewRecorder(), httptest.NewRequest("POST", "/api/undo/undo", nil)) + + restored, err := s.itemStore.Load("coin") + if err != nil || restored.Value != 1 { + t.Fatalf("undo left stale runtime state: %v value=%+v", err, restored) + } +} |
