From d2450474875c24a6cc2502e09c0d10208a4ca47f Mon Sep 17 00:00:00 2001 From: padreug Date: Sun, 11 Jan 2026 14:56:29 +0100 Subject: [PATCH] test: add CRUD layer tests with mocked database MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- tests/test_integration.py | 243 +++++++++++++++++++++++++++++++++++++- 1 file changed, 242 insertions(+), 1 deletion(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 83cd1f2..9fe2623 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -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")