4
4
5
5
import json
6
6
from pathlib import Path
7
- from unittest .mock import MagicMock , mock_open , patch , PropertyMock
7
+ from unittest .mock import MagicMock , PropertyMock , mock_open , patch
8
8
9
9
import pytest
10
10
import requests
@@ -84,6 +84,7 @@ def blockchain_client(mock_w3, mock_slack, mock_file):
84
84
class TestInitializationAndConnection :
85
85
"""Tests focusing on the client's initialization and RPC connection logic."""
86
86
87
+
87
88
def test_successful_initialization (self , blockchain_client : BlockchainClient , mock_w3 , mock_file ):
88
89
"""
89
90
Tests that the BlockchainClient initializes correctly on the happy path.
@@ -103,12 +104,11 @@ def test_successful_initialization(self, blockchain_client: BlockchainClient, mo
103
104
client .w3 .is_connected .assert_called_once ()
104
105
105
106
# Assert contract object was created
106
- client .mock_w3_instance .eth .contract .assert_called_once_with (
107
- address = MOCK_CONTRACT_ADDRESS , abi = MOCK_ABI
108
- )
107
+ client .mock_w3_instance .eth .contract .assert_called_once_with (address = MOCK_CONTRACT_ADDRESS , abi = MOCK_ABI )
109
108
assert client .w3 is not None
110
109
assert client .contract is not None
111
110
111
+
112
112
def test_initialization_fails_if_abi_not_found (self , mock_w3 , mock_slack ):
113
113
"""
114
114
Tests that BlockchainClient raises an exception if the ABI file cannot be found.
@@ -126,6 +126,7 @@ def test_initialization_fails_if_abi_not_found(self, mock_w3, mock_slack):
126
126
slack_notifier = mock_slack ,
127
127
)
128
128
129
+
129
130
def test_rpc_failover_mechanism (self , mock_w3 , mock_slack ):
130
131
"""
131
132
Tests that the client successfully fails over to a secondary RPC if the primary fails.
@@ -135,9 +136,7 @@ def test_rpc_failover_mechanism(self, mock_w3, mock_slack):
135
136
mock_w3_instance .is_connected .side_effect = [False , True ]
136
137
137
138
with patch ("builtins.open" , mock_open (read_data = json .dumps (MOCK_ABI ))):
138
- with patch (
139
- "src.models.blockchain_client.Web3" , MagicMock (return_value = mock_w3_instance )
140
- ) as MockWeb3 :
139
+ with patch ("src.models.blockchain_client.Web3" , MagicMock (return_value = mock_w3_instance )) as MockWeb3 :
141
140
# Act
142
141
client = BlockchainClient (
143
142
rpc_providers = MOCK_RPC_PROVIDERS ,
@@ -159,6 +158,7 @@ def test_rpc_failover_mechanism(self, mock_w3, mock_slack):
159
158
# The client should be connected and pointing to the secondary provider index
160
159
assert client .current_rpc_index == 1
161
160
161
+
162
162
def test_connection_error_if_all_rpcs_fail (self , mock_w3 , mock_slack ):
163
163
"""
164
164
Tests that a ConnectionError is raised if the client cannot connect to any RPC provider.
@@ -183,6 +183,7 @@ def test_connection_error_if_all_rpcs_fail(self, mock_w3, mock_slack):
183
183
slack_notifier = mock_slack ,
184
184
)
185
185
186
+
186
187
def test_execute_rpc_call_with_failover (self , blockchain_client : BlockchainClient ):
187
188
"""
188
189
Tests that _execute_rpc_call fails over to the next provider if the first one
@@ -214,6 +215,7 @@ def test_execute_rpc_call_with_failover(self, blockchain_client: BlockchainClien
214
215
call_kwargs = blockchain_client .slack_notifier .send_info_notification .call_args .kwargs
215
216
assert "Switching from previous RPC" in call_kwargs ["message" ]
216
217
218
+
217
219
def test_execute_rpc_call_reraises_unexpected_exceptions (self , blockchain_client : BlockchainClient ):
218
220
"""
219
221
Tests that _execute_rpc_call does not attempt to failover on unexpected,
@@ -231,6 +233,7 @@ def test_execute_rpc_call_reraises_unexpected_exceptions(self, blockchain_client
231
233
assert blockchain_client .current_rpc_index == 0
232
234
blockchain_client .slack_notifier .send_info_notification .assert_not_called ()
233
235
236
+
234
237
def test_initialization_fails_with_empty_rpc_provider_list (self , mock_w3 , mock_slack ):
235
238
"""
236
239
Tests that BlockchainClient raises an exception if initialized with an empty list of RPC providers.
@@ -252,6 +255,7 @@ def test_initialization_fails_with_empty_rpc_provider_list(self, mock_w3, mock_s
252
255
class TestTransactionLogic :
253
256
"""Tests focusing on the helper methods for building and sending a transaction."""
254
257
258
+
255
259
def test_setup_transaction_account_success (self , blockchain_client : BlockchainClient ):
256
260
"""
257
261
Tests that _setup_transaction_account returns the correct address and formatted key
@@ -263,12 +267,11 @@ def test_setup_transaction_account_success(self, blockchain_client: BlockchainCl
263
267
address , key = blockchain_client ._setup_transaction_account (MOCK_PRIVATE_KEY )
264
268
265
269
mock_validate .assert_called_once_with (MOCK_PRIVATE_KEY )
266
- blockchain_client .mock_w3_instance .eth .account .from_key .assert_called_once_with (
267
- MOCK_PRIVATE_KEY
268
- )
270
+ blockchain_client .mock_w3_instance .eth .account .from_key .assert_called_once_with (MOCK_PRIVATE_KEY )
269
271
assert address == MOCK_SENDER_ADDRESS
270
272
assert key == MOCK_PRIVATE_KEY
271
273
274
+
272
275
def test_setup_transaction_account_invalid_key_raises_key_validation_error (
273
276
self , blockchain_client : BlockchainClient
274
277
):
@@ -281,6 +284,7 @@ def test_setup_transaction_account_invalid_key_raises_key_validation_error(
281
284
with pytest .raises (KeyValidationError , match = "Invalid key" ):
282
285
blockchain_client ._setup_transaction_account ("invalid-key" )
283
286
287
+
284
288
def test_setup_transaction_account_unexpected_error (self , blockchain_client : BlockchainClient ):
285
289
"""
286
290
Tests that _setup_transaction_account raises a generic exception for unexpected errors.
@@ -291,6 +295,7 @@ def test_setup_transaction_account_unexpected_error(self, blockchain_client: Blo
291
295
with pytest .raises (Exception , match = "Unexpected error" ):
292
296
blockchain_client ._setup_transaction_account ("any-key" )
293
297
298
+
294
299
def test_estimate_transaction_gas_success (self , blockchain_client : BlockchainClient ):
295
300
"""
296
301
Tests that _estimate_transaction_gas correctly estimates gas and adds a 25% buffer.
@@ -310,9 +315,8 @@ def test_estimate_transaction_gas_success(self, blockchain_client: BlockchainCli
310
315
311
316
# Assert
312
317
assert gas_limit == 125_000 # 100_000 * 1.25
313
- mock_contract_func .return_value .estimate_gas .assert_called_once_with (
314
- {"from" : MOCK_SENDER_ADDRESS }
315
- )
318
+ mock_contract_func .return_value .estimate_gas .assert_called_once_with ({"from" : MOCK_SENDER_ADDRESS })
319
+
316
320
317
321
def test_estimate_transaction_gas_failure (self , blockchain_client : BlockchainClient ):
318
322
"""
@@ -331,6 +335,7 @@ def test_estimate_transaction_gas_failure(self, blockchain_client: BlockchainCli
331
335
sender_address = MOCK_SENDER_ADDRESS ,
332
336
)
333
337
338
+
334
339
def test_determine_transaction_nonce_new (self , blockchain_client : BlockchainClient ):
335
340
"""
336
341
Tests that the next available nonce is fetched for a new transaction (replace=False).
@@ -344,9 +349,8 @@ def test_determine_transaction_nonce_new(self, blockchain_client: BlockchainClie
344
349
345
350
# Assert
346
351
assert nonce == expected_nonce
347
- blockchain_client .mock_w3_instance .eth .get_transaction_count .assert_called_once_with (
348
- MOCK_SENDER_ADDRESS
349
- )
352
+ blockchain_client .mock_w3_instance .eth .get_transaction_count .assert_called_once_with (MOCK_SENDER_ADDRESS )
353
+
350
354
351
355
def test_determine_transaction_nonce_replace (self , blockchain_client : BlockchainClient ):
352
356
"""
@@ -368,15 +372,15 @@ def test_determine_transaction_nonce_replace(self, blockchain_client: Blockchain
368
372
369
373
# Assert
370
374
assert nonce == 12
371
- blockchain_client .mock_w3_instance .eth .get_block .assert_called_once_with (
372
- "pending" , full_transactions = True
373
- )
375
+ blockchain_client .mock_w3_instance .eth .get_block .assert_called_once_with ("pending" , full_transactions = True )
376
+
374
377
375
378
def test_determine_transaction_nonce_replace_no_pending_tx_with_nonce_gap (
376
379
self , blockchain_client : BlockchainClient
377
380
):
378
381
"""
379
- Tests that nonce determination falls back to the latest nonce if no pending txs are found but a nonce gap exists.
382
+ Tests that nonce determination falls back to the latest nonce
383
+ if no pending txs are found but a nonce gap exists.
380
384
"""
381
385
# Arrange
382
386
blockchain_client .mock_w3_instance .eth .get_block .return_value = {"transactions" : []}
@@ -392,6 +396,7 @@ def test_determine_transaction_nonce_replace_no_pending_tx_with_nonce_gap(
392
396
assert nonce == 9 # Should use the latest nonce from the gap
393
397
assert blockchain_client .mock_w3_instance .eth .get_transaction_count .call_count == 2
394
398
399
+
395
400
def test_determine_transaction_nonce_replace_no_pending_tx_no_gap_fallback (
396
401
self , blockchain_client : BlockchainClient
397
402
):
@@ -415,6 +420,7 @@ def test_determine_transaction_nonce_replace_no_pending_tx_no_gap_fallback(
415
420
w3_instance .eth .get_block .assert_called_once_with ("pending" , full_transactions = True )
416
421
assert w3_instance .eth .get_transaction_count .call_count == 3
417
422
423
+
418
424
def test_determine_transaction_nonce_replace_handles_errors (self , blockchain_client : BlockchainClient ):
419
425
"""
420
426
Tests that nonce determination falls back gracefully if checking for pending
@@ -433,6 +439,7 @@ def test_determine_transaction_nonce_replace_handles_errors(self, blockchain_cli
433
439
w3_instance .eth .get_block .assert_called_once ()
434
440
w3_instance .eth .get_transaction_count .assert_called ()
435
441
442
+
436
443
def test_get_gas_prices_success (self , blockchain_client : BlockchainClient ):
437
444
"""
438
445
Tests that _get_gas_prices successfully fetches and returns the base and priority fees.
@@ -451,6 +458,7 @@ def test_get_gas_prices_success(self, blockchain_client: BlockchainClient):
451
458
assert base_fee == mock_base_fee
452
459
assert max_priority_fee == mock_priority_fee
453
460
461
+
454
462
def test_get_gas_prices_fallback_on_base_fee_error (self , blockchain_client : BlockchainClient ):
455
463
"""
456
464
Tests that _get_gas_prices falls back to a default base fee if the RPC call fails.
@@ -466,6 +474,7 @@ def test_get_gas_prices_fallback_on_base_fee_error(self, blockchain_client: Bloc
466
474
assert base_fee == 10 * 10 ** 9
467
475
blockchain_client .mock_w3_instance .to_wei .assert_called_once_with (10 , "gwei" )
468
476
477
+
469
478
def test_get_gas_prices_fallback_on_priority_fee_error (self , blockchain_client : BlockchainClient ):
470
479
"""
471
480
Tests that _get_gas_prices falls back to a default priority fee if the RPC call fails.
@@ -483,6 +492,7 @@ def test_get_gas_prices_fallback_on_priority_fee_error(self, blockchain_client:
483
492
assert max_priority_fee == 2 * 10 ** 9
484
493
blockchain_client .mock_w3_instance .to_wei .assert_called_once_with (2 , "gwei" )
485
494
495
+
486
496
@pytest .mark .parametrize (
487
497
"replace, expected_max_fee, expected_priority_fee" ,
488
498
[
@@ -517,6 +527,7 @@ def test_build_transaction_params(
517
527
assert tx_params ["from" ] == MOCK_SENDER_ADDRESS
518
528
assert tx_params ["nonce" ] == 1
519
529
530
+
520
531
def test_build_and_sign_transaction_success (self , blockchain_client : BlockchainClient ):
521
532
"""
522
533
Tests that _build_and_sign_transaction successfully builds and signs a transaction.
@@ -540,13 +551,12 @@ def test_build_and_sign_transaction_success(self, blockchain_client: BlockchainC
540
551
541
552
# Assert
542
553
assert signed_tx == mock_signed_transaction
543
- mock_contract_func .return_value .build_transaction .assert_called_once_with (
544
- {"from" : MOCK_SENDER_ADDRESS }
545
- )
554
+ mock_contract_func .return_value .build_transaction .assert_called_once_with ({"from" : MOCK_SENDER_ADDRESS })
546
555
blockchain_client .w3 .eth .account .sign_transaction .assert_called_once_with (
547
556
mock_transaction , MOCK_PRIVATE_KEY
548
557
)
549
558
559
+
550
560
def test_build_and_sign_transaction_failure (self , blockchain_client : BlockchainClient ):
551
561
"""
552
562
Tests that _build_and_sign_transaction raises an exception if building fails.
@@ -565,6 +575,7 @@ def test_build_and_sign_transaction_failure(self, blockchain_client: BlockchainC
565
575
private_key = "key" ,
566
576
)
567
577
578
+
568
579
def test_send_signed_transaction_success (self , blockchain_client : BlockchainClient ):
569
580
"""
570
581
Tests that a signed transaction is sent and its hash is returned on success.
@@ -588,6 +599,7 @@ def test_send_signed_transaction_success(self, blockchain_client: BlockchainClie
588
599
mock_tx_hash , MOCK_TX_TIMEOUT_SECONDS
589
600
)
590
601
602
+
591
603
def test_send_signed_transaction_raises_exception_when_reverted (self , blockchain_client : BlockchainClient ):
592
604
"""
593
605
Tests that an exception is raised if the transaction is reverted on-chain.
@@ -606,6 +618,7 @@ def test_send_signed_transaction_raises_exception_when_reverted(self, blockchain
606
618
):
607
619
blockchain_client ._send_signed_transaction (mock_signed_tx )
608
620
621
+
609
622
def test_send_signed_transaction_raises_exception_on_timeout (self , blockchain_client : BlockchainClient ):
610
623
"""
611
624
Tests that an exception is raised if waiting for the transaction receipt times out.
@@ -619,7 +632,9 @@ def test_send_signed_transaction_raises_exception_on_timeout(self, blockchain_cl
619
632
)
620
633
621
634
# Act & Assert
622
- with pytest .raises (Exception , match = "Error sending transaction or waiting for receipt: All RPC providers are unreachable." ):
635
+ with pytest .raises (
636
+ Exception , match = "Error sending transaction or waiting for receipt: All RPC providers are unreachable."
637
+ ):
623
638
blockchain_client ._send_signed_transaction (mock_signed_tx )
624
639
625
640
@@ -665,6 +680,7 @@ def mock_full_transaction_flow(mocker: MockerFixture):
665
680
class TestOrchestrationAndBatching :
666
681
"""Tests focusing on the end-to-end orchestration and batch processing logic."""
667
682
683
+
668
684
def test_execute_complete_transaction_happy_path (
669
685
self ,
670
686
blockchain_client : BlockchainClient ,
@@ -709,6 +725,7 @@ def test_execute_complete_transaction_happy_path(
709
725
)
710
726
mock_full_transaction_flow ["send" ].assert_called_once_with ("signed_tx" )
711
727
728
+
712
729
def test_execute_complete_transaction_missing_params (self , blockchain_client : BlockchainClient ):
713
730
"""
714
731
Tests that _execute_complete_transaction raises ValueError if required parameters are missing.
@@ -720,6 +737,7 @@ def test_execute_complete_transaction_missing_params(self, blockchain_client: Bl
720
737
with pytest .raises (ValueError , match = "Missing required parameters for transaction." ):
721
738
blockchain_client ._execute_complete_transaction (incomplete_params )
722
739
740
+
723
741
def test_execute_complete_transaction_invalid_function (self , blockchain_client : BlockchainClient ):
724
742
"""
725
743
Tests that _execute_complete_transaction raises ValueError for a non-existent contract function.
@@ -742,6 +760,7 @@ def test_execute_complete_transaction_invalid_function(self, blockchain_client:
742
760
):
743
761
blockchain_client ._execute_complete_transaction (params )
744
762
763
+
745
764
def test_send_transaction_to_allow_indexers_orchestration (
746
765
self , blockchain_client : BlockchainClient , mocker : MockerFixture
747
766
):
@@ -772,6 +791,7 @@ def test_send_transaction_to_allow_indexers_orchestration(
772
791
assert call_args ["contract_function" ] == "allow"
773
792
assert call_args ["replace" ] is False
774
793
794
+
775
795
def test_batch_processing_splits_correctly (self , blockchain_client : BlockchainClient ):
776
796
"""
777
797
Tests that the batch processing logic correctly splits a list of addresses
@@ -801,6 +821,7 @@ def test_batch_processing_splits_correctly(self, blockchain_client: BlockchainCl
801
821
assert blockchain_client .send_transaction_to_allow_indexers .call_args_list [1 ][0 ][0 ] == addresses [2 :4 ]
802
822
assert blockchain_client .send_transaction_to_allow_indexers .call_args_list [2 ][0 ][0 ] == addresses [4 :5 ]
803
823
824
+
804
825
def test_batch_processing_halts_on_failure (self , blockchain_client : BlockchainClient ):
805
826
"""
806
827
Tests that the batch processing halts immediately if one of the transactions fails.
@@ -827,6 +848,7 @@ def test_batch_processing_halts_on_failure(self, blockchain_client: BlockchainCl
827
848
# The method should have only been called twice (the first success, the second failure)
828
849
assert blockchain_client .send_transaction_to_allow_indexers .call_count == 2
829
850
851
+
830
852
def test_batch_processing_handles_empty_list (self , blockchain_client : BlockchainClient ):
831
853
"""
832
854
Tests that batch processing handles an empty list of addresses gracefully.
0 commit comments