Skip to content

Commit 63fc3e0

Browse files
committed
Update tests for higher coverage
1 parent a321ae7 commit 63fc3e0

File tree

3 files changed

+138
-1
lines changed

3 files changed

+138
-1
lines changed

tests/test_blockchain_client.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,61 @@ def test_send_signed_transaction_reverted(blockchain_client: BlockchainClient):
377377
blockchain_client._send_signed_transaction(mock_signed_tx)
378378

379379

380-
# 3. Orchestration Tests
380+
# 3. Orchestration and Edge Case Tests
381+
382+
383+
def test_execute_rpc_call_with_failover(blockchain_client: BlockchainClient):
384+
"""
385+
Tests that _execute_rpc_call fails over to the next provider if the first one
386+
is unreachable, and sends a Slack notification.
387+
"""
388+
# 1. Setup
389+
# Simulate the first provider failing, then the second succeeding.
390+
# The inner retry will try 3 times, then the outer failover logic will switch provider.
391+
mock_func = MagicMock()
392+
mock_func.side_effect = [
393+
requests.exceptions.ConnectionError("RPC down 1"),
394+
requests.exceptions.ConnectionError("RPC down 2"),
395+
requests.exceptions.ConnectionError("RPC down 3"),
396+
"Success",
397+
]
398+
blockchain_client.slack_notifier.reset_mock() # Clear prior calls
399+
400+
# 2. Action
401+
# The _execute_rpc_call decorator will handle the retry/failover
402+
result = blockchain_client._execute_rpc_call(mock_func)
403+
404+
# 3. Assertions
405+
assert result == "Success"
406+
assert mock_func.call_count == 4
407+
assert blockchain_client.current_rpc_index == 1 # Should have failed over to the 2nd provider
408+
409+
# Verify a slack message was sent about the failover
410+
blockchain_client.slack_notifier.send_info_notification.assert_called_once()
411+
call_kwargs = blockchain_client.slack_notifier.send_info_notification.call_args.kwargs
412+
assert "Switching from previous RPC" in call_kwargs["message"]
413+
414+
415+
def test_determine_transaction_nonce_replace_handles_errors(blockchain_client: BlockchainClient):
416+
"""
417+
Tests that nonce determination falls back gracefully if checking for pending
418+
transactions or nonce gaps fails.
419+
"""
420+
# 1. Setup
421+
# Simulate errors when trying to get pending blocks and checking for gaps
422+
w3_instance = blockchain_client.mock_w3_instance
423+
w3_instance.eth.get_block.side_effect = ValueError("Cannot get pending block")
424+
# After all errors, the final fallback is to get the latest transaction count.
425+
w3_instance.eth.get_transaction_count.side_effect = [
426+
ValueError("Cannot get pending nonce"),
427+
ValueError("Cannot get latest nonce"),
428+
9, # This will be the final fallback value
429+
]
430+
431+
# 2. Action & Assertions
432+
# The function should swallow the errors and then raise the final one
433+
with pytest.raises(ValueError, match="Cannot get latest nonce"):
434+
blockchain_client._determine_transaction_nonce(MOCK_SENDER_ADDRESS, replace=True)
381435

382436

