"""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