Files
Pete/internal/storage/equip_test.go
prosolis 1589c36e96 adventure: let an owner equip and unequip from their own page
The one adventure ask that carries intent back to the game box, built the
mischief way: no new network route, Pete records the equip/unequip and gogobee
polls it and files a verdict. The who page grows Equip / Take off buttons on the
owner's own worn and backpack panels, and a pending-changes strip that shows
'queued' until the verdict lands, never claiming a change it can't see.

The item handle is the inventory row id, sent only on wearable magic items, so a
non-zero id is also what gates the button. gogobee resolves the owner by
localpart, never a name.
2026-07-17 08:44:28 -07:00

136 lines
4.2 KiB
Go

package storage
import (
"errors"
"testing"
"time"
)
func TestEquipOrderLifecycle(t *testing.T) {
setupTestDB(t)
o, err := InsertEquipOrder("sub-1", "josie", "Josie", 42, "Cloak of Elvenkind", "cloak", EquipActionEquip)
if err != nil {
t.Fatal(err)
}
if o.Status != EquipPending {
t.Fatalf("fresh order status = %q, want pending", o.Status)
}
if o.ItemID != 42 || o.Slot != "cloak" || o.Action != EquipActionEquip {
t.Fatalf("order fields lost through insert: %+v", o)
}
pending, err := PendingEquipOrders(10)
if err != nil {
t.Fatal(err)
}
if len(pending) != 1 || pending[0].GUID != o.GUID {
t.Fatalf("pending = %+v, want the one order we just placed", pending)
}
got, err := ResolveEquipOrder(o.GUID, EquipApplied, "worn and bonded")
if err != nil {
t.Fatal(err)
}
if got.Status != EquipApplied || got.Detail != "worn and bonded" {
t.Fatalf("resolved order = %+v, want applied with detail", got)
}
if pending, _ := PendingEquipOrders(10); len(pending) != 0 {
t.Fatalf("applied order still pending: %+v", pending)
}
}
// TestEquipResolveIsIdempotent: gogobee's poll loop retries, so a verdict can
// arrive twice — the second must not overwrite the first. This is the whole
// reason Pete can copy mischief's mechanic even though the game action underneath
// is not itself idempotent (gogobee guards that separately, on the order guid).
func TestEquipResolveIsIdempotent(t *testing.T) {
setupTestDB(t)
o, err := InsertEquipOrder("sub-1", "josie", "Josie", 7, "Ring of Protection", "ring_1", EquipActionEquip)
if err != nil {
t.Fatal(err)
}
if _, err := ResolveEquipOrder(o.GUID, EquipApplied, "first"); err != nil {
t.Fatal(err)
}
got, err := ResolveEquipOrder(o.GUID, EquipRejectedNotOwned, "second")
if err != nil {
t.Fatalf("re-resolve errored: %v", err)
}
if got.Status != EquipApplied || got.Detail != "first" {
t.Fatalf("idempotency broken: order became %+v", got)
}
}
func TestEquipResolveUnknownAndBadVerdict(t *testing.T) {
setupTestDB(t)
if _, err := ResolveEquipOrder("nope", EquipApplied, ""); !errors.Is(err, ErrNoSuchEquipOrder) {
t.Fatalf("unknown guid err = %v, want ErrNoSuchEquipOrder", err)
}
o, _ := InsertEquipOrder("sub-1", "josie", "Josie", 1, "Boots", "feet", EquipActionEquip)
if _, err := ResolveEquipOrder(o.GUID, "exploded", ""); err == nil {
t.Error("a bogus verdict status was accepted")
}
if got, _ := EquipOrderByGUID(o.GUID); got.Status != EquipPending {
t.Fatalf("order moved off pending on a bad verdict: %q", got.Status)
}
}
// TestEquipInsertRejectsBadAction: the action is part of the contract, so a value
// that isn't equip/unequip must not reach the table.
func TestEquipInsertRejectsBadAction(t *testing.T) {
setupTestDB(t)
if _, err := InsertEquipOrder("sub-1", "josie", "Josie", 1, "Thing", "cloak", "wield"); err == nil {
t.Fatal("a bogus action was accepted")
}
}
// TestEquipUnequipCarriesSlotNotItem: an unequip has no live inventory row to name,
// so it rides on the slot alone — item_id 0 is expected, not a bug.
func TestEquipUnequipCarriesSlotNotItem(t *testing.T) {
setupTestDB(t)
o, err := InsertEquipOrder("sub-1", "josie", "Josie", 0, "Cloak of Elvenkind", "cloak", EquipActionUnequip)
if err != nil {
t.Fatal(err)
}
got, _ := EquipOrderByGUID(o.GUID)
if got.Action != EquipActionUnequip || got.Slot != "cloak" || got.ItemID != 0 {
t.Fatalf("unequip order = %+v, want slot-keyed with no item id", got)
}
}
func TestEquipOrdersByOwnerAndCount(t *testing.T) {
setupTestDB(t)
for i := 0; i < 3; i++ {
if _, err := InsertEquipOrder("sub-A", "alice", "Alice", int64(i+1), "Item", "cloak", EquipActionEquip); err != nil {
t.Fatal(err)
}
}
if _, err := InsertEquipOrder("sub-B", "bob", "Bob", 9, "Item", "cloak", EquipActionEquip); err != nil {
t.Fatal(err)
}
mine, err := EquipOrdersByOwner("sub-A", 20)
if err != nil {
t.Fatal(err)
}
if len(mine) != 3 {
t.Fatalf("alice sees %d orders, want 3 (and none of bob's)", len(mine))
}
n, err := CountEquipOrdersSince("sub-A", time.Now().Add(-time.Hour).Unix())
if err != nil {
t.Fatal(err)
}
if n != 3 {
t.Fatalf("count since an hour ago = %d, want 3", n)
}
if n, _ := CountEquipOrdersSince("sub-A", time.Now().Add(time.Hour).Unix()); n != 0 {
t.Fatalf("count since the future = %d, want 0", n)
}
}