fifa17-python: commit working FUT backend deployment (client/server split)

Freeze the running offline FUT backend into version control as
fifa17-recon/docker/fifa17-python/ - declarative and rebuildable from a
fresh checkout:

* OPENFUT_BIND / OPENFUT_ADVERTISE client/server split in the responders
  (lsx, blaze, roster, utas, pow) + entrypoint.sh; OPENFUT_ADVERTISE is
  required for remote mode (compose and entrypoint fail without it)
* docker-compose.yml reproducing the frozen baseline container exactly
  (env, ports incl. the 8085->8080 POW-content remap, /state bind, restart)
* .env.example / .env for site config - the LAN IP is never hardcoded in source
* tools/ + data/ staged from openfut-fut-backend:python-baseline-2026-08-10,
  verified byte-identical to the running container at freeze time
* client_arm.sh (the 105 client-side arming counterpart)
* Dockerfile bakes /app/SHA256SUMS.txt so any image is self-identifying
* docs/BASELINE-python-2026-08-10.md: frozen image/container/hash record,
  restore instructions and rebuild-equivalence procedure

Secrets (redir key/cert, .env) and runtime state (docker/state) stay gitignored.
The live container is untouched pending the .105 launcher audit.
This commit is contained in:
root
2026-08-10 23:54:04 +00:00
parent edab23f04a
commit 70a64e3709
242 changed files with 175860 additions and 0 deletions
@@ -0,0 +1,92 @@
#!/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()))
# ---- SELL / list flow ----
owned_id = u.STORE.items()[0]["id"]
_, s = u.auctionhouse_route(FakeH("POST", "/ut/game/fifa17/auctionhouse",
json.dumps({"itemData": {"id": owned_id}, "startingBid": 1000, "buyNowPrice": 5000}).encode()))
ltid = s.get("id")
ok("list returns a tradeId", isinstance(ltid, int), repr(s))
_, tp = u.tradepile_route(FakeH("GET", "/ut/game/fifa17/tradePile"))
ok("tradePile shows the listing", tp.get("total") == 1, repr(tp.get("total")))
if tp.get("auctionInfo"):
rec = tp["auctionInfo"][0]
ok("listing tradeId matches", rec.get("tradeId") == ltid)
ok("listing buyNowPrice preserved", rec.get("buyNowPrice") == 5000)
ok("listing itemData is object", isinstance(rec.get("itemData"), dict))
ok("listing itemData.attributeList is array", isinstance(rec.get("itemData", {}).get("attributeList"), list))
u.delete_trade_route(FakeH("DELETE", f"/ut/delete/game/fifa17/trade/{ltid}"))
_, tp2 = u.tradepile_route(FakeH("GET", "/ut/game/fifa17/tradePile"))
ok("delist empties tradePile", tp2.get("total") == 0, repr(tp2.get("total")))
os.remove(os.environ["FUT_PROFILE"])
print(f"{'PASS' if not fails else 'FAIL'} - market buy+sell 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())