383437
def test_send_transaction_to_allow_indexers_orchestration(blockchain_client: BlockchainClient):
@@ -428,6 +482,7 @@ def test_batch_processing_splits_correctly(blockchain_client: BlockchainClient):
428482
# Assertions
429483
assert len(tx_hashes) == 3
430484
assert blockchain_client.send_transaction_to_allow_indexers.call_count == 3
485+
431486
# Check the contents of each call
432487
assert blockchain_client.send_transaction_to_allow_indexers.call_args_list[0][0][0] == addresses[0:2]
433488
assert blockchain_client.send_transaction_to_allow_indexers.call_args_list[1][0][0] == addresses[2:4]
@@ -440,6 +495,7 @@ def test_batch_processing_halts_on_failure(blockchain_client: BlockchainClient):
440495
"""
441496
# Setup
442497
addresses = [f"0x{i}" * 40 for i in range(5)]
498+
443499
# Simulate failure on the second call
444500
blockchain_client.send_transaction_to_allow_indexers = MagicMock(
445501
side_effect=["tx_hash_1", Exception("RPC Error"), "tx_hash_3"]

tests/test_configuration.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,50 @@ def test_credential_manager_invalid_json(mock_env):
215215
CredentialManager().setup_google_credentials()
216216

217217

218+
def test_credential_manager_incomplete_service_account(mock_env):
219+
"""Tests that a ValueError is raised for incomplete service account JSON."""
220+
# 1. Setup - Missing 'private_key'
221+
creds_json = '{"type": "service_account", "client_email": "ce", "project_id": "pi"}'
222+
mock_env.setenv("GOOGLE_APPLICATION_CREDENTIALS", creds_json)
223+
224+
# 2. Action & Assertion
225+
with pytest.raises(ValueError, match="Incomplete service_account credentials"):
226+
CredentialManager().setup_google_credentials()
227+
228+
229+
def test_credential_manager_unsupported_type(mock_env):
230+
"""Tests that a ValueError is raised for an unsupported credential type."""
231+
# 1. Setup
232+
creds_json = '{"type": "unsupported"}'
233+
mock_env.setenv("GOOGLE_APPLICATION_CREDENTIALS", creds_json)
234+
235+
# 2. Action & Assertion
236+
with pytest.raises(ValueError, match="Unsupported credential type"):
237+
CredentialManager().setup_google_credentials()
238+
239+
240+
def test_get_default_config_path_docker(tmp_path: Path):
241+
"""Tests that the loader finds the config in the default /app path for Docker."""
242+
# 1. Setup
243+
# Simulate being in a Docker container where the /app/config.toml exists
244+
docker_config_path = tmp_path / "app" / "config.toml"
245+
docker_config_path.parent.mkdir()
246+
docker_config_path.write_text(MOCK_TOML_CONFIG)
247+
248+
with patch("pathlib.Path.exists") as mock_exists:
249+
# The first check is for the Docker path, so we make it return True
250+
mock_exists.return_value = True
251+
loader = ConfigLoader()
252+
# Overwrite the loader's default path for this test
253+
loader.config_path = str(docker_config_path)
254+
255+
# 2. Action
256+
found_path = loader._get_default_config_path()
257+
258+
# 3. Assertions
259+
assert found_path == "/app/config.toml"
260+
261+
218262
# 4. Standalone Validator Tests
219263

220264

tests/test_scheduler.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,43 @@ def test_check_missed_runs_does_nothing_if_recent(scheduler_no_init: "Scheduler"
244244
scheduler.run_oracle.assert_not_called()
245245

246246

247+
def test_check_missed_runs_sends_slack_notification(scheduler_no_init: "Scheduler"):
248+
"""
249+
Tests that a Slack notification is sent when missed runs are detected.
250+
"""
251+
# 1. Setup
252+
scheduler = scheduler_no_init
253+
two_days_ago = datetime.now().date() - timedelta(days=2)
254+
scheduler.get_last_run_date = MagicMock(return_value=two_days_ago)
255+
scheduler.run_oracle = MagicMock() # Mock the main run function
256+
scheduler.slack_notifier.send_info_notification.reset_mock()
257+
258+
# 2. Action
259+
scheduler.check_missed_runs()
260+
261+
# 3. Assertions
262+
scheduler.slack_notifier.send_info_notification.assert_called_once()
263+
call_kwargs = scheduler.slack_notifier.send_info_notification.call_args.kwargs
264+
assert "Missed Runs Detected" in call_kwargs["title"]
265+
assert "Detected 1 missed oracle runs" in call_kwargs["message"]
266+
267+
268+
def test_get_last_run_date_handles_corrupted_file(scheduler: "Scheduler", mock_dependencies: dict):
269+
"""
270+
Tests that get_last_run_date returns None if the file contains an invalid date string.
271+
"""
272+
# 1. Setup
273+
mock_dependencies["os"].path.exists.return_value = True
274+
# Simulate a file with corrupted content
275+
mock_dependencies["open"].return_value.read.return_value = "not-a-date"
276+
277+
# 2. Action
278+
last_run = scheduler.get_last_run_date()
279+
280+
# 3. Assertions
281+
assert last_run is None
282+
283+
247284
def test_run_oracle_success(scheduler: "Scheduler", mock_dependencies: dict):
248285
"""
249286
Tests that `run_oracle` calls the main oracle function and saves state upon success.

0 commit comments

Comments
 (0)