# pylint: disable=protected-access # The above line disables pylint's protected member access warnings for this file, # allowing tests to access RedisAdapter's internal methods as needed for integration testing. """Integration tests for the RedisAdapter.""" from collections.abc import Generator import pytest import redis from redis.commands.json.path import Path as RedisPath from python_repositories.adapters.redis_adapter import RedisAdapter @pytest.fixture(scope="module") def data() -> Generator[dict[str, str]]: """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]]]: """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 # Cleanup raw_redis_client.delete(key) @pytest.fixture(scope="module") def redis_adapter(redis_container: str) -> Generator[RedisAdapter]: """Fixture to provide a connected RedisAdapter instance.""" adapter = RedisAdapter() 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.""" # Clear all keys before each test raw_redis_client.flushall() def test_should_adhere_to_interface(redis_container: str) -> None: """Test that the RedisAdapter adheres to the expected interface.""" # Instantiation fails if interface not adhered to _ = RedisAdapter() def test_should_have_logger_when_instantiated(redis_container: str) -> None: """Test that the RedisAdapter has a logger when instantiated.""" adapter = RedisAdapter() assert hasattr(adapter, "logger") assert adapter.logger is not None def test_should_not_be_connected_when_instantiated(redis_container: str) -> None: """Test that the RedisAdapter is not connected when instantiated.""" adapter = RedisAdapter() assert adapter._client is None assert not adapter.is_connected def test_should_raise_connection_error_when_unable_to_connect( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test that the RedisAdapter raises ConnectionError when unable to connect.""" # Arrange monkeypatch.setenv("REDIS_URI", "redis://invalid:6379") adapter = RedisAdapter() # Act & Assert 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.""" # Set an invalid URI monkeypatch.setenv("REDIS_URI", "redis://invalid:6379") # Monkeypatch redis.Redis.from_url to return a mock client class MockRedis: """A mock Redis client that simulates a failed ping.""" def ping(self) -> bool: """Simulate a failed ping.""" return False # Simulate failed ping monkeypatch.setattr("redis.Redis.from_url", lambda *a, **kw: MockRedis()) adapter = RedisAdapter() with pytest.raises(ConnectionError, match="Could not connect to Redis"): adapter.connect() def test_should_log_error_on_exception_during_exit( redis_container: str, caplog: pytest.LogCaptureFixture, ) -> None: """Test that the RedisAdapter logs an error when an exception occurs during context exit.""" try: with RedisAdapter() as adapter: assert adapter.is_connected raise ValueError("Simulated error") except ValueError: pass # Expected # Assert error was logged assert "Error while exiting context" in caplog.text def test_should_have_context_manager(redis_container: str) -> None: """Test that the RedisAdapter can be used as a context manager.""" with RedisAdapter() 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.""" # Arrange key, data = data_in_redis # Act value = redis_adapter._get(key) # Assert 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.""" # Act value = redis_adapter._get("nonexistent_key") # Assert 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.""" # Arrange invalid_keys = ["", 123, None] # Act & Assert 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_adapter: RedisAdapter, ) -> None: """Test that the RedisAdapter raises ConnectionError when getting while not connected.""" # Arrange adapter = RedisAdapter() # not connected # Act & Assert 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.""" # Arrange key = "test_key" received_data = redis_adapter._get(key) assert received_data is None # Ensure key does not exist # Act redis_adapter._set(key, data) # Assert 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.""" # Arrange 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 # Act redis_adapter._set(key, new_data) # Assert 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.""" # Arrange invalid_keys = ["", 123, None] # Act & Assert 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.""" # Arrange key = "test_key" invalid_data = ["", 123, None, [], {}] # Act & Assert 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_adapter: RedisAdapter, data: dict[str, str], ) -> None: """Test that the RedisAdapter raises ConnectionError when setting while not connected.""" # Arrange adapter = RedisAdapter() key = "test_key" # Act & Assert with pytest.raises(ConnectionError): adapter._set(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.""" # Arrange key, _ = data_in_redis received_data = redis_adapter._get(key) assert received_data is not None # Ensure key exists # Act redis_adapter._delete(key) # Assert 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.""" # Arrange invalid_keys = ["", 123, None] # Act & Assert 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_adapter: RedisAdapter, ) -> None: """Test that the RedisAdapter raises ConnectionError when deleting while not connected.""" # Arrange adapter = RedisAdapter() # Act & Assert 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.""" # Arrange redis_adapter._set("key1", {"a": 1}) redis_adapter._set("key2", {"b": 2}) # Act keys = redis_adapter._list_keys("key*") # Assert 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.""" # Arrange invalid_patterns = ["", 123, None] # Act & Assert 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_adapter: RedisAdapter, ) -> None: """Test that the RedisAdapter raises ConnectionError when listing keys while not connected.""" # Arrange adapter = RedisAdapter() # Act & Assert with pytest.raises(ConnectionError): adapter._list_keys("some_pattern") # allows local debugging by running file as script if __name__ == "__main__": pytest.main(["-s", "-v", __file__])