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) } }