Implement wallet and transaction ledger models and Alembic migration (task 8343bc0a730f)
This commit is contained in:
@@ -58,6 +58,8 @@ TEST_TABLES = (
|
||||
# (not CASCADE, deliberately -- see groups.py), so it wouldn't be reached
|
||||
# by cascading a truncate of users; list it explicitly for clarity too.
|
||||
"group_members", "groups",
|
||||
# Economy (app/models/wallet.py) -- transactions FK wallets FK users
|
||||
"transactions", "wallets",
|
||||
"sessions", "refresh_tokens", "users",
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"""Model-level integration tests for Wallets and Transaction ledger.
|
||||
|
||||
docs/planning/economy.md US-1 (task 8343bc0a730f).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
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 Wallet, Transaction, TransactionType
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
def _register(client: TestClient, username: str) -> int:
|
||||
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
|
||||
headers = {"Authorization": f"Bearer {resp.json()['access_token']}"}
|
||||
me = client.get("/api/users/me", headers=headers)
|
||||
assert me.status_code == 200, me.text
|
||||
return me.json()["id"]
|
||||
|
||||
|
||||
def _session_factory():
|
||||
engine = create_async_engine(settings.DATABASE_URL, poolclass=NullPool)
|
||||
return engine, async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_creation_and_defaults():
|
||||
client = TestClient(app)
|
||||
uid = _register(client, "wallet_user_1")
|
||||
engine, sf = _session_factory()
|
||||
try:
|
||||
async with sf() as session:
|
||||
wallet = Wallet(user_id=uid)
|
||||
session.add(wallet)
|
||||
await session.commit()
|
||||
await session.refresh(wallet)
|
||||
|
||||
assert wallet.id is not None
|
||||
assert wallet.user_id == uid
|
||||
assert wallet.balance == 0
|
||||
assert wallet.created_at is not None
|
||||
assert wallet.updated_at is not None
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_unique_user_id():
|
||||
client = TestClient(app)
|
||||
uid = _register(client, "wallet_user_2")
|
||||
engine, sf = _session_factory()
|
||||
try:
|
||||
async with sf() as session:
|
||||
w1 = Wallet(user_id=uid)
|
||||
session.add(w1)
|
||||
await session.commit()
|
||||
|
||||
async with sf() as session:
|
||||
w2 = Wallet(user_id=uid)
|
||||
session.add(w2)
|
||||
with pytest.raises(IntegrityError):
|
||||
await session.commit()
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_non_negative_balance_constraint():
|
||||
client = TestClient(app)
|
||||
uid = _register(client, "wallet_user_3")
|
||||
engine, sf = _session_factory()
|
||||
try:
|
||||
async with sf() as session:
|
||||
w = Wallet(user_id=uid, balance=-50)
|
||||
session.add(w)
|
||||
with pytest.raises(IntegrityError):
|
||||
await session.commit()
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transaction_ledger_and_idempotency():
|
||||
client = TestClient(app)
|
||||
uid = _register(client, "wallet_user_4")
|
||||
engine, sf = _session_factory()
|
||||
try:
|
||||
async with sf() as session:
|
||||
wallet = Wallet(user_id=uid, balance=500)
|
||||
session.add(wallet)
|
||||
await session.commit()
|
||||
await session.refresh(wallet)
|
||||
|
||||
tx1 = Transaction(
|
||||
wallet_id=wallet.id,
|
||||
type=TransactionType.credit_purchase,
|
||||
amount=500,
|
||||
balance_after=500,
|
||||
reference_type="purchase_session",
|
||||
reference_id="sess_123",
|
||||
idempotency_key="idemp_key_001",
|
||||
metadata_json={"payment_method": "simulated"},
|
||||
)
|
||||
session.add(tx1)
|
||||
await session.commit()
|
||||
await session.refresh(tx1)
|
||||
|
||||
assert tx1.id is not None
|
||||
assert tx1.type == TransactionType.credit_purchase
|
||||
assert tx1.amount == 500
|
||||
assert tx1.balance_after == 500
|
||||
|
||||
# Duplicate idempotency_key must raise IntegrityError
|
||||
async with sf() as session:
|
||||
tx2 = Transaction(
|
||||
wallet_id=wallet.id,
|
||||
type=TransactionType.credit_purchase,
|
||||
amount=500,
|
||||
balance_after=1000,
|
||||
idempotency_key="idemp_key_001",
|
||||
)
|
||||
session.add(tx2)
|
||||
with pytest.raises(IntegrityError):
|
||||
await session.commit()
|
||||
finally:
|
||||
await engine.dispose()
|
||||
Reference in New Issue
Block a user