diff --git a/fifa17-recon/tools/test_market_buy.py b/fifa17-recon/tools/test_market_buy.py new file mode 100644 index 0000000..2acd5cc --- /dev/null +++ b/fifa17-recon/tools/test_market_buy.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Offline unit test for the transfer-market BUY flow (tools/utas_server.trade_route). + +Runs entirely against a TEMP profile (FUT_PROFILE) so it never mutates the real +save. Verifies: buy-now deducts coins + grants the won card to the club + echoes +a CLOSED auction with the validated record shape; insufficient funds -> 461. +No server / no live FIFA needed. Run: python3 tools/test_market_buy.py +""" +import os, json, sys, tempfile + +os.environ["FUT_PROFILE"] = os.path.join(tempfile.gettempdir(), "fut_buy_test_profile.json") +if os.path.exists(os.environ["FUT_PROFILE"]): + os.remove(os.environ["FUT_PROFILE"]) + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import utas_server as u # noqa: E402 + + +class FakeH: + def __init__(self, cmd, path, body=b""): + self.command, self.path, self._body = cmd, path, body + + +def main(): + fails = [] + + def ok(name, cond, detail=""): + if not cond: + fails.append(f"{name}: {detail}") + + # a listing to buy (cheapest affordable) + _, mkt = u.auctionhouse_route(FakeH("GET", "/ut/game/fifa17/auctionhouse?type=player")) + recs = sorted(mkt["auctionInfo"], key=lambda r: r["buyNowPrice"]) + rec0 = recs[0] + tid, price = rec0["tradeId"], rec0["buyNowPrice"] + coins0, items0 = u.STORE.coins(), len(u.STORE.items()) + ok("test setup: affordable listing exists", price <= coins0, f"price {price} coins {coins0}") + + # BUY NOW + code, resp = u.trade_route(FakeH("POST", f"/ut/game/fifa17/trade/{tid}/bid", + json.dumps({"bid": price}).encode())) + r = resp.get("auctionInfo", [{}])[0] + ok("buy returns 200", code == 200, str(code)) + ok("buy auction closed", r.get("tradeState") == "closed", r.get("tradeState")) + ok("buy bidState highest", r.get("bidState") == "highest", r.get("bidState")) + ok("coins deducted by buyNow", u.STORE.coins() == coins0 - price, + f"{coins0}->{u.STORE.coins()} price {price}") + ok("won card added to club", len(u.STORE.items()) == items0 + 1, + f"{items0}->{len(u.STORE.items())}") + ok("won itemData is object", isinstance(r.get("itemData"), dict)) + ok("won itemData.attributeList is array", isinstance(r.get("itemData", {}).get("attributeList"), list)) + ok("credits is int", isinstance(resp.get("credits"), int)) + + # GET view of an auction + _, v = u.trade_route(FakeH("GET", f"/ut/game/fifa17/trade/{tid}")) + ok("view auctionInfo is array", isinstance(v.get("auctionInfo"), list)) + + # insufficient funds -> 461, coins unchanged + u.STORE._p["coins"] = 10 + u.STORE._save() + c2, _ = u.trade_route(FakeH("POST", f"/ut/game/fifa17/trade/{tid}/bid", b'{"bid":999999}')) + ok("insufficient funds -> 461", c2 == 461, str(c2)) + ok("coins unchanged on failed buy", u.STORE.coins() == 10, str(u.STORE.coins())) + + os.remove(os.environ["FUT_PROFILE"]) + print(f"{'PASS' if not fails else 'FAIL'} - market buy flow " + f"({0 if fails else 'all'} checks; {len(fails)} failed)") + for f in fails: + print(" FAIL:", f) + return 1 if fails else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/fifa17-recon/tools/utas_server.py b/fifa17-recon/tools/utas_server.py index 08df166..f2c9576 100755 --- a/fifa17-recon/tools/utas_server.py +++ b/fifa17-recon/tools/utas_server.py @@ -365,6 +365,14 @@ def _market_auctions(limit=21): for i in range(min(limit, len(PACK_POOL)))] +def _auction_by_tradeid(tid): + # tradeId space is _TRADE_ID_BASE + index into PACK_POOL -> reconstruct the auction. + i = tid - _TRADE_ID_BASE + if 0 <= i < len(PACK_POOL): + return _auction_record(i, PACK_POOL[i]) + return None + + def _market_body(auctions): return {"auctionInfo": auctions, "credits": STORE.coins(), "total": len(auctions), "duplicateItemIdList": []} @@ -385,10 +393,37 @@ def auctionhouse_route(h): def trade_route(h): - # GET view one auction / POST place bid -> {auctionInfo:[record], credits}. - # Echo a sample record so a viewed/bid auction resolves. - rec = _market_auctions(1) - return 200, {"auctionInfo": rec, "credits": STORE.coins()} + # GET view one auction -> {auctionInfo:[record], credits} + # POST/PUT place bid / buy-now -> stateful: on buy-now (bid >= buyNowPrice) deduct + # coins, grant the won card to the club, echo the CLOSED auction. Reuses the + # validated auction-record shape (0x18013e410) so it's freeze-safe; whether FIFA + # surfaces the won item post-buy is functional (needs live test). FutISOfferTrade. + m = re.search(r"/trade/(\d+)", h.path) + tid = int(m.group(1)) if m else -1 + rec = _auction_by_tradeid(tid) + if h.command in ("POST", "PUT"): + try: + body = json.loads(h._body.decode()) if getattr(h, "_body", b"") else {} + except Exception: + body = {} + if rec is None: + return 200, {"auctionInfo": [], "credits": STORE.coins()} + bid = body.get("bid") or rec["buyNowPrice"] + if bid >= rec["buyNowPrice"]: # BUY NOW + if not STORE.spend(rec["buyNowPrice"]): + return 461, {"reason": "insufficient_coins", "credits": STORE.coins()} + won = dict(rec["itemData"]); won.pop("id", None) + won["itemState"] = "free" # unassigned/won + granted = STORE.add_items([won])[0] + rec = dict(rec) + rec.update({"tradeState": "closed", "bidState": "highest", + "currentBid": rec["buyNowPrice"], "itemData": granted}) + log(" MARKET: bought tradeId %d for %d, coins=%d, card->club" + % (tid, rec["currentBid"], STORE.coins())) + else: # simple bid (we're sole bidder) + rec = dict(rec); rec.update({"currentBid": bid, "bidState": "highest"}) + return 200, {"auctionInfo": [rec], "credits": STORE.coins()} + return 200, {"auctionInfo": [rec] if rec else [], "credits": STORE.coins()} def tradepile_route(h):