"""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_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*"))