forked from sass/tipibot
Added tests, fix README.
This commit is contained in:
204
tests/test_economy_flows.py
Normal file
204
tests/test_economy_flows.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""Tests for the async economy flows against the in-memory PocketBase fake."""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from core import economy
|
||||
|
||||
from conftest import run
|
||||
|
||||
UID = 111
|
||||
OTHER = 222
|
||||
HOUSE = 999
|
||||
|
||||
|
||||
def _fixed_now(monkeypatch, dt: datetime):
|
||||
monkeypatch.setattr(economy, "_now", lambda: dt)
|
||||
return dt
|
||||
|
||||
|
||||
class TestGetUser:
|
||||
def test_creates_default_record(self, fake_pb):
|
||||
user = run(economy.get_user(UID))
|
||||
assert user["balance"] == 0
|
||||
assert fake_pb.record_for(UID)["user_id"] == str(UID)
|
||||
|
||||
def test_roundtrips_existing_data(self, fake_pb):
|
||||
run(economy.get_user(UID))
|
||||
fake_pb.record_for(UID)["balance"] = 1234
|
||||
assert run(economy.get_user(UID))["balance"] == 1234
|
||||
|
||||
|
||||
class TestDaily:
|
||||
def test_first_claim(self, fake_pb):
|
||||
res = run(economy.do_daily(UID))
|
||||
assert res["ok"] and res["streak"] == 1 and res["earned"] == 150
|
||||
|
||||
def test_cooldown_blocks_second_claim(self, fake_pb):
|
||||
run(economy.do_daily(UID))
|
||||
res = run(economy.do_daily(UID))
|
||||
assert not res["ok"] and res["reason"] == "cooldown"
|
||||
|
||||
def test_streak_increments_next_day(self, fake_pb, monkeypatch):
|
||||
t0 = _fixed_now(monkeypatch, datetime(2026, 7, 25, 12, tzinfo=timezone.utc))
|
||||
run(economy.do_daily(UID))
|
||||
_fixed_now(monkeypatch, t0 + timedelta(days=1))
|
||||
res = run(economy.do_daily(UID))
|
||||
assert res["ok"] and res["streak"] == 2
|
||||
|
||||
def test_streak_resets_after_missed_day(self, fake_pb, monkeypatch):
|
||||
t0 = _fixed_now(monkeypatch, datetime(2026, 7, 25, 12, tzinfo=timezone.utc))
|
||||
run(economy.do_daily(UID))
|
||||
_fixed_now(monkeypatch, t0 + timedelta(days=3))
|
||||
res = run(economy.do_daily(UID))
|
||||
assert res["ok"] and res["streak"] == 1
|
||||
|
||||
def test_karikas_preserves_streak(self, fake_pb, monkeypatch):
|
||||
t0 = _fixed_now(monkeypatch, datetime(2026, 7, 25, 12, tzinfo=timezone.utc))
|
||||
run(economy.do_daily(UID))
|
||||
fake_pb.record_for(UID)["items"] = ["karikas"]
|
||||
fake_pb.record_for(UID)["daily_streak"] = 5
|
||||
_fixed_now(monkeypatch, t0 + timedelta(days=3))
|
||||
res = run(economy.do_daily(UID))
|
||||
assert res["ok"] and res["streak"] == 5
|
||||
|
||||
def test_streak_multiplier_tiers(self, fake_pb, monkeypatch):
|
||||
t0 = _fixed_now(monkeypatch, datetime(2026, 7, 25, 12, tzinfo=timezone.utc))
|
||||
run(economy.get_user(UID))
|
||||
rec = fake_pb.record_for(UID)
|
||||
rec["daily_streak"] = 13
|
||||
rec["last_streak_date"] = (t0.date() - timedelta(days=1)).isoformat()
|
||||
res = run(economy.do_daily(UID))
|
||||
assert res["streak"] == 14 and res["streak_mult"] == 3.0 and res["earned"] == 450
|
||||
|
||||
|
||||
class TestBuy:
|
||||
def test_insufficient_funds(self, fake_pb):
|
||||
res = run(economy.do_buy(UID, "gaming_hiir"))
|
||||
assert not res["ok"] and res["reason"] == "insufficient"
|
||||
|
||||
def test_purchase_and_rebuy_blocked(self, fake_pb):
|
||||
run(economy.get_user(UID))
|
||||
fake_pb.record_for(UID)["balance"] = 1000
|
||||
res = run(economy.do_buy(UID, "gaming_hiir"))
|
||||
assert res["ok"] and res["balance"] == 500
|
||||
assert "gaming_hiir" in fake_pb.record_for(UID)["items"]
|
||||
res = run(economy.do_buy(UID, "gaming_hiir"))
|
||||
assert not res["ok"] and res["reason"] == "owned"
|
||||
|
||||
def test_tier2_requires_level(self, fake_pb):
|
||||
run(economy.get_user(UID))
|
||||
fake_pb.record_for(UID)["balance"] = 100_000
|
||||
res = run(economy.do_buy(UID, "jellyfin"))
|
||||
assert not res["ok"] and res["reason"] == "level_required"
|
||||
fake_pb.record_for(UID)["exp"] = economy.exp_for_level(10)
|
||||
assert run(economy.do_buy(UID, "jellyfin"))["ok"]
|
||||
|
||||
def test_anticheat_repurchase_after_depletion(self, fake_pb):
|
||||
run(economy.get_user(UID))
|
||||
rec = fake_pb.record_for(UID)
|
||||
rec["balance"] = 10_000
|
||||
assert run(economy.do_buy(UID, "anticheat"))["ok"]
|
||||
assert fake_pb.record_for(UID)["item_uses"]["anticheat"] == 2
|
||||
fake_pb.record_for(UID)["item_uses"]["anticheat"] = 0
|
||||
assert run(economy.do_buy(UID, "anticheat"))["ok"]
|
||||
assert fake_pb.record_for(UID)["item_uses"]["anticheat"] == 2
|
||||
|
||||
|
||||
class TestGive:
|
||||
def test_transfer(self, fake_pb):
|
||||
run(economy.get_user(UID))
|
||||
fake_pb.record_for(UID)["balance"] = 500
|
||||
res = run(economy.do_give(UID, OTHER, 200))
|
||||
assert res["ok"]
|
||||
assert fake_pb.record_for(UID)["balance"] == 300
|
||||
assert fake_pb.record_for(OTHER)["balance"] == 200
|
||||
|
||||
def test_insufficient(self, fake_pb):
|
||||
res = run(economy.do_give(UID, OTHER, 50))
|
||||
assert not res["ok"] and res["reason"] == "insufficient"
|
||||
|
||||
def test_opposite_transfers_do_not_deadlock(self, fake_pb):
|
||||
async def both():
|
||||
for uid in (UID, OTHER):
|
||||
await economy.get_user(uid)
|
||||
fake_pb.record_for(uid)["balance"] = 100
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(
|
||||
economy.do_give(UID, OTHER, 10),
|
||||
economy.do_give(OTHER, UID, 25),
|
||||
),
|
||||
timeout=5,
|
||||
)
|
||||
run(both())
|
||||
total = fake_pb.record_for(UID)["balance"] + fake_pb.record_for(OTHER)["balance"]
|
||||
assert total == 200
|
||||
|
||||
|
||||
class TestRob:
|
||||
def test_anticheat_blocks_and_depletes(self, fake_pb):
|
||||
run(economy.get_user(UID))
|
||||
run(economy.get_user(OTHER))
|
||||
economy.set_house(HOUSE)
|
||||
fake_pb.record_for(UID)["balance"] = 1000
|
||||
target = fake_pb.record_for(OTHER)
|
||||
target["balance"] = 1000
|
||||
target["items"] = ["anticheat"]
|
||||
target["item_uses"] = {"anticheat": 1}
|
||||
res = run(economy.do_rob(UID, OTHER))
|
||||
assert res["ok"] and not res["success"] and res["reason"] == "valvur"
|
||||
assert fake_pb.record_for(UID)["balance"] == 1000 - res["fine"]
|
||||
assert "anticheat" not in fake_pb.record_for(OTHER)["items"]
|
||||
# the fine flows to the house
|
||||
assert fake_pb.record_for(HOUSE)["balance"] == res["fine"]
|
||||
|
||||
|
||||
class TestGambling:
|
||||
def test_roulette_conserves_money_with_house(self, fake_pb):
|
||||
economy.set_house(HOUSE)
|
||||
run(economy.get_user(UID))
|
||||
fake_pb.record_for(UID)["balance"] = 1000
|
||||
random.seed(3)
|
||||
res = run(economy.do_roulette(UID, 100, "punane"))
|
||||
assert res["ok"]
|
||||
user_bal = fake_pb.record_for(UID)["balance"]
|
||||
if res["won"]:
|
||||
assert user_bal == 1000 + res["change"]
|
||||
else:
|
||||
assert user_bal == 900
|
||||
assert fake_pb.record_for(HOUSE)["balance"] == 100
|
||||
|
||||
def test_bet_larger_than_balance_rejected(self, fake_pb):
|
||||
res = run(economy.do_slots(UID, 50))
|
||||
assert not res["ok"] and res["reason"] == "insufficient"
|
||||
|
||||
def test_blackjack_bet_and_payout(self, fake_pb):
|
||||
economy.set_house(HOUSE)
|
||||
run(economy.get_user(UID))
|
||||
fake_pb.record_for(UID)["balance"] = 500
|
||||
assert run(economy.do_blackjack_bet(UID, 100))["ok"]
|
||||
assert fake_pb.record_for(UID)["balance"] == 400
|
||||
# player loses: payout 0 of 100 invested -> house gains the bet
|
||||
run(economy.do_blackjack_payout(UID, 0, total_invested=100))
|
||||
assert fake_pb.record_for(UID)["balance"] == 400
|
||||
assert fake_pb.record_for(HOUSE)["balance"] == 100
|
||||
|
||||
|
||||
class TestConcurrency:
|
||||
"""The per-user locks must serialize read-modify-write cycles."""
|
||||
|
||||
def test_concurrent_exp_awards_are_not_lost(self, fake_pb):
|
||||
async def hammer():
|
||||
await asyncio.gather(*(economy.award_exp(UID, 10) for _ in range(25)))
|
||||
run(hammer())
|
||||
assert fake_pb.record_for(UID)["exp"] == 250
|
||||
|
||||
def test_concurrent_credit_house_is_atomic(self, fake_pb):
|
||||
economy.set_house(HOUSE)
|
||||
|
||||
async def hammer():
|
||||
await economy.get_user(HOUSE)
|
||||
await asyncio.gather(*(economy._credit_house(7) for _ in range(30)))
|
||||
run(hammer())
|
||||
assert fake_pb.record_for(HOUSE)["balance"] == 210
|
||||
Reference in New Issue
Block a user