Spaces:
Sleeping
Sleeping
| """ | |
| Test suite for Credit Transaction Manager | |
| Tests all credit transaction operations including: | |
| - Reserve credits | |
| - Confirm credits | |
| - Refund credits | |
| - Add credits (purchases) | |
| - Balance verification | |
| - Transaction history | |
| """ | |
| import pytest | |
| import uuid | |
| from datetime import datetime | |
| from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker | |
| from sqlalchemy.pool import StaticPool | |
| from core.models import Base, User, CreditTransaction | |
| from services.credit_service.transaction_manager import ( | |
| CreditTransactionManager, | |
| InsufficientCreditsError, | |
| TransactionNotFoundError, | |
| UserNotFoundError | |
| ) | |
| # Test database setup | |
| TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:" | |
| async def engine(): | |
| """Create test database engine.""" | |
| engine = create_async_engine( | |
| TEST_DATABASE_URL, | |
| connect_args={"check_same_thread": False}, | |
| poolclass=StaticPool, | |
| ) | |
| async with engine.begin() as conn: | |
| await conn.run_sync(Base.metadata.create_all) | |
| yield engine | |
| async with engine.begin() as conn: | |
| await conn.run_sync(Base.metadata.drop_all) | |
| await engine.dispose() | |
| async def session(engine): | |
| """Create test database session.""" | |
| async_session = async_sessionmaker( | |
| engine, class_=AsyncSession, expire_on_commit=False | |
| ) | |
| async with async_session() as session: | |
| yield session | |
| async def test_user(session): | |
| """Create a test user with 100 credits.""" | |
| user = User( | |
| user_id=f"test_{uuid.uuid4().hex[:8]}", | |
| email=f"test_{uuid.uuid4().hex[:8]}@example.com", | |
| credits=100, | |
| is_active=True | |
| ) | |
| session.add(user) | |
| await session.commit() | |
| await session.refresh(user) | |
| return user | |
| # ============================================================================= | |
| # Reserve Credits Tests | |
| # ============================================================================= | |
| async def test_reserve_credits_success(session, test_user): | |
| """Test successfully reserving credits.""" | |
| initial_balance = test_user.credits | |
| transaction = await CreditTransactionManager.reserve_credits( | |
| session=session, | |
| user=test_user, | |
| amount=10, | |
| source="test", | |
| reference_type="test", | |
| reference_id="test_123", | |
| reason="Test reservation" | |
| ) | |
| await session.commit() | |
| await session.refresh(test_user) | |
| # Verify transaction | |
| assert transaction.transaction_type == "reserve" | |
| assert transaction.amount == -10 | |
| assert transaction.balance_before == initial_balance | |
| assert transaction.balance_after == initial_balance - 10 | |
| assert transaction.user_id == test_user.id | |
| assert transaction.source == "test" | |
| # Verify user balance | |
| assert test_user.credits == initial_balance - 10 | |
| async def test_reserve_credits_insufficient_funds(session, test_user): | |
| """Test reserving more credits than available.""" | |
| test_user.credits = 5 | |
| await session.commit() | |
| with pytest.raises(InsufficientCreditsError): | |
| await CreditTransactionManager.reserve_credits( | |
| session=session, | |
| user=test_user, | |
| amount=10, | |
| source="test", | |
| reference_type="test", | |
| reference_id="test_123" | |
| ) | |
| # Balance should be unchanged | |
| await session.refresh(test_user) | |
| assert test_user.credits == 5 | |
| async def test_reserve_credits_exact_amount(session, test_user): | |
| """Test reserving exact credit balance.""" | |
| test_user.credits = 10 | |
| await session.commit() | |
| transaction = await CreditTransactionManager.reserve_credits( | |
| session=session, | |
| user=test_user, | |
| amount=10, | |
| source="test", | |
| reference_type="test", | |
| reference_id="test_123" | |
| ) | |
| await session.commit() | |
| await session.refresh(test_user) | |
| assert test_user.credits == 0 | |
| assert transaction.balance_after == 0 | |
| # ============================================================================= | |
| # Confirm Credits Tests | |
| # ============================================================================= | |
| async def test_confirm_credits_success(session, test_user): | |
| """Test confirming reserved credits.""" | |
| # First reserve credits | |
| reserve_tx = await CreditTransactionManager.reserve_credits( | |
| session=session, | |
| user=test_user, | |
| amount=10, | |
| source="test", | |
| reference_type="test", | |
| reference_id="test_123" | |
| ) | |
| await session.commit() | |
| # Then confirm | |
| confirm_tx = await CreditTransactionManager.confirm_credits( | |
| session=session, | |
| transaction_id=reserve_tx.transaction_id, | |
| metadata={"status": "success"} | |
| ) | |
| await session.commit() | |
| # Verify confirmation | |
| assert confirm_tx.transaction_type == "confirm" | |
| assert confirm_tx.amount == 0 # No balance change | |
| assert confirm_tx.user_id == test_user.id | |
| assert confirm_tx.metadata["original_transaction_id"] == reserve_tx.transaction_id | |
| async def test_confirm_credits_nonexistent_transaction(session, test_user): | |
| """Test confirming a non-existent transaction.""" | |
| with pytest.raises(TransactionNotFoundError): | |
| await CreditTransactionManager.confirm_credits( | |
| session=session, | |
| transaction_id="nonexistent_tx_id" | |
| ) | |
| # ============================================================================= | |
| # Refund Credits Tests | |
| # ============================================================================= | |
| async def test_refund_credits_success(session, test_user): | |
| """Test refunding reserved credits.""" | |
| initial_balance = test_user.credits | |
| # Reserve credits | |
| reserve_tx = await CreditTransactionManager.reserve_credits( | |
| session=session, | |
| user=test_user, | |
| amount=10, | |
| source="test", | |
| reference_type="test", | |
| reference_id="test_123" | |
| ) | |
| await session.commit() | |
| await session.refresh(test_user) | |
| balance_after_reserve = test_user.credits | |
| assert balance_after_reserve == initial_balance - 10 | |
| # Refund | |
| refund_tx = await CreditTransactionManager.refund_credits( | |
| session=session, | |
| transaction_id=reserve_tx.transaction_id, | |
| reason="Test failed - refunding", | |
| metadata={"error": "test_error"} | |
| ) | |
| await session.commit() | |
| await session.refresh(test_user) | |
| # Verify refund | |
| assert refund_tx.transaction_type == "refund" | |
| assert refund_tx.amount == 10 # Positive for addition | |
| assert refund_tx.balance_before == balance_after_reserve | |
| assert refund_tx.balance_after == initial_balance | |
| assert test_user.credits == initial_balance | |
| async def test_refund_credits_nonexistent_transaction(session, test_user): | |
| """Test refunding a non-existent transaction.""" | |
| with pytest.raises(TransactionNotFoundError): | |
| await CreditTransactionManager.refund_credits( | |
| session=session, | |
| transaction_id="nonexistent_tx_id", | |
| reason="Test refund" | |
| ) | |
| # ============================================================================= | |
| # Add Credits Tests (Purchases) | |
| # ============================================================================= | |
| async def test_add_credits_success(session, test_user): | |
| """Test adding credits from purchase.""" | |
| initial_balance = test_user.credits | |
| transaction = await CreditTransactionManager.add_credits( | |
| session=session, | |
| user=test_user, | |
| amount=50, | |
| source="payment", | |
| reference_type="payment", | |
| reference_id="pay_123", | |
| reason="Purchase: 50 credits", | |
| metadata={"package_id": "basic"} | |
| ) | |
| await session.commit() | |
| await session.refresh(test_user) | |
| # Verify transaction | |
| assert transaction.transaction_type == "purchase" | |
| assert transaction.amount == 50 | |
| assert transaction.balance_before == initial_balance | |
| assert transaction.balance_after == initial_balance + 50 | |
| # Verify balance | |
| assert test_user.credits == initial_balance + 50 | |
| # ============================================================================= | |
| # Balance Verification Tests | |
| # ============================================================================= | |
| async def test_get_balance(session, test_user): | |
| """Test getting current balance.""" | |
| balance = await CreditTransactionManager.get_balance( | |
| session=session, | |
| user_id=test_user.id | |
| ) | |
| assert balance == test_user.credits | |
| async def test_get_balance_with_verification(session, test_user): | |
| """Test balance verification against transaction history.""" | |
| # Perform some transactions | |
| await CreditTransactionManager.reserve_credits( | |
| session=session, | |
| user=test_user, | |
| amount=10, | |
| source="test", | |
| reference_type="test", | |
| reference_id="test_1" | |
| ) | |
| await session.commit() | |
| await CreditTransactionManager.add_credits( | |
| session=session, | |
| user=test_user, | |
| amount=20, | |
| source="test", | |
| reference_type="test", | |
| reference_id="test_2" | |
| ) | |
| await session.commit() | |
| # Verify balance | |
| balance = await CreditTransactionManager.get_balance( | |
| session=session, | |
| user_id=test_user.id, | |
| verify=True | |
| ) | |
| await session.refresh(test_user) | |
| assert balance == test_user.credits | |
| async def test_get_balance_nonexistent_user(session): | |
| """Test getting balance for non-existent user.""" | |
| with pytest.raises(UserNotFoundError): | |
| await CreditTransactionManager.get_balance( | |
| session=session, | |
| user_id=99999 | |
| ) | |
| # ============================================================================= | |
| # Transaction History Tests | |
| # ============================================================================= | |
| async def test_get_transaction_history(session, test_user): | |
| """Test getting transaction history.""" | |
| # Create multiple transactions | |
| await CreditTransactionManager.reserve_credits( | |
| session=session, | |
| user=test_user, | |
| amount=10, | |
| source="test", | |
| reference_type="test", | |
| reference_id="test_1" | |
| ) | |
| await session.commit() | |
| await CreditTransactionManager.add_credits( | |
| session=session, | |
| user=test_user, | |
| amount=20, | |
| source="test", | |
| reference_type="test", | |
| reference_id="test_2" | |
| ) | |
| await session.commit() | |
| # Get history | |
| history = await CreditTransactionManager.get_transaction_history( | |
| session=session, | |
| user_id=test_user.id, | |
| limit=10 | |
| ) | |
| assert len(history) == 2 | |
| assert history[0].transaction_type in ["reserve", "purchase"] | |
| async def test_get_transaction_history_filtered(session, test_user): | |
| """Test getting filtered transaction history.""" | |
| # Create different transaction types | |
| await CreditTransactionManager.reserve_credits( | |
| session=session, | |
| user=test_user, | |
| amount=10, | |
| source="test", | |
| reference_type="test", | |
| reference_id="test_1" | |
| ) | |
| await session.commit() | |
| await CreditTransactionManager.add_credits( | |
| session=session, | |
| user=test_user, | |
| amount=20, | |
| source="payment", | |
| reference_type="payment", | |
| reference_id="pay_1" | |
| ) | |
| await session.commit() | |
| # Filter by purchase only | |
| history = await CreditTransactionManager.get_transaction_history( | |
| session=session, | |
| user_id=test_user.id, | |
| transaction_type="purchase" | |
| ) | |
| assert len(history) == 1 | |
| assert history[0].transaction_type == "purchase" | |
| # ============================================================================= | |
| # Integration Tests | |
| # ============================================================================= | |
| async def test_full_transaction_flow(session, test_user): | |
| """Test complete transaction flow: reserve β confirm.""" | |
| initial_balance = test_user.credits | |
| # Reserve | |
| reserve_tx = await CreditTransactionManager.reserve_credits( | |
| session=session, | |
| user=test_user, | |
| amount=10, | |
| source="middleware", | |
| reference_type="request", | |
| reference_id="POST:/api/endpoint" | |
| ) | |
| await session.commit() | |
| # Confirm | |
| confirm_tx = await CreditTransactionManager.confirm_credits( | |
| session=session, | |
| transaction_id=reserve_tx.transaction_id | |
| ) | |
| await session.commit() | |
| # Verify final state | |
| await session.refresh(test_user) | |
| assert test_user.credits == initial_balance - 10 | |
| # Verify transaction history | |
| history = await CreditTransactionManager.get_transaction_history( | |
| session=session, | |
| user_id=test_user.id | |
| ) | |
| assert len(history) == 2 | |
| assert history[1].transaction_type == "reserve" | |
| assert history[0].transaction_type == "confirm" | |
| async def test_full_refund_flow(session, test_user): | |
| """Test complete refund flow: reserve β refund.""" | |
| initial_balance = test_user.credits | |
| # Reserve | |
| reserve_tx = await CreditTransactionManager.reserve_credits( | |
| session=session, | |
| user=test_user, | |
| amount=10, | |
| source="middleware", | |
| reference_type="request", | |
| reference_id="POST:/api/endpoint" | |
| ) | |
| await session.commit() | |
| # Refund | |
| refund_tx = await CreditTransactionManager.refund_credits( | |
| session=session, | |
| transaction_id=reserve_tx.transaction_id, | |
| reason="Request failed" | |
| ) | |
| await session.commit() | |
| # Verify final state | |
| await session.refresh(test_user) | |
| assert test_user.credits == initial_balance # Back to original | |
| # Verify transaction history | |
| history = await CreditTransactionManager.get_transaction_history( | |
| session=session, | |
| user_id=test_user.id | |
| ) | |
| assert len(history) == 2 | |
| assert history[1].transaction_type == "reserve" | |
| assert history[0].transaction_type == "refund" | |