From 417e476ee2bc226625ac265817449d5472502a1d Mon Sep 17 00:00:00 2001 From: hajdul88 <52442977+hajdul88@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:33:11 +0200 Subject: [PATCH] feat: adds redis adapter and corresponding structure unit tests --- .../cache/redis/test_redis_adapter.py | 283 ++++++++++++++++++ .../databases/cache/test_cache_config.py | 127 ++++++++ .../databases/cache/test_cache_engine.py | 228 ++++++++++++++ 3 files changed, 638 insertions(+) create mode 100644 cognee/tests/unit/infrastructure/databases/cache/redis/test_redis_adapter.py create mode 100644 cognee/tests/unit/infrastructure/databases/cache/test_cache_config.py create mode 100644 cognee/tests/unit/infrastructure/databases/cache/test_cache_engine.py diff --git a/cognee/tests/unit/infrastructure/databases/cache/redis/test_redis_adapter.py b/cognee/tests/unit/infrastructure/databases/cache/redis/test_redis_adapter.py new file mode 100644 index 000000000..fab9c00b1 --- /dev/null +++ b/cognee/tests/unit/infrastructure/databases/cache/redis/test_redis_adapter.py @@ -0,0 +1,283 @@ +"""Tests for the RedisAdapter class.""" + +import pytest +from unittest.mock import MagicMock, patch +import redis +from cognee.infrastructure.databases.cache.redis.RedisAdapter import RedisAdapter +from cognee.infrastructure.databases.cache.cache_db_interface import CacheDBInterface + + +@pytest.fixture +def mock_redis(): + """Fixture to mock redis.Redis client.""" + with patch("redis.Redis") as mock: + yield mock + + +@pytest.fixture +def redis_adapter(mock_redis): + """Fixture to create a RedisAdapter instance with mocked Redis client.""" + adapter = RedisAdapter( + host="localhost", + port=6379, + lock_name="test-lock", + timeout=240, + blocking_timeout=300, + ) + return adapter + + +def test_redis_adapter_initialization(mock_redis): + """Test that RedisAdapter initializes correctly.""" + adapter = RedisAdapter( + host="localhost", + port=6379, + lock_name="my-lock", + timeout=120, + blocking_timeout=150, + ) + + assert adapter.host == "localhost" + assert adapter.port == 6379 + assert adapter.lock_key == "my-lock" + assert adapter.timeout == 120 + assert adapter.blocking_timeout == 150 + assert adapter.lock is None + + mock_redis.assert_called_once_with(host="localhost", port=6379) + + +def test_redis_adapter_inherits_cache_db_interface(mock_redis): + """Test that RedisAdapter properly inherits from CacheDBInterface.""" + adapter = RedisAdapter( + host="localhost", + port=6379, + lock_name="test-lock", + ) + + assert isinstance(adapter, CacheDBInterface) + + +def test_redis_adapter_custom_parameters(mock_redis): + """Test RedisAdapter with custom parameters.""" + adapter = RedisAdapter( + host="redis.example.com", + port=6380, + lock_name="custom-lock-key", + timeout=60, + blocking_timeout=90, + ) + + assert adapter.host == "redis.example.com" + assert adapter.port == 6380 + assert adapter.lock_key == "custom-lock-key" + assert adapter.timeout == 60 + assert adapter.blocking_timeout == 90 + + +def test_acquire_lock_success(redis_adapter): + """Test successful lock acquisition.""" + mock_lock = MagicMock() + mock_lock.acquire.return_value = True + redis_adapter.redis.lock.return_value = mock_lock + + result = redis_adapter.acquire() + + assert result is mock_lock + assert redis_adapter.lock is mock_lock + redis_adapter.redis.lock.assert_called_once_with( + name="test-lock", + timeout=240, + blocking_timeout=300, + ) + mock_lock.acquire.assert_called_once() + + +def test_acquire_lock_failure(redis_adapter): + """Test lock acquisition failure raises RuntimeError.""" + mock_lock = MagicMock() + mock_lock.acquire.return_value = False + redis_adapter.redis.lock.return_value = mock_lock + + with pytest.raises(RuntimeError, match="Could not acquire Redis lock: test-lock"): + redis_adapter.acquire() + + redis_adapter.redis.lock.assert_called_once() + mock_lock.acquire.assert_called_once() + + +def test_acquire_lock_with_custom_parameters(): + """Test lock acquisition with custom timeout parameters.""" + with patch("redis.Redis") as mock_redis_class: + adapter = RedisAdapter( + host="localhost", + port=6379, + lock_name="custom-lock", + timeout=100, + blocking_timeout=200, + ) + + mock_lock = MagicMock() + mock_lock.acquire.return_value = True + adapter.redis.lock.return_value = mock_lock + + adapter.acquire() + + adapter.redis.lock.assert_called_once_with( + name="custom-lock", + timeout=100, + blocking_timeout=200, + ) + + +def test_release_lock_success(redis_adapter): + """Test successful lock release.""" + mock_lock = MagicMock() + redis_adapter.lock = mock_lock + + redis_adapter.release() + + mock_lock.release.assert_called_once() + assert redis_adapter.lock is None + + +def test_release_lock_when_none(redis_adapter): + """Test releasing lock when no lock is held.""" + redis_adapter.lock = None + + redis_adapter.release() + + +def test_release_lock_handles_lock_error(redis_adapter): + """Test that release handles redis.exceptions.LockError gracefully.""" + mock_lock = MagicMock() + mock_lock.release.side_effect = redis.exceptions.LockError("Lock not owned") + redis_adapter.lock = mock_lock + + redis_adapter.release() + + mock_lock.release.assert_called_once() + + +def test_release_lock_propagates_other_exceptions(redis_adapter): + """Test that release propagates non-LockError exceptions.""" + mock_lock = MagicMock() + mock_lock.release.side_effect = redis.exceptions.ConnectionError("Connection lost") + redis_adapter.lock = mock_lock + + with pytest.raises(redis.exceptions.ConnectionError): + redis_adapter.release() + + +def test_hold_context_manager_success(redis_adapter): + """Test hold context manager with successful acquisition and release.""" + mock_lock = MagicMock() + mock_lock.acquire.return_value = True + redis_adapter.redis.lock.return_value = mock_lock + + with redis_adapter.hold(): + assert redis_adapter.lock is mock_lock + mock_lock.acquire.assert_called_once() + + mock_lock.release.assert_called_once() + + +def test_hold_context_manager_with_exception(redis_adapter): + """Test that hold context manager releases lock even when exception occurs.""" + mock_lock = MagicMock() + mock_lock.acquire.return_value = True + redis_adapter.redis.lock.return_value = mock_lock + + with pytest.raises(ValueError): + with redis_adapter.hold(): + mock_lock.acquire.assert_called_once() + raise ValueError("Test exception") + + mock_lock.release.assert_called_once() + + +def test_hold_context_manager_acquire_failure(redis_adapter): + """Test hold context manager when lock acquisition fails.""" + mock_lock = MagicMock() + mock_lock.acquire.return_value = False + redis_adapter.redis.lock.return_value = mock_lock + + with pytest.raises(RuntimeError, match="Could not acquire Redis lock"): + with redis_adapter.hold(): + pass + + mock_lock.release.assert_not_called() + + +def test_multiple_acquire_release_cycles(redis_adapter): + """Test multiple acquire/release cycles.""" + mock_lock1 = MagicMock() + mock_lock1.acquire.return_value = True + mock_lock2 = MagicMock() + mock_lock2.acquire.return_value = True + + redis_adapter.redis.lock.side_effect = [mock_lock1, mock_lock2] + + redis_adapter.acquire() + assert redis_adapter.lock is mock_lock1 + redis_adapter.release() + assert redis_adapter.lock is None + + redis_adapter.acquire() + assert redis_adapter.lock is mock_lock2 + redis_adapter.release() + assert redis_adapter.lock is None + + +def test_redis_adapter_redis_client_initialization(): + """Test that Redis client is initialized with correct connection parameters.""" + with patch("redis.Redis") as mock_redis_class: + mock_redis_instance = MagicMock() + mock_redis_class.return_value = mock_redis_instance + + adapter = RedisAdapter( + host="redis-server.example.com", + port=6380, + lock_name="test-lock", + ) + + mock_redis_class.assert_called_once_with( + host="redis-server.example.com", + port=6380, + ) + assert adapter.redis is mock_redis_instance + + +def test_lock_name_vs_lock_key_parameter(): + """Test that lock_name parameter is correctly assigned to lock_key attribute.""" + with patch("redis.Redis"): + adapter = RedisAdapter( + host="localhost", + port=6379, + lock_name="my-custom-lock-name", + ) + + assert adapter.lock_key == "my-custom-lock-name" + + +def test_default_timeout_parameters(): + """Test default timeout parameters.""" + with patch("redis.Redis"): + adapter = RedisAdapter( + host="localhost", + port=6379, + lock_name="test-lock", + ) + + assert adapter.timeout == 240 + assert adapter.blocking_timeout == 300 + + +def test_release_clears_lock_reference(redis_adapter): + """Test that release clears the lock reference.""" + mock_lock = MagicMock() + redis_adapter.lock = mock_lock + + redis_adapter.release() + + assert redis_adapter.lock is None diff --git a/cognee/tests/unit/infrastructure/databases/cache/test_cache_config.py b/cognee/tests/unit/infrastructure/databases/cache/test_cache_config.py new file mode 100644 index 000000000..c5cd349b0 --- /dev/null +++ b/cognee/tests/unit/infrastructure/databases/cache/test_cache_config.py @@ -0,0 +1,127 @@ +"""Tests for cache configuration.""" + +import pytest +from unittest.mock import patch +from cognee.infrastructure.databases.cache.config import CacheConfig, get_cache_config + + +@pytest.fixture(autouse=True) +def reset_cache_config_singleton(): + """Reset the singleton instance between tests.""" + get_cache_config.cache_clear() + yield + get_cache_config.cache_clear() + + +def test_cache_config_defaults(): + """Test that CacheConfig has the correct default values.""" + config = CacheConfig() + + assert config.caching is False + assert config.shared_kuzu_lock is False + assert config.cache_host == "localhost" + assert config.cache_port == 6379 + assert config.agentic_lock_expire == 240 + assert config.agentic_lock_timeout == 300 + + +def test_cache_config_custom_values(): + """Test that CacheConfig accepts custom values.""" + config = CacheConfig( + caching=True, + shared_kuzu_lock=True, + cache_host="redis.example.com", + cache_port=6380, + agentic_lock_expire=120, + agentic_lock_timeout=180, + ) + + assert config.caching is True + assert config.shared_kuzu_lock is True + assert config.cache_host == "redis.example.com" + assert config.cache_port == 6380 + assert config.agentic_lock_expire == 120 + assert config.agentic_lock_timeout == 180 + + +def test_cache_config_to_dict(): + """Test the to_dict method returns all configuration values.""" + config = CacheConfig( + caching=True, + shared_kuzu_lock=True, + cache_host="test-host", + cache_port=7000, + agentic_lock_expire=100, + agentic_lock_timeout=200, + ) + + config_dict = config.to_dict() + + assert config_dict == { + "caching": True, + "shared_kuzu_lock": True, + "cache_host": "test-host", + "cache_port": 7000, + "agentic_lock_expire": 100, + "agentic_lock_timeout": 200, + } + + +def test_get_cache_config_singleton(): + """Test that get_cache_config returns the same instance.""" + config1 = get_cache_config() + config2 = get_cache_config() + + assert config1 is config2 + + +def test_cache_config_from_environment(): + """Test that CacheConfig reads from environment variables.""" + with patch.dict( + "os.environ", + { + "CACHING": "true", + "SHARED_KUZU_LOCK": "true", + "CACHE_HOST": "env-redis", + "CACHE_PORT": "6380", + "AGENTIC_LOCK_EXPIRE": "300", + "AGENTIC_LOCK_TIMEOUT": "400", + }, + ): + get_cache_config.cache_clear() + config = get_cache_config() + + assert config.caching is True + assert config.shared_kuzu_lock is True + assert config.cache_host == "env-redis" + assert config.cache_port == 6380 + assert config.agentic_lock_expire == 300 + assert config.agentic_lock_timeout == 400 + + +def test_cache_config_extra_fields_allowed(): + """Test that CacheConfig allows extra fields due to extra='allow'.""" + config = CacheConfig(extra_field="extra_value", another_field=123) + + # Extra fields should be accepted without error + assert hasattr(config, "extra_field") + assert config.extra_field == "extra_value" + assert hasattr(config, "another_field") + assert config.another_field == 123 + + +def test_cache_config_port_type_validation(): + """Test that cache_port validates integer type.""" + with pytest.raises(Exception): # Pydantic validation error + CacheConfig(cache_port="not_a_number") + + +def test_cache_config_boolean_type_validation(): + """Test that boolean fields accept various truthy/falsy values.""" + config1 = CacheConfig(caching="true", shared_kuzu_lock="yes") + assert config1.caching is True + assert config1.shared_kuzu_lock is True + + config2 = CacheConfig(caching="false", shared_kuzu_lock="no") + assert config2.caching is False + assert config2.shared_kuzu_lock is False diff --git a/cognee/tests/unit/infrastructure/databases/cache/test_cache_engine.py b/cognee/tests/unit/infrastructure/databases/cache/test_cache_engine.py new file mode 100644 index 000000000..3c0b1942d --- /dev/null +++ b/cognee/tests/unit/infrastructure/databases/cache/test_cache_engine.py @@ -0,0 +1,228 @@ +"""Tests for cache engine factory methods.""" + +import pytest +from unittest.mock import patch, MagicMock +from cognee.infrastructure.databases.cache.get_cache_engine import ( + create_cache_engine, + get_cache_engine, +) +from cognee.infrastructure.databases.cache.redis.RedisAdapter import RedisAdapter + + +@pytest.fixture(autouse=True) +def reset_factory_cache(): + """Reset the lru_cache between tests.""" + create_cache_engine.cache_clear() + yield + create_cache_engine.cache_clear() + + +@pytest.fixture +def mock_cache_config(): + """Fixture to mock cache configuration.""" + with patch("cognee.infrastructure.databases.cache.get_cache_engine.get_cache_config") as mock: + mock_config = MagicMock() + mock_config.caching = True + mock_config.cache_host = "localhost" + mock_config.cache_port = 6379 + mock_config.agentic_lock_expire = 240 + mock_config.agentic_lock_timeout = 300 + mock.return_value = mock_config + yield mock_config + + +def test_create_cache_engine_with_caching_enabled(mock_cache_config): + """Test that create_cache_engine returns RedisAdapter when caching is enabled.""" + with patch("cognee.infrastructure.databases.cache.get_cache_engine.config") as mock_config: + mock_config.caching = True + + with patch("redis.Redis"): + engine = create_cache_engine( + cache_host="localhost", + cache_port=6379, + lock_key="test-lock", + agentic_lock_expire=240, + agentic_lock_timeout=300, + ) + + assert engine is not None + assert isinstance(engine, RedisAdapter) + assert engine.host == "localhost" + assert engine.port == 6379 + assert engine.lock_key == "test-lock" + assert engine.timeout == 240 + assert engine.blocking_timeout == 300 + + +def test_create_cache_engine_with_caching_disabled(): + """Test that create_cache_engine returns None when caching is disabled.""" + with patch("cognee.infrastructure.databases.cache.get_cache_engine.config") as mock_config: + mock_config.caching = False + + engine = create_cache_engine( + cache_host="localhost", + cache_port=6379, + lock_key="test-lock", + ) + + assert engine is None + + +def test_create_cache_engine_caching(): + """Test that create_cache_engine uses lru_cache and returns same instance.""" + with patch("cognee.infrastructure.databases.cache.get_cache_engine.config") as mock_config: + mock_config.caching = True + + with patch("redis.Redis"): + engine1 = create_cache_engine( + cache_host="localhost", + cache_port=6379, + lock_key="test-lock", + ) + + engine2 = create_cache_engine( + cache_host="localhost", + cache_port=6379, + lock_key="test-lock", + ) + + assert engine1 is engine2 + + +def test_create_cache_engine_different_params(): + """Test that create_cache_engine returns different instances for different parameters.""" + with patch("cognee.infrastructure.databases.cache.get_cache_engine.config") as mock_config: + mock_config.caching = True + + with patch("redis.Redis"): + engine1 = create_cache_engine( + cache_host="localhost", + cache_port=6379, + lock_key="lock-1", + ) + + engine2 = create_cache_engine( + cache_host="localhost", + cache_port=6379, + lock_key="lock-2", + ) + + assert engine1 is not engine2 + assert engine1.lock_key == "lock-1" + assert engine2.lock_key == "lock-2" + + +def test_create_cache_engine_custom_timeouts(): + """Test that create_cache_engine accepts custom timeout values.""" + with patch("cognee.infrastructure.databases.cache.get_cache_engine.config") as mock_config: + mock_config.caching = True + + with patch("redis.Redis"): + engine = create_cache_engine( + cache_host="redis.example.com", + cache_port=6380, + lock_key="custom-lock", + agentic_lock_expire=120, + agentic_lock_timeout=150, + ) + + assert isinstance(engine, RedisAdapter) + assert engine.host == "redis.example.com" + assert engine.port == 6380 + assert engine.timeout == 120 + assert engine.blocking_timeout == 150 + + +def test_get_cache_engine_uses_config(mock_cache_config): + """Test that get_cache_engine uses configuration values.""" + with patch("cognee.infrastructure.databases.cache.get_cache_engine.config", mock_cache_config): + mock_cache_config.caching = True + + with patch("redis.Redis"): + with patch( + "cognee.infrastructure.databases.cache.get_cache_engine.create_cache_engine" + ) as mock_create: + mock_create.return_value = MagicMock() + + get_cache_engine("my-lock-key") + + mock_create.assert_called_once_with( + cache_host="localhost", + cache_port=6379, + lock_key="my-lock-key", + agentic_lock_expire=240, + agentic_lock_timeout=300, + ) + + +def test_get_cache_engine_with_custom_config(): + """Test that get_cache_engine properly uses custom config values.""" + with patch("cognee.infrastructure.databases.cache.get_cache_engine.config") as mock_config: + mock_config.caching = True + mock_config.cache_host = "custom-redis" + mock_config.cache_port = 7000 + mock_config.agentic_lock_expire = 100 + mock_config.agentic_lock_timeout = 200 + + with patch("redis.Redis"): + engine = get_cache_engine("test-key") + + assert isinstance(engine, RedisAdapter) + assert engine.host == "custom-redis" + assert engine.port == 7000 + assert engine.lock_key == "test-key" + assert engine.timeout == 100 + assert engine.blocking_timeout == 200 + + +def test_get_cache_engine_when_disabled(): + """Test that get_cache_engine returns None when caching is disabled.""" + with patch("cognee.infrastructure.databases.cache.get_cache_engine.config") as mock_config: + mock_config.caching = False + + engine = get_cache_engine("test-key") + + assert engine is None + + +def test_create_cache_engine_default_timeout_values(): + """Test that create_cache_engine uses default timeout values.""" + with patch("cognee.infrastructure.databases.cache.get_cache_engine.config") as mock_config: + mock_config.caching = True + + with patch("redis.Redis"): + engine = create_cache_engine( + cache_host="localhost", + cache_port=6379, + lock_key="test-lock", + ) + + assert isinstance(engine, RedisAdapter) + assert engine.timeout == 240 + assert engine.blocking_timeout == 300 + + +def test_full_workflow_with_context_manager(): + """Test complete workflow: config -> factory -> adapter with context manager.""" + with patch("cognee.infrastructure.databases.cache.get_cache_engine.config") as mock_config: + mock_config.caching = True + mock_config.cache_host = "localhost" + mock_config.cache_port = 6379 + mock_config.agentic_lock_expire = 240 + mock_config.agentic_lock_timeout = 300 + + with patch("redis.Redis") as mock_redis_class: + mock_redis_instance = MagicMock() + mock_redis_class.return_value = mock_redis_instance + + mock_lock = MagicMock() + mock_lock.acquire.return_value = True + mock_redis_instance.lock.return_value = mock_lock + + engine = get_cache_engine("integration-test-lock") + + assert isinstance(engine, RedisAdapter) + with engine.hold(): + mock_lock.acquire.assert_called_once() + + mock_lock.release.assert_called_once()