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:
padreug 2026-01-11 14:56:29 +01:00
parent 49dd4d1844
commit d245047487

View file

@ -2,7 +2,7 @@
Integration tests for the full transaction processing flow.
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
processed correctly end-to-end.
@ -21,6 +21,10 @@ from ..models import (
CreateDcaPaymentData,
CreateLamassuTransactionData,
ClientBalanceSummary,
CreateDepositData,
DcaDeposit,
DcaPayment,
StoredLamassuTransaction,
)
@ -377,3 +381,240 @@ class TestExchangeRatePrecision:
# This is the 0.01 discrepancy we discussed, multiplied by number of clients
assert abs(total_fiat_distributed - tx["fiat_amount"]) < Decimal("0.05"), \
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")