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. 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")