Add integration test for duplicate payment confirmation count update bug

This test reproduces the exact issue where duplicate payments weren't getting
their confirmation counts updated in the main processing loop, causing stuck refunds.

The test:
- Creates a duplicate payment with STATUS_DOUBLEPAY_REFUND and 0 confirmations
- Mocks blockchain data showing the transaction has 15 confirmations
- Calls process_payment() to simulate the main processing cycle
- Asserts that current_confirmations gets updated from 0 to 15

This test would have caught the bug where duplicate payments remained stuck
at 0 confirmations despite transactions having 10+ confirmations on blockchain.

Currently FAILS (as expected) since the fix hasn't been fully verified.
The test will pass once the confirmation update logic is working correctly.
This commit is contained in:
Russell Ballestrini 2025-09-28 13:10:46 -04:00
parent c4eaacb453
commit b6fed206ce

View file

@ -667,6 +667,110 @@ class TestDoubleSpendIntegration(DatabaseIntegrationTests):
) # The duplicate amount
self.assertIsNone(duplicate_payment.invoice) # No invoice for duplicates
@patch("make_post_sell.lib.crypto_watcher.send_refund_email")
@patch("make_post_sell.lib.crypto_watcher.send_purchase_email")
@patch("make_post_sell.lib.crypto_watcher.send_sale_email")
def test_duplicate_payment_confirmation_count_updates_in_processing_cycle(
self, mock_sale_email, mock_purchase_email, mock_refund_email
):
"""
Integration test for the bug where duplicate payments weren't getting their
confirmation counts updated in the main processing loop, causing stuck refunds.
This tests the full cycle: create duplicate payment, process it with mock
blockchain data showing high confirmations, verify confirmation count updates.
"""
with transaction.manager:
# Create the base payment scenario
payment, invoice, shop, user = self._create_payment_scenario(
status=CryptoPayment.STATUS_CONFIRMED, received=100000000, expected=100000000
)
payment.tx_hashes = json.dumps(["original-tx-123"])
self.dbsession.add(payment)
# Create a duplicate payment that should get its confirmations updated
duplicate_payment = CryptoPayment(
invoice=None, # Duplicates don't have invoices
user=user,
shop=shop,
address=payment.address,
account_index=payment.account_index,
subaddress_index=payment.subaddress_index,
coin_type="XMR",
expected_amount=0, # Duplicates don't expect payment
rate_locked_usd_per_coin=Decimal("150.00"),
quote_expires_at_ms=int(time.time() * 1000) + 900000,
confirmations_required=10,
refund_address="test-refund-address-12345",
)
duplicate_payment.status = CryptoPayment.STATUS_DOUBLEPAY_REFUND
duplicate_payment.received_amount = 37080000 # Some amount received
duplicate_payment.current_confirmations = 0 # Starting with 0 - the bug!
duplicate_payment.tx_hashes = json.dumps(["duplicate-tx-456"])
self.dbsession.add(duplicate_payment)
self.dbsession.flush()
# Mock crypto client to simulate blockchain with confirmed transaction
mock_client = MagicMock()
# This simulates the _call to get_transfers returning the duplicate transaction with confirmations
mock_client._call.return_value = {
"in": [
{
"txid": "original-tx-123",
"amount": 100000000,
"confirmations": 25, # Original transaction
},
{
"txid": "duplicate-tx-456",
"amount": 37080000,
"confirmations": 15, # Duplicate transaction with sufficient confirmations
},
{
"txid": "newer-tx-789",
"amount": 20000000,
"confirmations": 2, # New transaction with low confirmations
},
]
}
# Mock PaymentRescue to prevent actual refund processing but allow confirmation updates
with patch("make_post_sell.lib.crypto_watcher.PaymentRescue") as mock_rescue_class:
# Simply return None to prevent refund processing
# The key test is that confirmations get updated, not that refund happens
mock_rescue_class.return_value = None
# Create mock request
mock_request = MagicMock()
mock_request.dbsession = self.dbsession
# TEST: Process the duplicate payment - should update confirmations and process refund
process_payment(mock_request, duplicate_payment, [], client=mock_client)
# CRITICAL ASSERTION: Confirmation count should have been updated from 0 to 15
self.assertEqual(
duplicate_payment.current_confirmations,
15,
"Duplicate payment confirmation count was not updated from blockchain data"
)
# Verify the specific blockchain query was made to get ALL transfers (not filtered)
mock_client._call.assert_called_once_with(
"get_transfers",
{
"in": True,
"out": False,
"pending": True,
"failed": False,
"pool": True,
"filter_by_height": False, # Critical: gets ALL transfers to find old confirmed txs
"subaddr_indices": [duplicate_payment.subaddress_index],
"account_index": duplicate_payment.account_index,
}
)
# Since we mocked PaymentRescue to return None, refund won't be attempted
# but the confirmation count update should still happen
if __name__ == "__main__":
import unittest