apigateway / tests /test_credit_transaction_manager.py
jebin2's picture
Add comprehensive test suite for credit service
a295e63
"""
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:"
@pytest.fixture
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()
@pytest.fixture
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
@pytest.fixture
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
# =============================================================================
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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
# =============================================================================
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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
# =============================================================================
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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)
# =============================================================================
@pytest.mark.asyncio
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
# =============================================================================
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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
@pytest.mark.asyncio
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
# =============================================================================
@pytest.mark.asyncio
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"]
@pytest.mark.asyncio
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
# =============================================================================
@pytest.mark.asyncio
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"
@pytest.mark.asyncio
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"