package player // Regression: LoadCharacter must sanitize the inventory/bank slot maps. A // hand-edited or corrupt save with out-of-range keys, nil entries, or // non-positive quantities used to leave items no gameplay loop could reach // while still consuming capacity, and could drive FreeSlots negative. import ( "os" "testing" ) func TestLoadCharacterSanitizesSlotMaps(t *testing.T) { s := testAccountStore(t) content := `name: Corrupt room_id: 1 inventory: 0: item_id: rope quantity: 2 3: null 5: item_id: ghost quantity: 0 7: item_id: negative quantity: -4 99: item_id: out_of_range quantity: 1 bank: 0: item_id: coins quantity: 100 30: item_id: far_slot quantity: 1 -2: item_id: bogus quantity: 3 4: null 6: item_id: empty quantity: 0 ` if err := os.WriteFile(s.CharPath("Corrupt"), []byte(content), 0600); err != nil { t.Fatal(err) } p, err := s.LoadCharacter("Corrupt") if err != nil { t.Fatal(err) } if len(p.Inventory) != 1 { t.Fatalf("inventory = %v, want only the valid slot 0", p.Inventory) } slot := p.Inventory[0] if slot == nil || slot.ItemID != "rope" || slot.Quantity != 2 { t.Errorf("valid slot 0 changed: %+v", slot) } if got := p.FreeSlots(); got != 27 { t.Errorf("FreeSlots() = %d, want 27", got) } if got := p.FreeSlots(); got < 0 { t.Errorf("FreeSlots() went negative: %d", got) } if len(p.Bank) != 2 { t.Fatalf("bank = %v, want only slots 0 and 30", p.Bank) } if bs := p.Bank[0]; bs == nil || bs.ItemID != "coins" || bs.Quantity != 100 { t.Errorf("valid bank slot 0 changed: %+v", bs) } // Bank slots past the inventory's 28 are valid: the bank grows unbounded. if bs := p.Bank[30]; bs == nil || bs.ItemID != "far_slot" { t.Errorf("valid bank slot 30 dropped: %+v", bs) } if _, ok := p.Bank[-2]; ok { t.Error("negative bank slot survived sanitization") } } func TestSaveLoadKeepsValidSlots(t *testing.T) { s := testAccountStore(t) p := New("Alice") p.SetInvSlot(0, &InventorySlot{ItemID: "rope", Quantity: 2, Quality: 3, MaxQuality: 5}) p.SetInvSlot(27, &InventorySlot{ItemID: "torch", Quantity: 1}) p.SetBankSlot(0, &InventorySlot{ItemID: "coins", Quantity: 100}) p.SetBankSlot(30, &InventorySlot{ItemID: "spade", Quantity: 1}) if err := s.SaveCharacter(p); err != nil { t.Fatal(err) } loaded, err := s.LoadCharacter("Alice") if err != nil { t.Fatal(err) } if len(loaded.Inventory) != 2 || len(loaded.Bank) != 2 { t.Fatalf("slots lost on a round trip: inventory=%v bank=%v", loaded.Inventory, loaded.Bank) } if got := loaded.Inventory[0]; got == nil || got.ItemID != "rope" || got.Quantity != 2 || got.Quality != 3 || got.MaxQuality != 5 { t.Errorf("inventory slot 0 changed: %+v", got) } if got := loaded.Inventory[27]; got == nil || got.ItemID != "torch" { t.Errorf("inventory slot 27 changed: %+v", got) } if got := loaded.Bank[30]; got == nil || got.ItemID != "spade" { t.Errorf("bank slot 30 changed: %+v", got) } if loaded.FreeSlots() != 26 { t.Errorf("FreeSlots() = %d, want 26", loaded.FreeSlots()) } }