W5a gave the web two verbs that cost nothing. These are the three that take arguments and spend coins: set out for a zone with a supply loadout, walk back into the run you extracted from, hire the pet sitter for a week or a month. Between them they cover the most common thing anybody does in the game, which until now could only be typed into Matrix. Arguments are the new surface, so they are the thing to be careful with. Nothing in a request is trusted: every zone, loadout and duration is looked up in the offer list gogobee pushed onto that owner's own private row, and the order stores what was found there rather than what was sent. A forged zone resolves to nothing and never becomes an order. gogobee then re-resolves all of it anyway, because an offer is a quote and a quote is not a permission. The money confirm is the equip panel's, lifted: cost, balance, and the balance it leaves. When it does not cover, it says so instead of printing a negative. Verified in a browser rather than only in tests, which is where both real defects came from: the confirm box was appending to the whole panel and so appeared at the bottom of the section instead of under the button that raised it, and button prices printed as EUR45000 above a dialog reading EUR45,000. Claude-Session: https://claude.ai/code/session_012bxpQQJDjC1mTtLN3VVtBQ
114 lines
3.6 KiB
Go
114 lines
3.6 KiB
Go
package storage
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func TestAdvOrderRoundTrip(t *testing.T) {
|
|
setupTestDB(t)
|
|
|
|
o, err := InsertAdvOrder("sub-1", "josie", "tok-josie", "Josie", AdvActionExtract, nil)
|
|
if err != nil {
|
|
t.Fatalf("insert: %v", err)
|
|
}
|
|
if o.GUID == "" || o.Status != AdvOrderPending {
|
|
t.Fatalf("order = %+v, want a guid and pending", o)
|
|
}
|
|
|
|
pending, err := PendingAdvOrders(10)
|
|
if err != nil {
|
|
t.Fatalf("pending: %v", err)
|
|
}
|
|
if len(pending) != 1 || pending[0].GUID != o.GUID || pending[0].Action != AdvActionExtract {
|
|
t.Fatalf("pending = %+v", pending)
|
|
}
|
|
|
|
got, err := ResolveAdvOrder(o.GUID, AdvOrderApplied, "out on day 3")
|
|
if err != nil {
|
|
t.Fatalf("resolve: %v", err)
|
|
}
|
|
if got.Status != AdvOrderApplied || got.Detail != "out on day 3" {
|
|
t.Fatalf("resolved = %+v", got)
|
|
}
|
|
if left, _ := PendingAdvOrders(10); len(left) != 0 {
|
|
t.Fatalf("a resolved order is still pending: %+v", left)
|
|
}
|
|
}
|
|
|
|
// TestAdvOrderVerdictOnlyMovesAPendingRow is the idempotency mechanic: gogobee
|
|
// retries its verdict push, so the second one must be a read, not a write.
|
|
func TestAdvOrderVerdictOnlyMovesAPendingRow(t *testing.T) {
|
|
setupTestDB(t)
|
|
o, _ := InsertAdvOrder("sub-1", "josie", "tok-josie", "Josie", AdvActionSiegeJoin, nil)
|
|
if _, err := ResolveAdvOrder(o.GUID, AdvOrderApplied, "first"); err != nil {
|
|
t.Fatalf("first verdict: %v", err)
|
|
}
|
|
got, err := ResolveAdvOrder(o.GUID, AdvRejectedNoSiege, "second")
|
|
if err != nil {
|
|
t.Fatalf("second verdict: %v", err)
|
|
}
|
|
if got.Status != AdvOrderApplied || got.Detail != "first" {
|
|
t.Fatalf("order = %q/%q, want the first verdict to stand", got.Status, got.Detail)
|
|
}
|
|
}
|
|
|
|
func TestAdvOrderRejectsBadInput(t *testing.T) {
|
|
setupTestDB(t)
|
|
|
|
if _, err := InsertAdvOrder("sub-1", "josie", "tok", "Josie", "sell_house", nil); err == nil {
|
|
t.Fatal("an unknown action was accepted")
|
|
}
|
|
o, _ := InsertAdvOrder("sub-1", "josie", "tok", "Josie", AdvActionExtract, nil)
|
|
if _, err := ResolveAdvOrder(o.GUID, "exploded", ""); err == nil {
|
|
t.Fatal("an unknown verdict was accepted")
|
|
}
|
|
if _, err := AdvOrderByGUID("nope"); !errors.Is(err, ErrNoSuchAdvOrder) {
|
|
t.Fatalf("unknown guid err = %v, want ErrNoSuchAdvOrder", err)
|
|
}
|
|
}
|
|
|
|
// TestHasPendingAdvOrderIsPerVerb: the guard stops a double-click on one button,
|
|
// not the other button.
|
|
func TestHasPendingAdvOrderIsPerVerb(t *testing.T) {
|
|
setupTestDB(t)
|
|
o, _ := InsertAdvOrder("sub-1", "josie", "tok", "Josie", AdvActionExtract, nil)
|
|
|
|
if got, _ := HasPendingAdvOrder("sub-1", AdvActionExtract); !got {
|
|
t.Fatal("a pending extract wasn't seen")
|
|
}
|
|
if got, _ := HasPendingAdvOrder("sub-1", AdvActionSiegeJoin); got {
|
|
t.Fatal("a pending extract blocked a bout")
|
|
}
|
|
if got, _ := HasPendingAdvOrder("sub-2", AdvActionExtract); got {
|
|
t.Fatal("one owner's pending order was seen for another")
|
|
}
|
|
// A resolved order stops holding the verb.
|
|
if _, err := ResolveAdvOrder(o.GUID, AdvOrderApplied, ""); err != nil {
|
|
t.Fatalf("resolve: %v", err)
|
|
}
|
|
if got, _ := HasPendingAdvOrder("sub-1", AdvActionExtract); got {
|
|
t.Fatal("a resolved order still holds its verb")
|
|
}
|
|
}
|
|
|
|
func TestAdvOrdersByOwnerScopes(t *testing.T) {
|
|
setupTestDB(t)
|
|
if _, err := InsertAdvOrder("sub-A", "alice", "tok-a", "Alice", AdvActionExtract, nil); err != nil {
|
|
t.Fatalf("insert: %v", err)
|
|
}
|
|
if _, err := InsertAdvOrder("sub-B", "bob", "tok-b", "Bob", AdvActionExtract, nil); err != nil {
|
|
t.Fatalf("insert: %v", err)
|
|
}
|
|
got, err := AdvOrdersByOwner("sub-A", 10)
|
|
if err != nil {
|
|
t.Fatalf("by owner: %v", err)
|
|
}
|
|
if len(got) != 1 || got[0].OwnerLocalpart != "alice" {
|
|
t.Fatalf("orders = %+v, want only alice's", got)
|
|
}
|
|
if n, _ := CountAdvOrdersSince("sub-A", 0); n != 1 {
|
|
t.Fatalf("count = %d, want 1", n)
|
|
}
|
|
}
|