test: add CRUD layer tests with mocked database
Tests verify Decimal values flow correctly through CRUD operations: - create_deposit passes Decimal amount to db.execute() - create_dca_payment passes Decimal fiat and exchange_rate - create_lamassu_transaction passes all 5 Decimal fields - get_client_balance_summary returns Decimal types 41 tests now pass (23 unit + 18 integration). 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
49dd4d1844
commit
d245047487
1 changed files with 242 additions and 1 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
Integration tests for the full transaction processing flow.
|
Integration tests for the full transaction processing flow.
|
||||||
|
|
||||||
These tests verify that data flows correctly from:
|
These tests verify that data flows correctly from:
|
||||||
CSV parsing → Decimal conversion → calculations → model creation
|
CSV parsing → Decimal conversion → calculations → model creation → CRUD
|
||||||
|
|
||||||
This gives us confidence that real Lamassu transactions will be
|
This gives us confidence that real Lamassu transactions will be
|
||||||
processed correctly end-to-end.
|
processed correctly end-to-end.
|
||||||
|
|
@ -21,6 +21,10 @@ from ..models import (
|
||||||
CreateDcaPaymentData,
|
CreateDcaPaymentData,
|
||||||
CreateLamassuTransactionData,
|
CreateLamassuTransactionData,
|
||||||
ClientBalanceSummary,
|
ClientBalanceSummary,
|
||||||
|
CreateDepositData,
|
||||||
|
DcaDeposit,
|
||||||
|
DcaPayment,
|
||||||
|
StoredLamassuTransaction,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -377,3 +381,240 @@ class TestExchangeRatePrecision:
|
||||||
# This is the 0.01 discrepancy we discussed, multiplied by number of clients
|
# This is the 0.01 discrepancy we discussed, multiplied by number of clients
|
||||||
assert abs(total_fiat_distributed - tx["fiat_amount"]) < Decimal("0.05"), \
|
assert abs(total_fiat_distributed - tx["fiat_amount"]) < Decimal("0.05"), \
|
||||||
f"Total distributed {total_fiat_distributed} vs original {tx['fiat_amount']}"
|
f"Total distributed {total_fiat_distributed} vs original {tx['fiat_amount']}"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CRUD LAYER TESTS (with mocked database)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestCrudLayerDecimalHandling:
|
||||||
|
"""
|
||||||
|
Test that CRUD operations correctly pass Decimal values to the database.
|
||||||
|
|
||||||
|
These tests mock the database layer to verify:
|
||||||
|
1. Decimal values from models are passed correctly to db.execute()
|
||||||
|
2. The correct SQL parameters include Decimal types
|
||||||
|
"""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_deposit_passes_decimal_amount(self):
|
||||||
|
"""Verify create_deposit passes Decimal amount to database."""
|
||||||
|
from unittest.mock import patch, AsyncMock
|
||||||
|
|
||||||
|
# Create deposit data with Decimal
|
||||||
|
deposit_data = CreateDepositData(
|
||||||
|
client_id="test_client_123",
|
||||||
|
amount=Decimal("1500.75"),
|
||||||
|
currency="GTQ",
|
||||||
|
notes="Test deposit"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the model stored it as Decimal
|
||||||
|
assert isinstance(deposit_data.amount, Decimal)
|
||||||
|
assert deposit_data.amount == Decimal("1500.75")
|
||||||
|
|
||||||
|
# Mock the database
|
||||||
|
mock_db = AsyncMock()
|
||||||
|
mock_db.execute = AsyncMock()
|
||||||
|
mock_db.fetchone = AsyncMock(return_value=DcaDeposit(
|
||||||
|
id="deposit_123",
|
||||||
|
client_id="test_client_123",
|
||||||
|
amount=Decimal("1500.75"),
|
||||||
|
currency="GTQ",
|
||||||
|
status="pending",
|
||||||
|
notes="Test deposit",
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
confirmed_at=None
|
||||||
|
))
|
||||||
|
|
||||||
|
with patch('satmachineadmin.crud.db', mock_db):
|
||||||
|
from ..crud import create_deposit
|
||||||
|
result = await create_deposit(deposit_data)
|
||||||
|
|
||||||
|
# Verify db.execute was called
|
||||||
|
mock_db.execute.assert_called_once()
|
||||||
|
|
||||||
|
# Get the parameters passed to execute
|
||||||
|
call_args = mock_db.execute.call_args
|
||||||
|
params = call_args[0][1] # Second positional arg is the params dict
|
||||||
|
|
||||||
|
# Verify the amount parameter is Decimal
|
||||||
|
assert isinstance(params["amount"], Decimal)
|
||||||
|
assert params["amount"] == Decimal("1500.75")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_dca_payment_passes_decimal_values(self):
|
||||||
|
"""Verify create_dca_payment passes Decimal fiat and exchange_rate."""
|
||||||
|
from unittest.mock import patch, AsyncMock
|
||||||
|
|
||||||
|
# Parse a real transaction
|
||||||
|
tx = parse_csv_like_transaction_processor(LAMASSU_CSV_DATA["5.5pct_no_discount"])
|
||||||
|
base_sats, _, _ = calculate_commission(
|
||||||
|
tx["crypto_amount"],
|
||||||
|
tx["commission_percentage"],
|
||||||
|
tx["discount"]
|
||||||
|
)
|
||||||
|
exchange_rate = calculate_exchange_rate(base_sats, tx["fiat_amount"])
|
||||||
|
|
||||||
|
# Create payment data
|
||||||
|
client_sats = 146682 # Half of base
|
||||||
|
client_fiat = (Decimal(client_sats) / exchange_rate).quantize(Decimal("0.01"))
|
||||||
|
|
||||||
|
payment_data = CreateDcaPaymentData(
|
||||||
|
client_id="test_client",
|
||||||
|
amount_sats=client_sats,
|
||||||
|
amount_fiat=client_fiat,
|
||||||
|
exchange_rate=exchange_rate,
|
||||||
|
transaction_type="flow",
|
||||||
|
lamassu_transaction_id="def456",
|
||||||
|
transaction_time=tx["transaction_time"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify model has Decimal types
|
||||||
|
assert isinstance(payment_data.amount_fiat, Decimal)
|
||||||
|
assert isinstance(payment_data.exchange_rate, Decimal)
|
||||||
|
|
||||||
|
# Mock database
|
||||||
|
mock_db = AsyncMock()
|
||||||
|
mock_db.execute = AsyncMock()
|
||||||
|
mock_db.fetchone = AsyncMock(return_value=DcaPayment(
|
||||||
|
id="payment_123",
|
||||||
|
client_id="test_client",
|
||||||
|
amount_sats=client_sats,
|
||||||
|
amount_fiat=client_fiat,
|
||||||
|
exchange_rate=exchange_rate,
|
||||||
|
transaction_type="flow",
|
||||||
|
lamassu_transaction_id="def456",
|
||||||
|
payment_hash=None,
|
||||||
|
status="pending",
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
transaction_time=tx["transaction_time"],
|
||||||
|
))
|
||||||
|
|
||||||
|
with patch('satmachineadmin.crud.db', mock_db):
|
||||||
|
from ..crud import create_dca_payment
|
||||||
|
result = await create_dca_payment(payment_data)
|
||||||
|
|
||||||
|
# Verify db.execute was called
|
||||||
|
mock_db.execute.assert_called_once()
|
||||||
|
|
||||||
|
# Get params
|
||||||
|
call_args = mock_db.execute.call_args
|
||||||
|
params = call_args[0][1]
|
||||||
|
|
||||||
|
# Verify Decimal types in params
|
||||||
|
assert isinstance(params["amount_fiat"], Decimal)
|
||||||
|
assert isinstance(params["exchange_rate"], Decimal)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_lamassu_transaction_passes_all_decimals(self):
|
||||||
|
"""Verify create_lamassu_transaction passes all Decimal fields."""
|
||||||
|
from unittest.mock import patch, AsyncMock
|
||||||
|
|
||||||
|
tx = parse_csv_like_transaction_processor(LAMASSU_CSV_DATA["8.75pct_large"])
|
||||||
|
base_sats, commission_sats, effective = calculate_commission(
|
||||||
|
tx["crypto_amount"],
|
||||||
|
tx["commission_percentage"],
|
||||||
|
tx["discount"]
|
||||||
|
)
|
||||||
|
exchange_rate = calculate_exchange_rate(base_sats, tx["fiat_amount"])
|
||||||
|
|
||||||
|
transaction_data = CreateLamassuTransactionData(
|
||||||
|
lamassu_transaction_id=tx["transaction_id"],
|
||||||
|
fiat_amount=tx["fiat_amount"],
|
||||||
|
crypto_amount=tx["crypto_amount"],
|
||||||
|
commission_percentage=tx["commission_percentage"],
|
||||||
|
discount=tx["discount"],
|
||||||
|
effective_commission=effective,
|
||||||
|
commission_amount_sats=commission_sats,
|
||||||
|
base_amount_sats=base_sats,
|
||||||
|
exchange_rate=exchange_rate,
|
||||||
|
crypto_code=tx["crypto_code"],
|
||||||
|
fiat_code=tx["fiat_code"],
|
||||||
|
device_id=tx["device_id"],
|
||||||
|
transaction_time=tx["transaction_time"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify all Decimal fields
|
||||||
|
assert isinstance(transaction_data.fiat_amount, Decimal)
|
||||||
|
assert isinstance(transaction_data.commission_percentage, Decimal)
|
||||||
|
assert isinstance(transaction_data.discount, Decimal)
|
||||||
|
assert isinstance(transaction_data.effective_commission, Decimal)
|
||||||
|
assert isinstance(transaction_data.exchange_rate, Decimal)
|
||||||
|
|
||||||
|
# Mock database
|
||||||
|
mock_db = AsyncMock()
|
||||||
|
mock_db.execute = AsyncMock()
|
||||||
|
mock_db.fetchone = AsyncMock(return_value=StoredLamassuTransaction(
|
||||||
|
id="tx_123",
|
||||||
|
lamassu_transaction_id=tx["transaction_id"],
|
||||||
|
fiat_amount=tx["fiat_amount"],
|
||||||
|
crypto_amount=tx["crypto_amount"],
|
||||||
|
commission_percentage=tx["commission_percentage"],
|
||||||
|
discount=tx["discount"],
|
||||||
|
effective_commission=effective,
|
||||||
|
commission_amount_sats=commission_sats,
|
||||||
|
base_amount_sats=base_sats,
|
||||||
|
exchange_rate=exchange_rate,
|
||||||
|
crypto_code=tx["crypto_code"],
|
||||||
|
fiat_code=tx["fiat_code"],
|
||||||
|
device_id=tx["device_id"],
|
||||||
|
transaction_time=tx["transaction_time"],
|
||||||
|
processed_at=datetime.now(timezone.utc),
|
||||||
|
clients_count=0,
|
||||||
|
distributions_total_sats=0,
|
||||||
|
))
|
||||||
|
|
||||||
|
with patch('satmachineadmin.crud.db', mock_db):
|
||||||
|
from ..crud import create_lamassu_transaction
|
||||||
|
result = await create_lamassu_transaction(transaction_data)
|
||||||
|
|
||||||
|
# Verify db.execute was called
|
||||||
|
mock_db.execute.assert_called_once()
|
||||||
|
|
||||||
|
# Get params
|
||||||
|
call_args = mock_db.execute.call_args
|
||||||
|
params = call_args[0][1]
|
||||||
|
|
||||||
|
# Verify all Decimal fields in params
|
||||||
|
assert isinstance(params["fiat_amount"], Decimal), f"fiat_amount is {type(params['fiat_amount'])}"
|
||||||
|
assert isinstance(params["commission_percentage"], Decimal)
|
||||||
|
assert isinstance(params["discount"], Decimal)
|
||||||
|
assert isinstance(params["effective_commission"], Decimal)
|
||||||
|
assert isinstance(params["exchange_rate"], Decimal)
|
||||||
|
|
||||||
|
# Verify values match
|
||||||
|
assert params["fiat_amount"] == Decimal("2000")
|
||||||
|
assert params["commission_percentage"] == Decimal("0.0875")
|
||||||
|
assert params["base_amount_sats"] == 284322
|
||||||
|
assert params["commission_amount_sats"] == 24878
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_client_balance_summary_returns_decimals(self):
|
||||||
|
"""Verify get_client_balance_summary returns Decimal types."""
|
||||||
|
from unittest.mock import patch, AsyncMock
|
||||||
|
|
||||||
|
# Mock database responses
|
||||||
|
mock_db = AsyncMock()
|
||||||
|
|
||||||
|
# Mock deposits query result
|
||||||
|
mock_db.fetchone = AsyncMock(side_effect=[
|
||||||
|
# First call: deposits sum
|
||||||
|
{"total": Decimal("5000.00"), "currency": "GTQ"},
|
||||||
|
# Second call: payments sum
|
||||||
|
{"total": Decimal("1234.56")},
|
||||||
|
])
|
||||||
|
|
||||||
|
with patch('satmachineadmin.crud.db', mock_db):
|
||||||
|
from ..crud import get_client_balance_summary
|
||||||
|
result = await get_client_balance_summary("test_client")
|
||||||
|
|
||||||
|
# Verify result has Decimal types
|
||||||
|
assert isinstance(result.total_deposits, Decimal)
|
||||||
|
assert isinstance(result.total_payments, Decimal)
|
||||||
|
assert isinstance(result.remaining_balance, Decimal)
|
||||||
|
|
||||||
|
# Verify values
|
||||||
|
assert result.total_deposits == Decimal("5000.00")
|
||||||
|
assert result.total_payments == Decimal("1234.56")
|
||||||
|
assert result.remaining_balance == Decimal("3765.44")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue