package web import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "pete/internal/storage" ) // newStore is a server with the adventure seam on, a signed-in buyer, and a // board + catalog already pushed. Auth is built by hand for the same reason the // casino tests do it: the OIDC handshake is a network call and none of what's // under test is about it. func newStore(t *testing.T) *Server { t.Helper() s, _ := newAdvServer(t, "tok") s.auth = &Authenticator{secret: []byte("test-secret-key-at-least-16")} now := time.Now().Unix() push := rosterPush{ SnapshotAt: now, Adventurers: []storage.RosterEntry{entry("tok-josie", "Josie", "expedition", "holymachina")}, Tiers: []storage.MischiefTier{ {Key: "grunt", Display: "Grunt", Fee: 40, SignedFee: 50}, {Key: "boss", Display: "Boss", Fee: 1200, SignedFee: 1500}, }, Balances: []storage.MischiefBalance{{Username: "reala", Euro: 500}}, } if w := postRoster(t, s, "tok", push); w.Code != 200 { t.Fatalf("seed roster = %d", w.Code) } return s } // jsonReq builds a bearer-authed (or unauthed, empty token) machine request. func jsonReq(t *testing.T, method, path, token string, body any) *http.Request { t.Helper() var buf bytes.Buffer if body != nil { if err := json.NewEncoder(&buf).Encode(body); err != nil { t.Fatal(err) } } r := httptest.NewRequest(method, path, &buf) if token != "" { r.Header.Set("Authorization", "Bearer "+token) } return r } func placeOrder(t *testing.T, s *Server, username string, req mischiefOrderReq) *httptest.ResponseRecorder { t.Helper() r := as(t, s, username, "POST", "/api/mischief/order", req) w := httptest.NewRecorder() s.handleMischiefOrder(w, r) return w } // TestMischiefOrderHappyPath: a signed-in buyer names a mark on the board and a // tier gogobee offers, and gets a pending order back. func TestMischiefOrderHappyPath(t *testing.T) { s := newStore(t) w := placeOrder(t, s, "reala", mischiefOrderReq{TargetToken: "tok-josie", Tier: "grunt", Signed: false}) if w.Code != 200 { t.Fatalf("order = %d body=%s", w.Code, w.Body.String()) } var o storage.MischiefOrder if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil { t.Fatal(err) } if o.Status != storage.MischiefPending || o.BuyerUsername != "reala" || o.TargetName != "Josie" { t.Fatalf("order = %+v", o) } if pending, _ := storage.PendingMischiefOrders(10); len(pending) != 1 { t.Fatalf("order didn't land in the pending set") } } func TestMischiefOrderRejections(t *testing.T) { s := newStore(t) if w := placeOrder(t, s, "reala", mischiefOrderReq{TargetToken: "tok-josie", Tier: "dragon"}); w.Code != 400 { t.Errorf("unknown tier = %d, want 400", w.Code) } if w := placeOrder(t, s, "reala", mischiefOrderReq{TargetToken: "ghost", Tier: "grunt"}); w.Code != 404 { t.Errorf("off-board target = %d, want 404", w.Code) } if w := placeOrder(t, s, "reala", mischiefOrderReq{Tier: "grunt"}); w.Code != 400 { t.Errorf("empty target = %d, want 400", w.Code) } // Signed in, but no username the ledger can name (a pre-storefront session). if w := placeOrder(t, s, "", mischiefOrderReq{TargetToken: "tok-josie", Tier: "grunt"}); w.Code != 409 { t.Errorf("usernameless session = %d, want 409", w.Code) } } func TestMischiefOrderBurstGuard(t *testing.T) { s := newStore(t) for i := 0; i < mischiefBurstMax; i++ { if w := placeOrder(t, s, "reala", mischiefOrderReq{TargetToken: "tok-josie", Tier: "grunt"}); w.Code != 200 { t.Fatalf("order %d = %d, want 200", i, w.Code) } } if w := placeOrder(t, s, "reala", mischiefOrderReq{TargetToken: "tok-josie", Tier: "grunt"}); w.Code != 429 { t.Fatalf("over-cap order = %d, want 429", w.Code) } } func TestMischiefOrderRequiresAuth(t *testing.T) { s := newStore(t) r := httptest.NewRequest("POST", "/api/mischief/order", nil) w := httptest.NewRecorder() s.handleMischiefOrder(w, r) if w.Code != 401 { t.Fatalf("anonymous order = %d, want 401", w.Code) } } func TestMischiefCatalogCarriesBalance(t *testing.T) { s := newStore(t) r := as(t, s, "reala", "GET", "/api/mischief/catalog", nil) w := httptest.NewRecorder() s.handleMischiefCatalog(w, r) if w.Code != 200 { t.Fatalf("catalog = %d", w.Code) } var resp mischiefCatalogResp if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatal(err) } if len(resp.Tiers) != 2 || !resp.HasBalance || resp.Euro != 500 { t.Fatalf("catalog resp = %+v", resp) } } // ---- the bearer wire ----------------------------------------------------------- func TestMischiefPendingAndClaimBearer(t *testing.T) { s := newStore(t) if w := placeOrder(t, s, "reala", mischiefOrderReq{TargetToken: "tok-josie", Tier: "boss", Signed: true}); w.Code != 200 { t.Fatalf("seed order = %d", w.Code) } // Missing bearer is rejected. { w := httptest.NewRecorder() s.handleMischiefPending(w, jsonReq(t, "GET", "/api/mischief/pending", "", nil)) if w.Code != 401 { t.Fatalf("no-bearer pending = %d, want 401", w.Code) } } // Poll returns the order. var pending []storage.MischiefOrder { w := httptest.NewRecorder() s.handleMischiefPending(w, jsonReq(t, "GET", "/api/mischief/pending", "tok", nil)) if w.Code != 200 { t.Fatalf("pending = %d", w.Code) } if err := json.Unmarshal(w.Body.Bytes(), &pending); err != nil { t.Fatal(err) } } if len(pending) != 1 { t.Fatalf("pending count = %d, want 1", len(pending)) } guid := pending[0].GUID // Claim it placed. { w := httptest.NewRecorder() s.handleMischiefClaim(w, jsonReq(t, "POST", "/api/mischief/claim", "tok", mischiefVerdict{GUID: guid, Status: storage.MischiefPlaced, Detail: "out for delivery"})) if w.Code != 200 { t.Fatalf("claim = %d body=%s", w.Code, w.Body.String()) } } if got, _ := storage.MischiefOrderByGUID(guid); got.Status != storage.MischiefPlaced { t.Fatalf("order status after claim = %q", got.Status) } // And it's gone from the pending set. { w := httptest.NewRecorder() s.handleMischiefPending(w, jsonReq(t, "GET", "/api/mischief/pending", "tok", nil)) var again []storage.MischiefOrder _ = json.Unmarshal(w.Body.Bytes(), &again) if len(again) != 0 { t.Fatalf("placed order still polled: %d", len(again)) } } } func TestMischiefClaimUnknownGUIDis400(t *testing.T) { s := newStore(t) w := httptest.NewRecorder() s.handleMischiefClaim(w, jsonReq(t, "POST", "/api/mischief/claim", "tok", mischiefVerdict{GUID: "nope", Status: storage.MischiefPlaced})) if w.Code != 400 { t.Fatalf("unknown-guid claim = %d, want 400 (so gogobee parks it)", w.Code) } } func TestMischiefClaimBadVerdictIs400(t *testing.T) { s := newStore(t) if w := placeOrder(t, s, "reala", mischiefOrderReq{TargetToken: "tok-josie", Tier: "grunt"}); w.Code != 200 { t.Fatal("seed") } pending, _ := storage.PendingMischiefOrders(10) w := httptest.NewRecorder() s.handleMischiefClaim(w, jsonReq(t, "POST", "/api/mischief/claim", "tok", mischiefVerdict{GUID: pending[0].GUID, Status: "exploded"})) if w.Code != 400 { t.Fatalf("bad-verdict claim = %d, want 400", w.Code) } }