PR Title Check / check-title (pull_request) Successful in 5s
Code Quality Pipeline / code-quality (pull_request) Successful in 26s
Test Python Package / integration-tests (pull_request) Successful in 1m5s
Test Python Package / unit-tests (pull_request) Successful in 29s
Test Python Package / coverage-report (pull_request) Successful in 41s
Restore 100% coverage for the _validate_injected_client early-return paths in Redis and MinIO adapters. Co-authored-by: Cursor <cursoragent@cursor.com>
196 lines
6.4 KiB
Python
196 lines
6.4 KiB
Python
"""Unit tests for RedisAdapter instantiation and injection."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
import redis
|
|
|
|
from python_repositories.adapters.redis_adapter import RedisAdapter
|
|
from python_repositories.interfaces import JsonRepositoryInterface
|
|
from tests.conftest import TEST_REDIS_CONFIG
|
|
|
|
|
|
def test_should_adhere_to_interface() -> None:
|
|
assert issubclass(RedisAdapter, JsonRepositoryInterface)
|
|
_ = RedisAdapter(config=TEST_REDIS_CONFIG)
|
|
|
|
|
|
def test_should_have_logger_when_instantiated() -> None:
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG)
|
|
assert hasattr(adapter, "logger")
|
|
assert adapter.logger is not None
|
|
|
|
|
|
def test_should_not_be_connected_when_instantiated() -> None:
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG)
|
|
assert adapter._client is None
|
|
assert not adapter.is_connected()
|
|
|
|
|
|
def test_constructs_with_injected_config_without_env(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.delenv("REDIS_URI", raising=False)
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG)
|
|
assert adapter._config == TEST_REDIS_CONFIG
|
|
|
|
|
|
def test_constructs_with_injected_client_without_env(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.delenv("REDIS_URI", raising=False)
|
|
mock_client = MagicMock(spec=redis.Redis)
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG, client=mock_client)
|
|
assert adapter._client is mock_client
|
|
assert adapter._client_injected is True
|
|
|
|
|
|
def test_raises_when_client_provided_without_config() -> None:
|
|
mock_client = MagicMock(spec=redis.Redis)
|
|
with pytest.raises(ValueError, match="config is required"):
|
|
RedisAdapter(client=mock_client)
|
|
|
|
|
|
def test_disconnect_does_not_close_injected_client() -> None:
|
|
mock_client = MagicMock(spec=redis.Redis)
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG, client=mock_client)
|
|
adapter.disconnect()
|
|
mock_client.close.assert_not_called()
|
|
assert adapter._client is None
|
|
|
|
|
|
class CustomEnvRedisAdapter(RedisAdapter):
|
|
uri_env_var_name = "CUSTOM_REDIS_URI"
|
|
|
|
|
|
def test_subclass_custom_env_var_name(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("CUSTOM_REDIS_URI", "redis://custom:6379")
|
|
adapter = CustomEnvRedisAdapter()
|
|
assert adapter._config.uri == "redis://custom:6379"
|
|
|
|
|
|
def test_connect_with_injected_client_succeeds_when_ping_ok() -> None:
|
|
mock_client = MagicMock(spec=redis.Redis)
|
|
mock_client.ping.return_value = True
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG, client=mock_client)
|
|
|
|
adapter.connect()
|
|
|
|
mock_client.ping.assert_called_once()
|
|
|
|
|
|
def test_connect_with_injected_client_raises_when_ping_false() -> None:
|
|
mock_client = MagicMock(spec=redis.Redis)
|
|
mock_client.ping.return_value = False
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG, client=mock_client)
|
|
|
|
with pytest.raises(ConnectionError, match="Could not connect to Redis"):
|
|
adapter.connect()
|
|
|
|
|
|
def test_connect_with_injected_client_raises_on_redis_error() -> None:
|
|
mock_client = MagicMock(spec=redis.Redis)
|
|
mock_client.ping.side_effect = redis.ConnectionError("connection lost")
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG, client=mock_client)
|
|
|
|
with pytest.raises(ConnectionError, match="Could not connect to Redis"):
|
|
adapter.connect()
|
|
|
|
|
|
def test_connect_with_injected_client_skips_validation_when_client_cleared() -> None:
|
|
mock_client = MagicMock(spec=redis.Redis)
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG, client=mock_client)
|
|
adapter.disconnect()
|
|
|
|
adapter.connect()
|
|
|
|
mock_client.ping.assert_not_called()
|
|
|
|
|
|
def test_connect_reconnects_when_existing_client_unhealthy(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
stale_client = MagicMock(spec=redis.Redis)
|
|
stale_client.ping.side_effect = redis.ConnectionError("connection lost")
|
|
new_client = MagicMock(spec=redis.Redis)
|
|
new_client.ping.return_value = True
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG)
|
|
adapter._client = stale_client
|
|
|
|
monkeypatch.setattr("redis.Redis.from_url", lambda *args, **kwargs: new_client)
|
|
|
|
adapter.connect()
|
|
|
|
stale_client.close.assert_called_once()
|
|
assert adapter._client is new_client
|
|
|
|
|
|
def test_connect_skips_reconnect_when_already_connected(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
stale_client = MagicMock(spec=redis.Redis)
|
|
stale_client.ping.return_value = True
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG)
|
|
adapter._client = stale_client
|
|
|
|
from_url = MagicMock()
|
|
monkeypatch.setattr("redis.Redis.from_url", from_url)
|
|
|
|
adapter.connect()
|
|
|
|
stale_client.close.assert_not_called()
|
|
from_url.assert_not_called()
|
|
assert adapter._client is stale_client
|
|
|
|
|
|
def test_scan_keys_yields_decoded_keys() -> None:
|
|
mock_client = MagicMock(spec=redis.Redis)
|
|
mock_client.scan_iter.return_value = iter([b"key1", b"key2"])
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG, client=mock_client)
|
|
|
|
keys = list(adapter.scan_keys("key*"))
|
|
|
|
assert keys == ["key1", "key2"]
|
|
mock_client.scan_iter.assert_called_once_with(match="key*")
|
|
mock_client.keys.assert_not_called()
|
|
|
|
|
|
def test_scan_keys_forwards_count() -> None:
|
|
mock_client = MagicMock(spec=redis.Redis)
|
|
mock_client.scan_iter.return_value = iter([b"key1"])
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG, client=mock_client)
|
|
|
|
keys = list(adapter.scan_keys("key*", count=50))
|
|
|
|
assert keys == ["key1"]
|
|
mock_client.scan_iter.assert_called_once_with(match="key*", count=50)
|
|
|
|
|
|
def test_list_keys_raises_value_error_on_invalid_pattern() -> None:
|
|
mock_client = MagicMock(spec=redis.Redis)
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG, client=mock_client)
|
|
|
|
invalid_patterns = ["", 123, None]
|
|
for pattern in invalid_patterns:
|
|
with pytest.raises(ValueError, match="Pattern must be a non-empty string"):
|
|
adapter.list_keys(pattern) # type: ignore[arg-type]
|
|
|
|
|
|
def test_scan_keys_raises_value_error_on_invalid_pattern() -> None:
|
|
mock_client = MagicMock(spec=redis.Redis)
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG, client=mock_client)
|
|
|
|
invalid_patterns = ["", 123, None]
|
|
for pattern in invalid_patterns:
|
|
with pytest.raises(ValueError, match="Pattern must be a non-empty string"):
|
|
list(adapter.scan_keys(pattern)) # type: ignore[arg-type]
|
|
|
|
|
|
def test_scan_keys_raises_connection_error_when_not_connected() -> None:
|
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG)
|
|
|
|
with pytest.raises(ConnectionError):
|
|
list(adapter.scan_keys("key*"))
|