diff --git a/src/cachekit/backends/provider.py b/src/cachekit/backends/provider.py index 4ef8e26..8d617ba 100644 --- a/src/cachekit/backends/provider.py +++ b/src/cachekit/backends/provider.py @@ -102,32 +102,79 @@ async def get_async_client(self): class DefaultBackendProvider(BackendProviderInterface): - """Default backend provider using Redis backend. + """Auto-detecting backend provider based on environment variables. - Creates RedisBackendProvider singleton with connection pooling. - Delegates to RedisBackendProvider.get_backend() for per-request wrappers. + Resolution priority (first match wins): + 1. CACHEKIT_API_KEY → CachekitIOBackend + 2. CACHEKIT_REDIS_URL → RedisBackend + 3. CACHEKIT_MEMCACHED_SERVERS → MemcachedBackend + 4. CACHEKIT_FILE_CACHE_DIR → FileBackend + 5. REDIS_URL (fallback) → RedisBackend For single-tenant deployments (default), sets tenant_context to "default". For multi-tenant deployments, tenant_context must be set externally. """ def __init__(self): - self._provider = None + self._backend = None def get_backend(self): - """Get per-request backend instance from singleton provider.""" - if self._provider is None: + """Get backend instance, auto-detected from environment on first call.""" + if self._backend is None: + self._backend = self._resolve_backend() + return self._backend + + def _resolve_backend(self): + """Resolve backend from environment variables (priority order).""" + import logging + import os + + logger = logging.getLogger(__name__) + + api_key = os.environ.get("CACHEKIT_API_KEY") + redis_url = os.environ.get("CACHEKIT_REDIS_URL") + memcached_servers = os.environ.get("CACHEKIT_MEMCACHED_SERVERS") + file_cache_dir = os.environ.get("CACHEKIT_FILE_CACHE_DIR") + redis_url_fallback = os.environ.get("REDIS_URL") + + if api_key: + if redis_url or redis_url_fallback: + logger.warning("Both CACHEKIT_API_KEY and Redis URL configured; using CachekitIO (higher priority)") + from cachekit.backends.cachekitio import CachekitIOBackend + + return CachekitIOBackend() # reads from env via pydantic-settings + + if redis_url or redis_url_fallback: from cachekit.backends.redis.config import RedisBackendConfig from cachekit.backends.redis.provider import RedisBackendProvider, tenant_context redis_config = RedisBackendConfig.from_env() - self._provider = RedisBackendProvider(redis_url=redis_config.redis_url) - - # Set default tenant for single-tenant mode (if not already set) + provider = RedisBackendProvider(redis_url=redis_config.redis_url) if tenant_context.get() is None: tenant_context.set("default") + return provider.get_backend() + + if memcached_servers: + from cachekit.backends.memcached import MemcachedBackend + + return MemcachedBackend() # reads from env via pydantic-settings + + if file_cache_dir: + from cachekit.backends.file import FileBackend, FileBackendConfig + + config = FileBackendConfig.from_env() + return FileBackend(config) + + # No backend env vars found — fall back to Redis (will fail at connection time + # with a clear error if no Redis is available) + from cachekit.backends.redis.config import RedisBackendConfig + from cachekit.backends.redis.provider import RedisBackendProvider, tenant_context - return self._provider.get_backend() + redis_config = RedisBackendConfig.from_env() + provider = RedisBackendProvider(redis_url=redis_config.redis_url) + if tenant_context.get() is None: + tenant_context.set("default") + return provider.get_backend() __all__ = [ diff --git a/tests/unit/backends/test_provider.py b/tests/unit/backends/test_provider.py index 8d7d4c7..2a57752 100644 --- a/tests/unit/backends/test_provider.py +++ b/tests/unit/backends/test_provider.py @@ -5,7 +5,7 @@ - SimpleLogger all logging methods - DefaultLoggerProvider - DefaultCacheClientProvider (sync and async) -- DefaultBackendProvider with lazy initialization and tenant context +- DefaultBackendProvider auto-detection from environment variables """ from __future__ import annotations @@ -31,16 +31,12 @@ class TestCacheClientProvider: """Test CacheClientProvider abstract interface.""" def test_get_sync_client_not_implemented(self) -> None: - """Test get_sync_client raises NotImplementedError.""" provider = CacheClientProvider() - with pytest.raises(NotImplementedError): provider.get_sync_client() def test_get_async_client_not_implemented(self) -> None: - """Test get_async_client raises NotImplementedError.""" provider = CacheClientProvider() - with pytest.raises(NotImplementedError): import asyncio @@ -52,9 +48,7 @@ class TestLoggerProvider: """Test LoggerProvider abstract interface.""" def test_get_logger_not_implemented(self) -> None: - """Test get_logger raises NotImplementedError.""" provider = LoggerProvider() - with pytest.raises(NotImplementedError): provider.get_logger("test") @@ -64,112 +58,75 @@ class TestSimpleLogger: """Test SimpleLogger wrapper for cache-specific logging.""" def test_debug_message(self) -> None: - """Test debug logging.""" mock_logger = mock.MagicMock() logger = SimpleLogger(mock_logger) - logger.debug("test message") - mock_logger.debug.assert_called_once_with("test message") def test_info_message(self) -> None: - """Test info logging.""" mock_logger = mock.MagicMock() logger = SimpleLogger(mock_logger) - logger.info("test info") - mock_logger.info.assert_called_once_with("test info") def test_info_message_with_extra(self) -> None: - """Test info logging with extra parameter (ignored by SimpleLogger).""" mock_logger = mock.MagicMock() logger = SimpleLogger(mock_logger) - logger.info("test info", extra={"key": "value"}) - - # SimpleLogger ignores extra parameter mock_logger.info.assert_called_once_with("test info") def test_warning_message(self) -> None: - """Test warning logging.""" mock_logger = mock.MagicMock() logger = SimpleLogger(mock_logger) - logger.warning("test warning") - mock_logger.warning.assert_called_once_with("test warning") def test_error_message(self) -> None: - """Test error logging.""" mock_logger = mock.MagicMock() logger = SimpleLogger(mock_logger) - logger.error("test error") - mock_logger.error.assert_called_once_with("test error") def test_cache_hit_default_source(self) -> None: - """Test cache hit logging with default source.""" mock_logger = mock.MagicMock() logger = SimpleLogger(mock_logger) - logger.cache_hit("key:123") - mock_logger.debug.assert_called_once_with("Redis cache hit for key: key:123") def test_cache_hit_custom_source(self) -> None: - """Test cache hit logging with custom source.""" mock_logger = mock.MagicMock() logger = SimpleLogger(mock_logger) - logger.cache_hit("key:456", source="Memcached") - mock_logger.debug.assert_called_once_with("Memcached cache hit for key: key:456") def test_cache_miss(self) -> None: - """Test cache miss logging.""" mock_logger = mock.MagicMock() logger = SimpleLogger(mock_logger) - logger.cache_miss("key:789") - mock_logger.debug.assert_called_once_with("Cache miss for key: key:789") def test_cache_stored_without_ttl(self) -> None: - """Test cache storage logging without TTL.""" mock_logger = mock.MagicMock() logger = SimpleLogger(mock_logger) - logger.cache_stored("key:111") - mock_logger.debug.assert_called_once_with("Cached result for key: key:111") def test_cache_stored_with_ttl(self) -> None: - """Test cache storage logging with TTL.""" mock_logger = mock.MagicMock() logger = SimpleLogger(mock_logger) - logger.cache_stored("key:222", ttl=3600) - mock_logger.debug.assert_called_once_with("Cached result for key: key:222 with TTL 3600") def test_cache_invalidated_default_source(self) -> None: - """Test cache invalidation logging with default source.""" mock_logger = mock.MagicMock() logger = SimpleLogger(mock_logger) - logger.cache_invalidated("key:333") - mock_logger.debug.assert_called_once_with("Invalidated Redis cache for key: key:333") def test_cache_invalidated_custom_source(self) -> None: - """Test cache invalidation logging with custom source.""" mock_logger = mock.MagicMock() logger = SimpleLogger(mock_logger) - logger.cache_invalidated("key:444", source="L1") - mock_logger.debug.assert_called_once_with("Invalidated L1 cache for key: key:444") @@ -178,30 +135,20 @@ class TestDefaultLoggerProvider: """Test DefaultLoggerProvider creates SimpleLogger instances.""" def test_get_logger_returns_simple_logger(self) -> None: - """Test get_logger returns a SimpleLogger instance.""" provider = DefaultLoggerProvider() - logger = provider.get_logger("test.module") - assert isinstance(logger, SimpleLogger) def test_get_logger_uses_standard_logging(self) -> None: - """Test get_logger wraps standard Python logger.""" provider = DefaultLoggerProvider() - logger = provider.get_logger("test.module") - - # The inner logger should be a standard Python logger assert isinstance(logger._logger, logging.Logger) assert logger._logger.name == "test.module" def test_get_logger_different_names(self) -> None: - """Test get_logger creates loggers with different names.""" provider = DefaultLoggerProvider() - logger1 = provider.get_logger("module1") logger2 = provider.get_logger("module2") - assert logger1._logger.name == "module1" assert logger2._logger.name == "module2" @@ -211,9 +158,7 @@ class TestBackendProviderInterface: """Test BackendProviderInterface abstract interface.""" def test_get_backend_not_implemented(self) -> None: - """Test get_backend raises NotImplementedError.""" provider = BackendProviderInterface() - with pytest.raises(NotImplementedError): provider.get_backend() @@ -223,168 +168,214 @@ class TestDefaultCacheClientProvider: """Test DefaultCacheClientProvider implementation.""" def test_get_sync_client(self) -> None: - """Test get_sync_client returns a Redis client.""" provider = DefaultCacheClientProvider() - with mock.patch("cachekit.backends.redis.client.get_cached_redis_client") as mock_get: mock_client = mock.MagicMock() mock_get.return_value = mock_client - client = provider.get_sync_client() - assert client is mock_client mock_get.assert_called_once() @pytest.mark.asyncio async def test_get_async_client(self) -> None: - """Test get_async_client returns an async Redis client.""" provider = DefaultCacheClientProvider() - with mock.patch("cachekit.backends.redis.client.get_cached_async_redis_client") as mock_get: mock_client = mock.AsyncMock() mock_get.return_value = mock_client - client = await provider.get_async_client() - assert client is mock_client mock_get.assert_called_once() @pytest.mark.unit -class TestDefaultBackendProvider: - """Test DefaultBackendProvider lazy initialization and tenant context.""" - - def test_init_provider_is_none(self) -> None: - """Test __init__ sets _provider to None.""" - provider = DefaultBackendProvider() - - assert provider._provider is None - - def test_get_backend_lazy_initialization(self) -> None: - """Test get_backend creates provider on first call.""" - provider = DefaultBackendProvider() - - with mock.patch("cachekit.backends.redis.config.RedisBackendConfig") as mock_config_class: - with mock.patch("cachekit.backends.redis.provider.RedisBackendProvider") as mock_provider_class: - with mock.patch("cachekit.backends.redis.provider.tenant_context") as mock_context: - mock_config_instance = mock.MagicMock() - mock_config_class.from_env.return_value = mock_config_instance - mock_config_instance.redis_url = "redis://localhost:6379" - - mock_provider_instance = mock.MagicMock() - mock_backend_instance = mock.MagicMock() - mock_provider_class.return_value = mock_provider_instance - mock_provider_instance.get_backend.return_value = mock_backend_instance - - mock_context.get.return_value = None - - backend = provider.get_backend() - - assert backend is mock_backend_instance - mock_config_class.from_env.assert_called_once() - mock_provider_class.assert_called_once_with(redis_url="redis://localhost:6379") - - def test_get_backend_uses_cached_provider(self) -> None: - """Test get_backend reuses provider on subsequent calls.""" - provider = DefaultBackendProvider() - - with mock.patch("cachekit.backends.redis.config.RedisBackendConfig") as mock_config_class: - with mock.patch("cachekit.backends.redis.provider.RedisBackendProvider") as mock_provider_class: - with mock.patch("cachekit.backends.redis.provider.tenant_context") as mock_context: - mock_config_instance = mock.MagicMock() - mock_config_class.from_env.return_value = mock_config_instance - mock_config_instance.redis_url = "redis://localhost:6379" - - mock_provider_instance = mock.MagicMock() - mock_backend_instance = mock.MagicMock() - mock_provider_class.return_value = mock_provider_instance - mock_provider_instance.get_backend.return_value = mock_backend_instance - - mock_context.get.return_value = None - - # First call - backend1 = provider.get_backend() - - # Second call - backend2 = provider.get_backend() - - # Should use cached provider - assert backend1 is mock_backend_instance - assert backend2 is mock_backend_instance - # RedisBackendProvider should only be instantiated once - mock_provider_class.assert_called_once() - - def test_get_backend_sets_default_tenant_on_init(self) -> None: - """Test get_backend sets default tenant context if not already set.""" - provider = DefaultBackendProvider() - - with mock.patch("cachekit.backends.redis.config.RedisBackendConfig") as mock_config_class: - with mock.patch("cachekit.backends.redis.provider.RedisBackendProvider") as mock_provider_class: - with mock.patch("cachekit.backends.redis.provider.tenant_context") as mock_context: - mock_config_instance = mock.MagicMock() - mock_config_class.from_env.return_value = mock_config_instance - mock_config_instance.redis_url = "redis://localhost:6379" - - mock_provider_instance = mock.MagicMock() - mock_backend_instance = mock.MagicMock() - mock_provider_class.return_value = mock_provider_instance - mock_provider_instance.get_backend.return_value = mock_backend_instance - - # Tenant context is None initially - mock_context.get.return_value = None - - provider.get_backend() - - # Should set default tenant - mock_context.set.assert_called_once_with("default") - - def test_get_backend_skips_tenant_setup_if_already_set(self) -> None: - """Test get_backend doesn't override existing tenant context.""" - provider = DefaultBackendProvider() - - with mock.patch("cachekit.backends.redis.config.RedisBackendConfig") as mock_config_class: - with mock.patch("cachekit.backends.redis.provider.RedisBackendProvider") as mock_provider_class: - with mock.patch("cachekit.backends.redis.provider.tenant_context") as mock_context: - mock_config_instance = mock.MagicMock() - mock_config_class.from_env.return_value = mock_config_instance - mock_config_instance.redis_url = "redis://localhost:6379" - - mock_provider_instance = mock.MagicMock() - mock_backend_instance = mock.MagicMock() - mock_provider_class.return_value = mock_provider_instance - mock_provider_instance.get_backend.return_value = mock_backend_instance - - # Tenant context is already set - mock_context.get.return_value = "existing-tenant" - - provider.get_backend() - - # Should NOT call set - mock_context.set.assert_not_called() - - def test_get_backend_multiple_calls_no_tenant_override(self) -> None: - """Test that subsequent calls don't reset tenant context.""" - provider = DefaultBackendProvider() - - with mock.patch("cachekit.backends.redis.config.RedisBackendConfig") as mock_config_class: - with mock.patch("cachekit.backends.redis.provider.RedisBackendProvider") as mock_provider_class: - with mock.patch("cachekit.backends.redis.provider.tenant_context") as mock_context: - mock_config_instance = mock.MagicMock() - mock_config_class.from_env.return_value = mock_config_instance - mock_config_instance.redis_url = "redis://localhost:6379" - - mock_provider_instance = mock.MagicMock() - mock_backend_instance = mock.MagicMock() - mock_provider_class.return_value = mock_provider_instance - mock_provider_instance.get_backend.return_value = mock_backend_instance - - # First call - tenant is None - mock_context.get.return_value = None - provider.get_backend() - - # Second call - tenant was set by first call - mock_context.get.return_value = "default" - provider.get_backend() - - # Should only set once (on first call) - mock_context.set.assert_called_once_with("default") +class TestDefaultBackendProviderAutoDetect: + """Test DefaultBackendProvider auto-detection from environment variables.""" + + def test_cachekitio_from_api_key(self, monkeypatch: pytest.MonkeyPatch) -> None: + """CACHEKIT_API_KEY resolves to CachekitIOBackend.""" + monkeypatch.setenv("CACHEKIT_API_KEY", "ck_test_abc123") + monkeypatch.delenv("CACHEKIT_REDIS_URL", raising=False) + monkeypatch.delenv("REDIS_URL", raising=False) + monkeypatch.delenv("CACHEKIT_MEMCACHED_SERVERS", raising=False) + monkeypatch.delenv("CACHEKIT_FILE_CACHE_DIR", raising=False) + + with mock.patch("cachekit.backends.cachekitio.CachekitIOBackend") as mock_backend: + mock_instance = mock.MagicMock() + mock_backend.return_value = mock_instance + + provider = DefaultBackendProvider() + backend = provider.get_backend() + + assert backend is mock_instance + mock_backend.assert_called_once() + + def test_redis_from_cachekit_redis_url(self, monkeypatch: pytest.MonkeyPatch) -> None: + """CACHEKIT_REDIS_URL resolves to RedisBackend.""" + monkeypatch.setenv("CACHEKIT_REDIS_URL", "redis://localhost:6379") + monkeypatch.delenv("CACHEKIT_API_KEY", raising=False) + monkeypatch.delenv("REDIS_URL", raising=False) + monkeypatch.delenv("CACHEKIT_MEMCACHED_SERVERS", raising=False) + monkeypatch.delenv("CACHEKIT_FILE_CACHE_DIR", raising=False) + + with mock.patch("cachekit.backends.redis.provider.RedisBackendProvider") as mock_provider: + with mock.patch("cachekit.backends.redis.provider.tenant_context") as mock_ctx: + mock_ctx.get.return_value = None + mock_inst = mock.MagicMock() + mock_backend = mock.MagicMock() + mock_provider.return_value = mock_inst + mock_inst.get_backend.return_value = mock_backend + + provider = DefaultBackendProvider() + backend = provider.get_backend() + + assert backend is mock_backend + mock_provider.assert_called_once() + + def test_redis_from_fallback_redis_url(self, monkeypatch: pytest.MonkeyPatch) -> None: + """REDIS_URL (fallback) resolves to RedisBackend.""" + monkeypatch.setenv("REDIS_URL", "redis://fallback:6379") + monkeypatch.delenv("CACHEKIT_API_KEY", raising=False) + monkeypatch.delenv("CACHEKIT_REDIS_URL", raising=False) + monkeypatch.delenv("CACHEKIT_MEMCACHED_SERVERS", raising=False) + monkeypatch.delenv("CACHEKIT_FILE_CACHE_DIR", raising=False) + + with mock.patch("cachekit.backends.redis.provider.RedisBackendProvider") as mock_provider: + with mock.patch("cachekit.backends.redis.provider.tenant_context") as mock_ctx: + mock_ctx.get.return_value = None + mock_inst = mock.MagicMock() + mock_backend = mock.MagicMock() + mock_provider.return_value = mock_inst + mock_inst.get_backend.return_value = mock_backend + + provider = DefaultBackendProvider() + backend = provider.get_backend() + + assert backend is mock_backend + + def test_memcached_from_servers(self, monkeypatch: pytest.MonkeyPatch) -> None: + """CACHEKIT_MEMCACHED_SERVERS resolves to MemcachedBackend.""" + monkeypatch.setenv("CACHEKIT_MEMCACHED_SERVERS", '["127.0.0.1:11211"]') + monkeypatch.delenv("CACHEKIT_API_KEY", raising=False) + monkeypatch.delenv("CACHEKIT_REDIS_URL", raising=False) + monkeypatch.delenv("REDIS_URL", raising=False) + monkeypatch.delenv("CACHEKIT_FILE_CACHE_DIR", raising=False) + + with mock.patch("cachekit.backends.memcached.MemcachedBackend") as mock_backend: + mock_instance = mock.MagicMock() + mock_backend.return_value = mock_instance + + provider = DefaultBackendProvider() + backend = provider.get_backend() + + assert backend is mock_instance + mock_backend.assert_called_once() + + def test_file_from_cache_dir(self, monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: + """CACHEKIT_FILE_CACHE_DIR resolves to FileBackend.""" + monkeypatch.setenv("CACHEKIT_FILE_CACHE_DIR", str(tmp_path)) + monkeypatch.delenv("CACHEKIT_API_KEY", raising=False) + monkeypatch.delenv("CACHEKIT_REDIS_URL", raising=False) + monkeypatch.delenv("REDIS_URL", raising=False) + monkeypatch.delenv("CACHEKIT_MEMCACHED_SERVERS", raising=False) + + with mock.patch("cachekit.backends.file.FileBackend") as mock_backend: + mock_instance = mock.MagicMock() + mock_backend.return_value = mock_instance + + provider = DefaultBackendProvider() + backend = provider.get_backend() + + assert backend is mock_instance + mock_backend.assert_called_once() + + def test_api_key_wins_over_redis_url(self, monkeypatch: pytest.MonkeyPatch) -> None: + """CACHEKIT_API_KEY has higher priority than REDIS_URL.""" + monkeypatch.setenv("CACHEKIT_API_KEY", "ck_test_priority") + monkeypatch.setenv("REDIS_URL", "redis://localhost:6379") + monkeypatch.delenv("CACHEKIT_REDIS_URL", raising=False) + monkeypatch.delenv("CACHEKIT_MEMCACHED_SERVERS", raising=False) + monkeypatch.delenv("CACHEKIT_FILE_CACHE_DIR", raising=False) + + with mock.patch("cachekit.backends.cachekitio.CachekitIOBackend") as mock_backend: + mock_instance = mock.MagicMock() + mock_backend.return_value = mock_instance + + provider = DefaultBackendProvider() + backend = provider.get_backend() + + assert backend is mock_instance + + def test_warns_when_both_api_key_and_redis(self, monkeypatch: pytest.MonkeyPatch, caplog) -> None: + """Logs warning when both CachekitIO and Redis env vars are set.""" + monkeypatch.setenv("CACHEKIT_API_KEY", "ck_test_warn") + monkeypatch.setenv("CACHEKIT_REDIS_URL", "redis://localhost:6379") + monkeypatch.delenv("CACHEKIT_MEMCACHED_SERVERS", raising=False) + monkeypatch.delenv("CACHEKIT_FILE_CACHE_DIR", raising=False) + + with mock.patch("cachekit.backends.cachekitio.CachekitIOBackend") as mock_backend: + mock_backend.return_value = mock.MagicMock() + + with caplog.at_level(logging.WARNING): + provider = DefaultBackendProvider() + provider.get_backend() + + assert "Both CACHEKIT_API_KEY and Redis URL configured" in caplog.text + + def test_caches_backend_on_subsequent_calls(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Second call reuses the backend from first call.""" + monkeypatch.setenv("CACHEKIT_API_KEY", "ck_test_cache") + monkeypatch.delenv("CACHEKIT_REDIS_URL", raising=False) + monkeypatch.delenv("REDIS_URL", raising=False) + monkeypatch.delenv("CACHEKIT_MEMCACHED_SERVERS", raising=False) + monkeypatch.delenv("CACHEKIT_FILE_CACHE_DIR", raising=False) + + with mock.patch("cachekit.backends.cachekitio.CachekitIOBackend") as mock_backend: + mock_instance = mock.MagicMock() + mock_backend.return_value = mock_instance + + provider = DefaultBackendProvider() + b1 = provider.get_backend() + b2 = provider.get_backend() + + assert b1 is b2 + mock_backend.assert_called_once() + + def test_fallback_to_redis_when_no_env_vars(self, monkeypatch: pytest.MonkeyPatch) -> None: + """No env vars → falls back to Redis (will fail at connection time).""" + monkeypatch.delenv("CACHEKIT_API_KEY", raising=False) + monkeypatch.delenv("CACHEKIT_REDIS_URL", raising=False) + monkeypatch.delenv("REDIS_URL", raising=False) + monkeypatch.delenv("CACHEKIT_MEMCACHED_SERVERS", raising=False) + monkeypatch.delenv("CACHEKIT_FILE_CACHE_DIR", raising=False) + + with mock.patch("cachekit.backends.redis.provider.RedisBackendProvider") as mock_provider: + with mock.patch("cachekit.backends.redis.provider.tenant_context") as mock_ctx: + mock_ctx.get.return_value = None + mock_inst = mock.MagicMock() + mock_backend = mock.MagicMock() + mock_provider.return_value = mock_inst + mock_inst.get_backend.return_value = mock_backend + + provider = DefaultBackendProvider() + backend = provider.get_backend() + + assert backend is mock_backend + + def test_redis_sets_default_tenant(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Redis path sets tenant_context to 'default' if not already set.""" + monkeypatch.setenv("CACHEKIT_REDIS_URL", "redis://localhost:6379") + monkeypatch.delenv("CACHEKIT_API_KEY", raising=False) + monkeypatch.delenv("REDIS_URL", raising=False) + monkeypatch.delenv("CACHEKIT_MEMCACHED_SERVERS", raising=False) + monkeypatch.delenv("CACHEKIT_FILE_CACHE_DIR", raising=False) + + with mock.patch("cachekit.backends.redis.provider.RedisBackendProvider") as mock_provider: + with mock.patch("cachekit.backends.redis.provider.tenant_context") as mock_ctx: + mock_ctx.get.return_value = None + mock_provider.return_value = mock.MagicMock() + mock_provider.return_value.get_backend.return_value = mock.MagicMock() + + provider = DefaultBackendProvider() + provider.get_backend() + + mock_ctx.set.assert_called_once_with("default")