test_topup: use real migrated DB and current handler API

PaymentService construction now initializes crypto master keys, so the tests
need a real migrated SQLite DB rather than a Mock. Also align with the current
code: initiate_topup delegates to create_topup_intent/create_topup_order; the
Stripe webhook path requires amount in metadata, passes a metadata dict, and is
exercised by patching stripe.Webhook.construct_event; crypto deposits credit the
fiat wallet directly via SQL (assert the resulting balance) using the monitor's
own price service (returning a float for SQLite binding).
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 9ab81894
...@@ -5,8 +5,12 @@ import pytest ...@@ -5,8 +5,12 @@ import pytest
from decimal import Decimal from decimal import Decimal
from unittest.mock import Mock, patch, AsyncMock from unittest.mock import Mock, patch, AsyncMock
import tempfile
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from aisbf.database import DatabaseManager
from aisbf.payments.migrations import PaymentMigrations
from aisbf.payments.service import PaymentService from aisbf.payments.service import PaymentService
from aisbf.payments.wallet.manager import WalletManager from aisbf.payments.wallet.manager import WalletManager
...@@ -14,10 +18,19 @@ from aisbf.payments.wallet.manager import WalletManager ...@@ -14,10 +18,19 @@ from aisbf.payments.wallet.manager import WalletManager
_TEST_ENCRYPTION_KEY = Fernet.generate_key().decode() _TEST_ENCRYPTION_KEY = Fernet.generate_key().decode()
def _make_payment_db():
"""A real migrated SQLite DB. PaymentService construction now initializes
crypto master keys, so a plain Mock can't stand in for the database."""
path = tempfile.mktemp(suffix=".db")
db = DatabaseManager({'type': 'sqlite', 'sqlite_path': path})
PaymentMigrations(db).run_migrations()
return db
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_topup_amount_configuration(): async def test_topup_amount_configuration():
"""Test that supported top up amounts are properly configured""" """Test that supported top up amounts are properly configured"""
db = Mock() db = _make_payment_db()
config = {'encryption_key': _TEST_ENCRYPTION_KEY, 'base_url': 'http://localhost'} config = {'encryption_key': _TEST_ENCRYPTION_KEY, 'base_url': 'http://localhost'}
service = PaymentService(db, config) service = PaymentService(db, config)
...@@ -37,80 +50,87 @@ async def test_topup_amount_configuration(): ...@@ -37,80 +50,87 @@ async def test_topup_amount_configuration():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_initiate_stripe_topup(): async def test_initiate_stripe_topup():
"""Test initiating a Stripe top up payment""" """Test initiating a Stripe top up payment"""
db = Mock() db = _make_payment_db()
config = {'encryption_key': _TEST_ENCRYPTION_KEY, 'base_url': 'http://localhost'} config = {'encryption_key': _TEST_ENCRYPTION_KEY, 'base_url': 'http://localhost'}
service = PaymentService(db, config) service = PaymentService(db, config)
service.stripe_handler.create_payment_intent = AsyncMock(return_value={ # initiate_topup delegates to stripe_handler.create_topup_intent and returns
# its result verbatim.
service.stripe_handler.create_topup_intent = AsyncMock(return_value={
'success': True, 'success': True,
'client_secret': 'test_secret_123', 'client_secret': 'test_secret_123',
'payment_intent_id': 'pi_123' 'payment_intent_id': 'pi_123',
'amount': Decimal('20.00'),
'payment_method': 'stripe'
}) })
result = await service.initiate_topup( result = await service.initiate_topup(
user_id=123, user_id=123,
amount=Decimal('20.00'), amount=Decimal('20.00'),
payment_method='stripe', payment_method='stripe',
payment_method_id='pm_123' payment_method_id='pm_123'
) )
assert result['success'] is True assert result['success'] is True
assert 'client_secret' in result assert 'client_secret' in result
assert result['amount'] == Decimal('20.00') assert result['amount'] == Decimal('20.00')
assert result['payment_method'] == 'stripe' assert result['payment_method'] == 'stripe'
service.stripe_handler.create_payment_intent.assert_called_once() service.stripe_handler.create_topup_intent.assert_called_once()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_initiate_paypal_topup(): async def test_initiate_paypal_topup():
"""Test initiating a PayPal top up payment""" """Test initiating a PayPal top up payment"""
db = Mock() db = _make_payment_db()
config = {'encryption_key': _TEST_ENCRYPTION_KEY, 'base_url': 'http://localhost'} config = {'encryption_key': _TEST_ENCRYPTION_KEY, 'base_url': 'http://localhost'}
service = PaymentService(db, config) service = PaymentService(db, config)
service.paypal_handler.create_order = AsyncMock(return_value={ # initiate_topup delegates to paypal_handler.create_topup_order.
service.paypal_handler.create_topup_order = AsyncMock(return_value={
'success': True, 'success': True,
'order_id': 'test_order_123', 'order_id': 'test_order_123',
'approval_url': 'https://paypal.com/approve' 'approval_url': 'https://paypal.com/approve'
}) })
result = await service.initiate_topup( result = await service.initiate_topup(
user_id=123, user_id=123,
amount=Decimal('50.00'), amount=Decimal('50.00'),
payment_method='paypal' payment_method='paypal'
) )
assert result['success'] is True assert result['success'] is True
assert 'order_id' in result assert 'order_id' in result
assert 'approval_url' in result assert 'approval_url' in result
service.paypal_handler.create_order.assert_called_once() service.paypal_handler.create_topup_order.assert_called_once()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_stripe_webhook_credits_wallet(): async def test_stripe_webhook_credits_wallet():
"""Test that successful Stripe payment webhook credits user wallet""" """Test that successful Stripe payment webhook credits user wallet"""
db = Mock() db = _make_payment_db()
config = {'encryption_key': _TEST_ENCRYPTION_KEY, 'base_url': 'http://localhost'} config = {'encryption_key': _TEST_ENCRYPTION_KEY, 'base_url': 'http://localhost'}
with patch('aisbf.payments.wallet.manager.WalletManager.credit_wallet', new_callable=AsyncMock) as mock_credit: event = {
service = PaymentService(db, config) 'type': 'payment_intent.succeeded',
'data': {
mock_credit.return_value = {'success': True, 'new_balance': Decimal('20.00')} 'object': {
'id': 'pi_12345',
event = { # amount lives in metadata for top-up intents
'type': 'payment_intent.succeeded', 'metadata': {'user_id': '123', 'topup': 'true', 'amount': '20.00'},
'data': { 'amount': 2000,
'object': { 'currency': 'usd'
'id': 'pi_12345',
'metadata': {'user_id': '123', 'topup': 'true'},
'amount': 2000,
'currency': 'usd'
}
} }
} }
}
result = await service.stripe_handler.handle_webhook(b'', 'test_sig')
with patch('aisbf.payments.wallet.manager.WalletManager.credit_wallet', new_callable=AsyncMock) as mock_credit, \
patch('stripe.Webhook.construct_event', return_value=event):
service = PaymentService(db, config)
mock_credit.return_value = {'success': True, 'new_balance': Decimal('20.00')}
result = await service.stripe_handler.handle_webhook(b'{}', 'test_sig')
assert result['status'] == 'success' assert result['status'] == 'success'
mock_credit.assert_called_once_with( mock_credit.assert_called_once_with(
user_id=123, user_id=123,
...@@ -118,7 +138,8 @@ async def test_stripe_webhook_credits_wallet(): ...@@ -118,7 +138,8 @@ async def test_stripe_webhook_credits_wallet():
transaction_details={ transaction_details={
'payment_gateway': 'stripe', 'payment_gateway': 'stripe',
'gateway_transaction_id': 'pi_12345', 'gateway_transaction_id': 'pi_12345',
'description': 'Wallet top up via Stripe' 'description': 'Wallet top up via Stripe',
'metadata': {'payment_intent': 'pi_12345'}
} }
) )
...@@ -126,31 +147,44 @@ async def test_stripe_webhook_credits_wallet(): ...@@ -126,31 +147,44 @@ async def test_stripe_webhook_credits_wallet():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_crypto_payment_credits_wallet(): async def test_crypto_payment_credits_wallet():
"""Test that confirmed crypto payment credits user wallet""" """Test that confirmed crypto payment credits user wallet"""
db = Mock() db = _make_payment_db()
config = {'encryption_key': _TEST_ENCRYPTION_KEY, 'base_url': 'http://localhost'} config = {'encryption_key': _TEST_ENCRYPTION_KEY, 'base_url': 'http://localhost'}
with patch('aisbf.payments.wallet.manager.WalletManager.credit_wallet', new_callable=AsyncMock) as mock_credit: # Create the user so the fiat wallet row can be created/credited.
service = PaymentService(db, config) with db._get_connection() as conn:
service.price_service.convert_crypto_to_fiat = AsyncMock(return_value=Decimal('25.00')) cursor = conn.cursor()
cursor.execute("""
await service.blockchain_monitor.credit_user_wallet( INSERT INTO users (id, username, email, password_hash, role, email_verified)
user_id=123, VALUES (123, 'crypto-user', 'crypto@example.com', 'hash', 'user', 1)
crypto_type='btc', """)
amount=0.0005, conn.commit()
tx_id=456
) service = PaymentService(db, config)
# credit_user_wallet uses the monitor's own price service.
mock_credit.assert_called_once() service.blockchain_monitor.price_service.convert_crypto_to_fiat = AsyncMock(return_value=25.00)
args = mock_credit.call_args
assert args[1]['user_id'] == 123 # credit_user_wallet credits the fiat wallet directly via SQL (no
assert args[1]['amount'] == Decimal('25.00') # WalletManager.credit_wallet call), so verify the resulting balance.
assert args[1]['transaction_details']['payment_gateway'] == 'crypto_btc' await service.blockchain_monitor.credit_user_wallet(
user_id=123,
crypto_type='btc',
amount=0.0005,
tx_id=456
)
with db._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT balance FROM user_wallets WHERE user_id = 123")
row = cursor.fetchone()
assert row is not None
assert Decimal(str(row[0])) == Decimal('25.00')
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_invalid_topup_amount(): async def test_invalid_topup_amount():
"""Test that invalid top up amounts are rejected""" """Test that invalid top up amounts are rejected"""
db = Mock() db = _make_payment_db()
config = {'encryption_key': _TEST_ENCRYPTION_KEY, 'base_url': 'http://localhost'} config = {'encryption_key': _TEST_ENCRYPTION_KEY, 'base_url': 'http://localhost'}
service = PaymentService(db, config) service = PaymentService(db, config)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment