stepstead-backend/tests/econ_e2e.py
jts ea049b8ccd İlk commit: Stepstead backend (FastAPI)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:30:24 +03:00

227 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Ekonomi + günlük + yerel chat E2E — canlı backend'e karşı.
Çalıştırma: cd /opt/stepstead && ./venv/bin/python3 <bu dosya>
"""
import asyncio
import json
import sys
sys.path.insert(0, "/opt/stepstead")
import asyncpg
import httpx
import websockets
from config import settings
from security import create_access_token
BASE = "http://127.0.0.1:8002"
WS = "ws://127.0.0.1:8002/ws"
DSN = settings.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql://")
fails = 0
def check(ok: bool, what: str) -> None:
global fails
print(("" if ok else "") + what)
if not ok:
fails += 1
async def main() -> None:
conn = await asyncpg.connect(DSN)
uid_a = await conn.fetchval("SELECT id FROM users WHERE email='jts@jts.rocks'")
uid_b = await conn.fetchval("SELECT id FROM users WHERE email='wstest@stepstead.local'")
cid_a = await conn.fetchval("SELECT id FROM characters WHERE user_id=$1", uid_a)
cid_b = await conn.fetchval("SELECT id FROM characters WHERE user_id=$1", uid_b)
# Hazırlık: ikisi de pazarda, bekleyen olay yok, bakiye/gold garanti, temiz ilanlar
await conn.execute("""
UPDATE characters SET pending_event=NULL,
location_id=(SELECT id FROM locations WHERE code='pazar'),
step_balance=GREATEST(step_balance,3000), gold=GREATEST(gold,500)
WHERE id = ANY($1::bigint[])""", [cid_a, cid_b])
await conn.execute("UPDATE market_listings SET status='cancelled', closed_at=now() WHERE status='open'")
# A'ya satılık 5 nane
mint = await conn.fetchval("SELECT id FROM items WHERE code='herb_mint'")
slot = await conn.fetchval("SELECT COALESCE(MAX(slot)+1,0) FROM inventory WHERE character_id=$1", cid_a)
await conn.execute("""
INSERT INTO inventory (character_id,item_id,qty,slot)
VALUES ($1,$2,10,$3)""", cid_a, mint, slot)
# Günlük test için A'nın günlük durumunu sıfırla
await conn.execute("""
UPDATE characters SET daily_steps=0, daily_date=NULL, streak_days=0, last_claim_date=NULL
WHERE id=$1""", cid_a)
tok_a, tok_b = create_access_token(uid_a), create_access_token(uid_b)
ha = {"Authorization": f"Bearer {tok_a}"}
hb = {"Authorization": f"Bearer {tok_b}"}
async with httpx.AsyncClient(base_url=BASE, timeout=15) as cl:
print("== Market ==")
r = await cl.post("/market/list", headers=ha,
json={"item_code": "herb_mint", "qty": 5, "unit_price": 3})
check(r.status_code == 200, f"A ilan verdi ({r.status_code} {r.text[:80]})")
lid = r.json().get("listing_id")
r = await cl.get("/market/listings", headers=hb)
check(r.status_code == 200 and any(x["id"] == lid for x in r.json()), "B ilanı listede gördü")
gold_b0 = (await cl.get("/character/me", headers=hb)).json()["gold"]
r = await cl.post("/market/buy", headers=hb, json={"listing_id": lid, "qty": 2})
check(r.status_code == 200 and r.json()["total_price"] == 6, f"B kısmi satın aldı ({r.text[:80]})")
check(r.json()["gold"] == gold_b0 - 6, "B gold düştü")
gold_a = (await cl.get("/character/me", headers=ha)).json()["gold"]
r = await cl.post("/market/buy", headers=ha, json={"listing_id": lid})
check(r.status_code == 400, "A kendi ilanını alamadı")
r = await cl.post("/market/cancel", headers=ha, json={"listing_id": lid})
check(r.status_code == 200, "A kalan ilanı iptal etti (item geri)")
r = await cl.post("/market/vendor/sell", headers=ha, json={"item_code": "herb_mint", "qty": 2})
check(r.status_code == 200 and r.json()["gold_gained"] == 10,
f"vendor'a satış 2×5 gold ({r.text[:80]})")
# Pazar dışında market engeli
koy = await conn.fetchval("SELECT id FROM locations WHERE code='koy'")
await conn.execute("UPDATE characters SET location_id=$1 WHERE id=$2", koy, cid_a)
r = await cl.post("/market/vendor/sell", headers=ha, json={"item_code": "herb_mint", "qty": 1})
check(r.status_code == 400, "pazar dışında satış reddedildi")
await conn.execute("UPDATE characters SET location_id=(SELECT id FROM locations WHERE code='pazar') WHERE id=$1", cid_a)
print("== Günlük hedef ==")
r = await cl.get("/world/daily", headers=ha)
d = r.json()
check(r.status_code == 200 and d["today_steps"] == 0 and not d["claimable"], f"daily başlangıç ({r.text[:100]})")
r = await cl.post("/world/daily/claim", headers=ha)
check(r.status_code == 400, "hedef dolmadan claim reddedildi")
await cl.post("/world/debug/add-steps", headers=ha, json={"steps": 3000})
r = await cl.get("/world/daily", headers=ha)
check(r.json()["claimable"], "3000 adım sonrası claimable")
r = await cl.post("/world/daily/claim", headers=ha)
d = r.json()
check(r.status_code == 200 and d["streak_days"] == 1 and d["bonus_steps"] == 250,
f"claim: streak 1, +250 adım +5 gold ({r.text[:100]})")
r = await cl.post("/world/daily/claim", headers=ha)
check(r.status_code == 400, "aynı gün ikinci claim reddedildi")
print("== Lokasyona özel olay ==")
await conn.execute("UPDATE roads SET event_chance=1.0")
await conn.execute("""
UPDATE characters SET location_id=(SELECT id FROM locations WHERE code='cayirlar'),
pending_event=NULL, step_balance=GREATEST(step_balance,3000) WHERE id=$1""", cid_a)
seen = set()
got_targeted = False
for _ in range(12):
r = await cl.post("/world/travel", headers=ha, json={"to_code": "av_topraklari"})
if r.status_code != 200:
break
ev = r.json().get("event")
if ev:
seen.add(ev["code"])
row = await conn.fetchval("SELECT location_codes FROM events WHERE code=$1", ev["code"])
if row and "av_topraklari" in row:
got_targeted = True
rr = await cl.post("/world/event/resolve", headers=ha, json={"choice": 0})
if rr.status_code != 200:
check(False, f"resolve başarısız: {rr.text[:100]}")
break
# geri dön
r = await cl.post("/world/travel", headers=ha, json={"to_code": "cayirlar"})
if r.status_code == 200 and r.json().get("event"):
await cl.post("/world/event/resolve", headers=ha, json={"choice": 0})
if got_targeted:
break
check(got_targeted, f"av_topraklari'na özel olay çıktı (görülen: {seen})")
await conn.execute("UPDATE roads SET event_chance=0.3")
print("== Gold olay ödülü (korsan) ==")
# korsan_yagmasi'ni zorla pending yap, choice 0 çöz — gold alanı dönmeli
ev = await conn.fetchrow("SELECT code, name, text, choices FROM events WHERE code='korsan_yagmasi'")
choices_client = [{"text": c["text"]} for c in json.loads(ev["choices"])]
payload = {"code": ev["code"], "name": ev["name"], "text": ev["text"], "choices": choices_client}
await conn.execute("UPDATE characters SET pending_event=$1::jsonb WHERE id=$2", json.dumps(payload), cid_a)
r = await cl.post("/world/event/resolve", headers=ha, json={"choice": 0})
d = r.json()
check(r.status_code == 200 and "gold" in d, f"resolve gold alanı var (delta={d.get('gold_delta')}, check={d.get('skill_check', {}).get('success')})")
print("== Yerel chat ==")
# A pazar'da, B pazar'da → local mesaj B'ye ulaşmalı; B koy'a gidince ulaşmamalı
await conn.execute("""
UPDATE characters SET location_id=(SELECT id FROM locations WHERE code='pazar'),
pending_event=NULL WHERE id = ANY($1::bigint[])""", [cid_a, cid_b])
async with websockets.connect(f"{WS}?token={tok_a}") as wa, \
websockets.connect(f"{WS}?token={tok_b}") as wb:
# açılış mesajlarını tüket (online_list + chat_history)
for w in (wa, wb):
for _ in range(2):
await asyncio.wait_for(w.recv(), 5)
await wa.send(json.dumps({"type": "chat", "channel": "local", "text": "yerel selam"}))
got = None
try:
while True:
m = json.loads(await asyncio.wait_for(wb.recv(), 5))
if m.get("type") == "chat":
got = m
break
except asyncio.TimeoutError:
pass
check(got is not None and got["channel"] == "local" and got["text"] == "yerel selam",
f"aynı lokasyonda yerel chat ulaştı ({got})")
# yerel history isteği
await wb.send(json.dumps({"type": "get_local_history"}))
hist = None
try:
while True:
m = json.loads(await asyncio.wait_for(wb.recv(), 5))
if m.get("type") == "chat_history" and m.get("channel") == "local":
hist = m
break
except asyncio.TimeoutError:
pass
check(hist is not None and any(x["text"] == "yerel selam" for x in hist["messages"]),
"yerel chat history döndü")
# B başka lokasyona gitsin → A'nın yerel mesajı B'ye GİTMEMELİ
r = await cl.post("/world/travel", headers=hb, json={"to_code": "koy"})
if r.status_code == 200 and r.json().get("event"):
await cl.post("/world/event/resolve", headers=hb, json={"choice": 0})
await asyncio.sleep(0.5)
# B'nin kuyruğunu boşalt (player_moved vs.)
try:
while True:
await asyncio.wait_for(wb.recv(), 0.5)
except asyncio.TimeoutError:
pass
await asyncio.sleep(1.1) # flood koruması
await wa.send(json.dumps({"type": "chat", "channel": "local", "text": "bunu gorme"}))
leak = None
try:
while True:
m = json.loads(await asyncio.wait_for(wb.recv(), 3))
if m.get("type") == "chat" and m.get("channel") == "local":
leak = m
break
except asyncio.TimeoutError:
pass
check(leak is None, "farklı lokasyona yerel chat sızmadı")
# global hâlâ çalışıyor
await asyncio.sleep(1.1)
await wa.send(json.dumps({"type": "chat", "text": "global selam"}))
gg = None
try:
while True:
m = json.loads(await asyncio.wait_for(wb.recv(), 5))
if m.get("type") == "chat" and m.get("channel") == "global":
gg = m
break
except asyncio.TimeoutError:
pass
check(gg is not None and gg["text"] == "global selam", "global chat farklı lokasyona ulaştı")
await conn.close()
print(f"== SONUÇ: {'BAŞARILI' if fails == 0 else 'BAŞARISIZ'} ({fails} hata) ==")
sys.exit(1 if fails else 0)
asyncio.run(main())