163 lines
5.2 KiB
Python
163 lines
5.2 KiB
Python
"""Integration tests for Wallet service and API endpoints.
|
|
|
|
docs/planning/economy.md US-1 (task 909e3e9a59af).
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
from sqlalchemy.pool import NullPool
|
|
|
|
from app.config import settings
|
|
from app.main import app
|
|
from app.models.wallet import TransactionType
|
|
from app.services.wallet import InsufficientFundsError, InvalidAmountError, wallet_service
|
|
|
|
|
|
def _register(client: TestClient, username: str) -> tuple[int, dict[str, str]]:
|
|
resp = client.post(
|
|
"/api/auth/register",
|
|
json={
|
|
"username": username,
|
|
"email": f"{username}@example.com",
|
|
"password": "secret123",
|
|
"date_of_birth": "1990-01-01",
|
|
},
|
|
)
|
|
assert resp.status_code == 201, resp.text
|
|
token = resp.json()["access_token"]
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
me = client.get("/api/users/me", headers=headers)
|
|
assert me.status_code == 200, me.text
|
|
return me.json()["id"], headers
|
|
|
|
|
|
def _session_factory():
|
|
engine = create_async_engine(settings.DATABASE_URL, poolclass=NullPool)
|
|
return engine, async_sessionmaker(engine, expire_on_commit=False)
|
|
|
|
|
|
def test_wallet_unauthorized():
|
|
client = TestClient(app)
|
|
resp = client.get("/api/wallet")
|
|
assert resp.status_code == 401
|
|
|
|
resp = client.get("/api/wallet/transactions")
|
|
assert resp.status_code == 401
|
|
|
|
|
|
def test_get_wallet_lazy_creation():
|
|
client = TestClient(app)
|
|
uid, headers = _register(client, "wallet_api_user_1")
|
|
|
|
resp = client.get("/api/wallet", headers=headers)
|
|
assert resp.status_code == 200, resp.text
|
|
data = resp.json()
|
|
assert data["user_id"] == uid
|
|
assert data["balance"] == 0
|
|
assert data["id"] is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wallet_service_credit_and_debit_flow():
|
|
client = TestClient(app)
|
|
uid, headers = _register(client, "wallet_api_user_2")
|
|
|
|
engine, sf = _session_factory()
|
|
try:
|
|
async with sf() as session:
|
|
# 1. Credit
|
|
wallet, tx = await wallet_service.credit(
|
|
session,
|
|
user_id=uid,
|
|
amount=1000,
|
|
tx_type=TransactionType.credit_purchase,
|
|
reference_type="test_ref",
|
|
reference_id="ref_1",
|
|
idempotency_key="credit_idemp_1",
|
|
)
|
|
assert wallet.balance == 1000
|
|
assert tx.amount == 1000
|
|
assert tx.balance_after == 1000
|
|
|
|
# 2. Idempotent re-credit does not increase balance
|
|
wallet2, tx2 = await wallet_service.credit(
|
|
session,
|
|
user_id=uid,
|
|
amount=1000,
|
|
tx_type=TransactionType.credit_purchase,
|
|
idempotency_key="credit_idemp_1",
|
|
)
|
|
assert wallet2.balance == 1000
|
|
assert tx2.id == tx.id
|
|
|
|
# 3. Debit
|
|
wallet3, tx3 = await wallet_service.debit(
|
|
session,
|
|
user_id=uid,
|
|
amount=400,
|
|
tx_type=TransactionType.marketplace_purchase,
|
|
reference_type="purchase",
|
|
reference_id="item_42",
|
|
idempotency_key="debit_idemp_1",
|
|
)
|
|
assert wallet3.balance == 600
|
|
assert tx3.amount == -400
|
|
assert tx3.balance_after == 600
|
|
|
|
# 4. Over-debit raises InsufficientFundsError
|
|
with pytest.raises(InsufficientFundsError):
|
|
await wallet_service.debit(
|
|
session,
|
|
user_id=uid,
|
|
amount=1000,
|
|
tx_type=TransactionType.marketplace_purchase,
|
|
)
|
|
|
|
# Balance remains 600
|
|
w_check = await wallet_service.get_or_create_wallet(session, uid)
|
|
assert w_check.balance == 600
|
|
finally:
|
|
await engine.dispose()
|
|
|
|
# Verify via API
|
|
resp = client.get("/api/wallet", headers=headers)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["balance"] == 600
|
|
|
|
resp_tx = client.get("/api/wallet/transactions", headers=headers)
|
|
assert resp_tx.status_code == 200
|
|
tx_data = resp_tx.json()
|
|
assert tx_data["total"] == 2
|
|
assert len(tx_data["transactions"]) == 2
|
|
# Newest first: debit (-400) first, then credit (+1000)
|
|
assert tx_data["transactions"][0]["amount"] == -400
|
|
assert tx_data["transactions"][1]["amount"] == 1000
|
|
|
|
|
|
def test_wallet_reward_endpoint():
|
|
client = TestClient(app)
|
|
uid, headers = _register(client, "wallet_reward_user")
|
|
|
|
# Initial balance is 0
|
|
resp = client.get("/api/wallet", headers=headers)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["balance"] == 0
|
|
|
|
# Reward 25 Buffalo Chips
|
|
reward_resp = client.post(
|
|
"/api/wallet/reward",
|
|
json={"amount": 25, "reason": "found_chest"},
|
|
headers=headers,
|
|
)
|
|
assert reward_resp.status_code == 200
|
|
data = reward_resp.json()
|
|
assert data["success"] is True
|
|
assert data["amount"] == 25
|
|
assert data["new_balance"] == 25
|
|
|
|
# Check updated wallet
|
|
resp = client.get("/api/wallet", headers=headers)
|
|
assert resp.json()["balance"] == 25
|
|
|