"""Integration tests for the RedisAdapter.""" from collections.abc import Generator import logging import pytest import redis from redis.commands.json.path import Path as RedisPath from python_repositories.adapters.redis_adapter import RedisAdapter from python_repositories.config import RedisConfig pytestmark = [pytest.mark.integration, pytest.mark.needs_redis] @pytest.fixture(scope="module") def data() -> Generator[dict[str, str], None, None]: """Provide a sample data dictionary for tests.""" yield {"foo": "bar"} @pytest.fixture(scope="function") def data_in_redis( raw_redis_client: redis.Redis, data: dict[str, str], ) -> Generator[tuple[str, dict[str, str]], None, None]: """Fixture to set up a known value in Redis before each test.""" key = "test_key" path = RedisPath.root_path() raw_redis_client.json().set(key, path, data) yield key, data raw_redis_client.delete(key) @pytest.fixture(scope="module") def redis_adapter(redis_config: RedisConfig) -> Generator[RedisAdapter, None, None]: """Fixture to provide a connected RedisAdapter instance.""" adapter = RedisAdapter(config=redis_config) adapter.connect() yield adapter adapter.disconnect() @pytest.fixture(scope="function", autouse=True) def clear_redis(raw_redis_client: redis.Redis) -> None: """Fixture to clear all Redis keys before each test.""" raw_redis_client.flushall() def test_should_log_info_when_already_connected( redis_adapter: RedisAdapter, caplog: pytest.LogCaptureFixture, ) -> None: """Test that the RedisAdapter logs info when connect is called while already connected.""" with caplog.at_level(logging.INFO): redis_adapter.connect() assert "Already connected" in caplog.text assert "Redis" in caplog.text def test_should_raise_connection_error_when_unable_to_connect() -> None: """Test that the RedisAdapter raises ConnectionError when unable to connect.""" adapter = RedisAdapter(config=RedisConfig(uri="redis://invalid:6379")) with pytest.raises(ConnectionError): adapter.connect() assert adapter._client is None assert not adapter.is_connected() def test_connect_raises_connection_error_when_unable_to_ping( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test that the RedisAdapter raises ConnectionError when ping fails.""" class MockRedis: """A mock Redis client that simulates a failed ping.""" def ping(self) -> bool: return False monkeypatch.setattr("redis.Redis.from_url", lambda *a, **kw: MockRedis()) adapter = RedisAdapter(config=RedisConfig(uri="redis://invalid:6379")) with pytest.raises(ConnectionError, match="Could not connect to Redis"): adapter.connect() def test_should_log_error_on_exception_during_exit( redis_config: RedisConfig, caplog: pytest.LogCaptureFixture, ) -> None: """Test that the RedisAdapter logs an error when an exception occurs during context exit.""" try: with RedisAdapter(config=redis_config) as adapter: assert adapter.is_connected() raise ValueError("Simulated error") except ValueError: pass assert "Error while exiting context" in caplog.text def test_should_have_context_manager(redis_config: RedisConfig) -> None: """Test that the RedisAdapter can be used as a context manager.""" with RedisAdapter(config=redis_config) as adapter: assert adapter._client is not None assert adapter._client is None def test_should_get_value( data_in_redis: tuple[str, dict[str, str]], redis_adapter: RedisAdapter, ) -> None: """Test that the RedisAdapter can get a value.""" key, data = data_in_redis value = redis_adapter.get(key) assert value is not None assert value == data def test_should_get_none_for_missing_key( redis_adapter: RedisAdapter, ) -> None: """Test that getting a non-existent key returns None.""" value = redis_adapter.get("nonexistent_key") assert value is None def test_should_raise_value_error_on_invalid_get_key( redis_adapter: RedisAdapter, ) -> None: """Test that the RedisAdapter raises ValueError when getting with an invalid key.""" invalid_keys = ["", 123, None] for key in invalid_keys: with pytest.raises(ValueError): redis_adapter.get(key) # type: ignore def test_should_raise_connection_error_on_get_when_not_connected( redis_config: RedisConfig, ) -> None: """Test that the RedisAdapter raises ConnectionError when getting while not connected.""" adapter = RedisAdapter(config=redis_config) with pytest.raises(ConnectionError): adapter.get("some_key") def test_should_set_value( data: dict[str, str], redis_adapter: RedisAdapter, ) -> None: """Test that the RedisAdapter can set a value.""" key = "test_key" received_data = redis_adapter.get(key) assert received_data is None redis_adapter.set(key, data) received_data = redis_adapter.get(key) assert received_data is not None assert received_data == data def test_should_update_value( data_in_redis: tuple[str, dict[str, str]], redis_adapter: RedisAdapter, ) -> None: """Test that the RedisAdapter can update an existing value.""" key, _ = data_in_redis new_data = {"new_key": "new_value"} received_data = redis_adapter.get(key) assert received_data is not None assert received_data != new_data redis_adapter.set(key, new_data) assert redis_adapter.get(key) == new_data def test_should_raise_value_error_on_invalid_set_key( redis_adapter: RedisAdapter, data: dict[str, str], ) -> None: """Test that the RedisAdapter raises ValueError when setting with an invalid key.""" invalid_keys = ["", 123, None] for key in invalid_keys: with pytest.raises(ValueError): redis_adapter.set(key, data) # type: ignore def test_should_raise_value_error_on_invalid_set_data( redis_adapter: RedisAdapter, ) -> None: """Test that the RedisAdapter raises ValueError when setting with invalid data.""" key = "test_key" invalid_data = ["", 123, None, [], {}] for data in invalid_data: with pytest.raises(ValueError): redis_adapter.set(key, data) # type: ignore def test_should_raise_connection_error_on_set_when_not_connected( redis_config: RedisConfig, data: dict[str, str], ) -> None: """Test that the RedisAdapter raises ConnectionError when setting while not connected.""" adapter = RedisAdapter(config=redis_config) with pytest.raises(ConnectionError): adapter.set("test_key", data) def test_should_delete_key( data_in_redis: tuple[str, dict[str, str]], redis_adapter: RedisAdapter, ) -> None: """Test that deleting a key removes it from Redis.""" key, _ = data_in_redis received_data = redis_adapter.get(key) assert received_data is not None redis_adapter.delete(key) assert redis_adapter.get(key) is None def test_should_raise_value_error_on_invalid_delete_key( redis_adapter: RedisAdapter, ) -> None: """Test that the RedisAdapter raises ValueError when deleting with an invalid key.""" invalid_keys = ["", 123, None] for key in invalid_keys: with pytest.raises(ValueError): redis_adapter.delete(key) # type: ignore def test_should_raise_connection_error_on_delete_when_not_connected( redis_config: RedisConfig, ) -> None: """Test that the RedisAdapter raises ConnectionError when deleting while not connected.""" adapter = RedisAdapter(config=redis_config) with pytest.raises(ConnectionError): adapter.delete("some_key") def test_should_list_keys( redis_adapter: RedisAdapter, ) -> None: """Test listing keys matching a pattern returns correct keys.""" redis_adapter.set("key1", {"a": 1}) redis_adapter.set("key2", {"b": 2}) keys = redis_adapter.list_keys("key*") assert set(keys) == {"key1", "key2"} def test_should_raise_value_error_on_invalid_list_keys_pattern( redis_adapter: RedisAdapter, ) -> None: """Test that the RedisAdapter raises ValueError when listing keys with an invalid pattern.""" invalid_patterns = ["", 123, None] for pattern in invalid_patterns: with pytest.raises(ValueError): redis_adapter.list_keys(pattern) # type: ignore def test_should_raise_connection_error_on_list_keys_when_not_connected( redis_config: RedisConfig, ) -> None: """Test that the RedisAdapter raises ConnectionError when listing keys while not connected.""" adapter = RedisAdapter(config=redis_config) with pytest.raises(ConnectionError): adapter.list_keys("some_pattern") def test_should_scan_keys( redis_adapter: RedisAdapter, ) -> None: """Test scanning keys matching a pattern returns correct keys.""" redis_adapter.set("key1", {"a": 1}) redis_adapter.set("key2", {"b": 2}) keys = set(redis_adapter.scan_keys("key*")) assert keys == {"key1", "key2"} def test_should_raise_value_error_on_invalid_scan_keys_pattern( redis_adapter: RedisAdapter, ) -> None: """Test that the RedisAdapter raises ValueError when scanning keys with an invalid pattern.""" invalid_patterns = ["", 123, None] for pattern in invalid_patterns: with pytest.raises(ValueError): list(redis_adapter.scan_keys(pattern)) # type: ignore[arg-type] def test_should_raise_connection_error_on_scan_keys_when_not_connected( redis_config: RedisConfig, ) -> None: """Test that the RedisAdapter raises ConnectionError when scanning keys while not connected.""" adapter = RedisAdapter(config=redis_config) with pytest.raises(ConnectionError): list(adapter.scan_keys("some_pattern")) if __name__ == "__main__": pytest.main(["-s", "-v", __file__])