PR Title Check / check-title (pull_request) Successful in 8s
Test Python Package / unit-tests (pull_request) Successful in 10s
Code Quality Pipeline / code-quality (pull_request) Failing after 17s
Test Python Package / integration-tests (pull_request) Successful in 47s
Test Python Package / coverage-report (pull_request) Successful in 12s
Co-authored-by: Cursor <[email protected]>
237 lines
8.1 KiB
Python
237 lines
8.1 KiB
Python
"""Unit tests for TTL-cached connection health checks on ConnectionAwareAdapter."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import cast
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import psycopg
|
|
import redis
|
|
|
|
from python_repositories.adapters.minio_adapter import MinioAdapter
|
|
from python_repositories.adapters.postgres_adapter import PostgresAdapter
|
|
from python_repositories.adapters.redis_adapter import RedisAdapter
|
|
from tests.conftest import TEST_MINIO_CONFIG, TEST_POSTGRES_CONFIG, TEST_REDIS_CONFIG
|
|
|
|
|
|
class TestRedisConnectionHealth:
|
|
def test_not_connected_when_no_client(self) -> None:
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG)
|
|
assert not adapter.is_connected()
|
|
|
|
def test_connected_when_probe_succeeds(self, redis_adapter: RedisAdapter) -> None:
|
|
mock_client = cast(MagicMock, redis_adapter._client)
|
|
mock_client.ping.return_value = True
|
|
|
|
assert redis_adapter.is_connected()
|
|
mock_client.ping.assert_called_once()
|
|
|
|
def test_stale_connection_when_probe_fails(
|
|
self, redis_adapter: RedisAdapter
|
|
) -> None:
|
|
cast(MagicMock, redis_adapter._client).ping.side_effect = redis.ConnectionError(
|
|
"connection lost"
|
|
)
|
|
|
|
assert not redis_adapter.is_connected()
|
|
|
|
def test_cache_hit_avoids_second_probe(self, redis_adapter: RedisAdapter) -> None:
|
|
mock_client = cast(MagicMock, redis_adapter._client)
|
|
mock_client.ping.return_value = True
|
|
|
|
with patch(
|
|
"python_repositories.adapters.connection_aware_adapter.time.monotonic",
|
|
return_value=100.0,
|
|
):
|
|
assert redis_adapter.is_connected()
|
|
assert redis_adapter.is_connected()
|
|
|
|
mock_client.ping.assert_called_once()
|
|
|
|
def test_cache_miss_runs_probe_again(self, redis_adapter: RedisAdapter) -> None:
|
|
mock_client = cast(MagicMock, redis_adapter._client)
|
|
mock_client.ping.return_value = True
|
|
|
|
with patch(
|
|
"python_repositories.adapters.connection_aware_adapter.time.monotonic",
|
|
side_effect=[100.0, 102.0],
|
|
):
|
|
assert redis_adapter.is_connected()
|
|
assert redis_adapter.is_connected()
|
|
|
|
assert mock_client.ping.call_count == 2
|
|
|
|
def test_disconnect_clears_cache(self, redis_adapter: RedisAdapter) -> None:
|
|
mock_client = cast(MagicMock, redis_adapter._client)
|
|
mock_client.ping.return_value = True
|
|
|
|
with patch(
|
|
"python_repositories.adapters.connection_aware_adapter.time.monotonic",
|
|
return_value=100.0,
|
|
):
|
|
assert redis_adapter.is_connected()
|
|
|
|
redis_adapter.disconnect()
|
|
reinjected = RedisAdapter(
|
|
config=redis_adapter._config,
|
|
client=mock_client,
|
|
)
|
|
|
|
with patch(
|
|
"python_repositories.adapters.connection_aware_adapter.time.monotonic",
|
|
return_value=100.0,
|
|
):
|
|
assert reinjected.is_connected()
|
|
|
|
assert mock_client.ping.call_count == 2
|
|
|
|
|
|
class TestMinioConnectionHealth:
|
|
def test_not_connected_when_no_client(self) -> None:
|
|
adapter = MinioAdapter(config=TEST_MINIO_CONFIG)
|
|
assert not adapter.is_connected()
|
|
|
|
def test_not_connected_when_bucket_name_missing(
|
|
self, minio_adapter: MinioAdapter
|
|
) -> None:
|
|
minio_adapter._bucket_name = None
|
|
|
|
assert not minio_adapter.is_connected()
|
|
|
|
def test_connected_when_probe_succeeds(self, minio_adapter: MinioAdapter) -> None:
|
|
mock_client = cast(MagicMock, minio_adapter._client)
|
|
mock_client.bucket_exists.return_value = True
|
|
|
|
assert minio_adapter.is_connected()
|
|
mock_client.bucket_exists.assert_called_once_with("test-bucket")
|
|
|
|
def test_stale_connection_when_probe_fails(
|
|
self, minio_adapter: MinioAdapter
|
|
) -> None:
|
|
cast(MagicMock, minio_adapter._client).bucket_exists.side_effect = Exception(
|
|
"connection lost"
|
|
)
|
|
|
|
assert not minio_adapter.is_connected()
|
|
|
|
def test_cache_hit_avoids_second_probe(self, minio_adapter: MinioAdapter) -> None:
|
|
mock_client = cast(MagicMock, minio_adapter._client)
|
|
mock_client.bucket_exists.return_value = True
|
|
|
|
with patch(
|
|
"python_repositories.adapters.connection_aware_adapter.time.monotonic",
|
|
return_value=100.0,
|
|
):
|
|
assert minio_adapter.is_connected()
|
|
assert minio_adapter.is_connected()
|
|
|
|
mock_client.bucket_exists.assert_called_once()
|
|
|
|
def test_cache_miss_runs_probe_again(self, minio_adapter: MinioAdapter) -> None:
|
|
mock_client = cast(MagicMock, minio_adapter._client)
|
|
mock_client.bucket_exists.return_value = True
|
|
|
|
with patch(
|
|
"python_repositories.adapters.connection_aware_adapter.time.monotonic",
|
|
side_effect=[100.0, 102.0],
|
|
):
|
|
assert minio_adapter.is_connected()
|
|
assert minio_adapter.is_connected()
|
|
|
|
assert mock_client.bucket_exists.call_count == 2
|
|
|
|
def test_disconnect_clears_cache(self, minio_adapter: MinioAdapter) -> None:
|
|
mock_client = cast(MagicMock, minio_adapter._client)
|
|
mock_client.bucket_exists.return_value = True
|
|
|
|
with patch(
|
|
"python_repositories.adapters.connection_aware_adapter.time.monotonic",
|
|
return_value=100.0,
|
|
):
|
|
assert minio_adapter.is_connected()
|
|
|
|
minio_adapter.disconnect()
|
|
reinjected = MinioAdapter(
|
|
config=minio_adapter._config,
|
|
client=mock_client,
|
|
)
|
|
|
|
with patch(
|
|
"python_repositories.adapters.connection_aware_adapter.time.monotonic",
|
|
return_value=100.0,
|
|
):
|
|
assert reinjected.is_connected()
|
|
|
|
assert mock_client.bucket_exists.call_count == 2
|
|
|
|
|
|
class TestPostgresConnectionHealth:
|
|
def test_not_connected_when_no_client(self) -> None:
|
|
adapter = PostgresAdapter(config=TEST_POSTGRES_CONFIG)
|
|
assert not adapter.is_connected()
|
|
|
|
def test_connected_when_probe_succeeds(
|
|
self, postgres_adapter: PostgresAdapter
|
|
) -> None:
|
|
assert postgres_adapter.is_connected()
|
|
cast(MagicMock, postgres_adapter._client).cursor.assert_called()
|
|
|
|
def test_stale_connection_when_probe_fails(
|
|
self, postgres_adapter: PostgresAdapter
|
|
) -> None:
|
|
mock_cursor = cast(MagicMock, postgres_adapter._client).cursor.return_value
|
|
mock_cursor.execute.side_effect = psycopg.OperationalError("connection lost")
|
|
|
|
assert not postgres_adapter.is_connected()
|
|
|
|
def test_cache_hit_avoids_second_probe(
|
|
self, postgres_adapter: PostgresAdapter
|
|
) -> None:
|
|
mock_client = cast(MagicMock, postgres_adapter._client)
|
|
|
|
with patch(
|
|
"python_repositories.adapters.connection_aware_adapter.time.monotonic",
|
|
return_value=100.0,
|
|
):
|
|
assert postgres_adapter.is_connected()
|
|
assert postgres_adapter.is_connected()
|
|
|
|
assert mock_client.cursor.call_count == 1
|
|
|
|
def test_cache_miss_runs_probe_again(
|
|
self, postgres_adapter: PostgresAdapter
|
|
) -> None:
|
|
mock_client = cast(MagicMock, postgres_adapter._client)
|
|
|
|
with patch(
|
|
"python_repositories.adapters.connection_aware_adapter.time.monotonic",
|
|
side_effect=[100.0, 102.0],
|
|
):
|
|
assert postgres_adapter.is_connected()
|
|
assert postgres_adapter.is_connected()
|
|
|
|
assert mock_client.cursor.call_count == 2
|
|
|
|
def test_disconnect_clears_cache(self, postgres_adapter: PostgresAdapter) -> None:
|
|
mock_client = cast(MagicMock, postgres_adapter._client)
|
|
|
|
with patch(
|
|
"python_repositories.adapters.connection_aware_adapter.time.monotonic",
|
|
return_value=100.0,
|
|
):
|
|
assert postgres_adapter.is_connected()
|
|
|
|
postgres_adapter.disconnect()
|
|
reinjected = PostgresAdapter(
|
|
config=postgres_adapter._config,
|
|
client=mock_client,
|
|
)
|
|
|
|
with patch(
|
|
"python_repositories.adapters.connection_aware_adapter.time.monotonic",
|
|
return_value=100.0,
|
|
):
|
|
assert reinjected.is_connected()
|
|
|
|
assert mock_client.cursor.call_count == 2
|