227 lines
11 KiB
Python
227 lines
11 KiB
Python
"""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())
|