From b6fed206cee300d3d4a5ebcf312b5ae2e4bd9fbe Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Sun, 28 Sep 2025 13:10:46 -0400 Subject: [PATCH] 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. --- .../tests/test_double_spend_integration.py | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/make_post_sell/tests/test_double_spend_integration.py b/make_post_sell/tests/test_double_spend_integration.py index ef57979..063d7f2 100644 --- a/make_post_sell/tests/test_double_spend_integration.py +++ b/make_post_sell/tests/test_double_spend_integration.py @@ -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