Files
gogobee/internal/plugin/adventure_shop_sell_test.go
prosolis 25accab5c0 UX S1: sell-all guard against magic-item deletion (B1)
`!adventure sell all` and `!adventure sell <name>` previously treated
Type=="magic_item" rows as bulk loot, silently deleting bonded curios for
their base coin Value. Mirror the existing MasterworkGear/Arena/card
branch and reroute the player to `!adventure equip-magic`.

Tally kept curios in the sell-all summary; surface a dedicated
empty-rejection message when the only contents are curios; add the same
guard to the single-item path.

Tests cover: mixed bag (magic item preserved, junk sells), curio-only
bag (no-op + reroute), explicit sell <curio name> (refused + reroute).
2026-05-14 21:19:31 -07:00

120 lines
3.4 KiB
Go

package plugin
import (
"strings"
"testing"
"gogobee/internal/db"
"maunium.net/go/mautrix/id"
)
// freshShopTestDB spins up an empty DB in a tempdir for the sell-path tests.
// Distinct from setupAuditTestDB, which clones the prod snapshot.
func freshShopTestDB(t *testing.T) {
t.Helper()
dir := t.TempDir()
db.Close()
if err := db.Init(dir); err != nil {
t.Fatal(err)
}
t.Cleanup(db.Close)
}
// TestSellAll_PreservesMagicItems — B1 (data-loss blocker): `sell all` must
// route around curios (Type=="magic_item") so a careless liquidation can't
// silently delete a Legendary Ring. Junk in the same bag still sells.
func TestSellAll_PreservesMagicItems(t *testing.T) {
freshShopTestDB(t)
uid := id.UserID("@sellall_magic:test")
curio := AdvItem{Name: "Ring of Protection", Type: "magic_item", Tier: 3, Value: 500}
junk := AdvItem{Name: "Iron Ore", Type: "material", Tier: 1, Value: 40}
if err := addAdvInventoryItem(uid, curio); err != nil {
t.Fatal(err)
}
if err := addAdvInventoryItem(uid, junk); err != nil {
t.Fatal(err)
}
euro := &EuroPlugin{}
euro.ensureBalance(uid)
p := &AdventurePlugin{euro: euro}
msg := p.advSellAll(uid)
if !strings.Contains(msg, "Sold 1 item") {
t.Errorf("expected exactly one item sold, msg=%q", msg)
}
if !strings.Contains(strings.ToLower(msg), "curio") {
t.Errorf("sell-all summary should call out kept curios, msg=%q", msg)
}
left, err := loadAdvInventory(uid)
if err != nil {
t.Fatal(err)
}
if len(left) != 1 || left[0].Type != "magic_item" {
t.Fatalf("magic item not preserved: %+v", left)
}
// Payout reflects only the junk's €40 (5% pot cut → ~€38 credited).
bal := euro.GetBalance(uid)
if bal <= 0 || bal > 40 {
t.Errorf("payout %v outside expected (0,40] for €40 junk", bal)
}
}
// TestSellAll_OnlyMagicItems — when the bag is curios only, sell-all reroutes
// to !adventure equip-magic and touches nothing.
func TestSellAll_OnlyMagicItems(t *testing.T) {
freshShopTestDB(t)
uid := id.UserID("@sellall_only_magic:test")
if err := addAdvInventoryItem(uid, AdvItem{Name: "Cloak of Elvenkind", Type: "magic_item", Value: 300}); err != nil {
t.Fatal(err)
}
euro := &EuroPlugin{}
euro.ensureBalance(uid)
p := &AdventurePlugin{euro: euro}
msg := p.advSellAll(uid)
if !strings.Contains(msg, "equip-magic") {
t.Errorf("expected reroute to equip-magic, msg=%q", msg)
}
if euro.GetBalance(uid) != 0 {
t.Errorf("balance moved on curio-only sell-all: %v", euro.GetBalance(uid))
}
left, _ := loadAdvInventory(uid)
if len(left) != 1 {
t.Errorf("curio deleted by sell-all: %+v", left)
}
}
// TestSellItem_MagicItemRerouted — explicit `sell <curio name>` must refuse
// and point the player at the bonding command.
func TestSellItem_MagicItemRerouted(t *testing.T) {
freshShopTestDB(t)
uid := id.UserID("@sellone_magic:test")
curio := AdvItem{Name: "Amulet of Health", Type: "magic_item", Value: 750}
if err := addAdvInventoryItem(uid, curio); err != nil {
t.Fatal(err)
}
euro := &EuroPlugin{}
euro.ensureBalance(uid)
p := &AdventurePlugin{euro: euro}
msg := p.advSellItem(uid, "amulet")
if !strings.Contains(msg, "equip-magic") {
t.Errorf("expected reroute to equip-magic, msg=%q", msg)
}
if euro.GetBalance(uid) != 0 {
t.Errorf("balance moved on refused curio sale: %v", euro.GetBalance(uid))
}
left, _ := loadAdvInventory(uid)
if len(left) != 1 {
t.Errorf("curio deleted by refused sale: %+v", left)
}
}