Files
TimeTracker/tests/test_utils/test_cache.py
2025-11-29 07:13:23 +01:00

178 lines
5.5 KiB
Python

"""
Tests for caching utilities (Redis and in-memory fallback).
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from app.utils.cache import get_cache, InMemoryCache, RedisCache
class TestInMemoryCache:
"""Tests for in-memory cache implementation"""
def test_get_set_delete(self):
"""Test basic cache operations"""
cache = InMemoryCache()
# Test set and get
cache.set("test_key", "test_value", ttl=3600)
assert cache.get("test_key") == "test_value"
# Test delete
cache.delete("test_key")
assert cache.get("test_key") is None
def test_expiration(self):
"""Test that expired entries are not returned"""
cache = InMemoryCache(default_ttl=1)
cache.set("expired_key", "value", ttl=0.1) # Very short TTL
assert cache.get("expired_key") == "value"
import time
time.sleep(0.2)
assert cache.get("expired_key") is None
def test_exists(self):
"""Test exists method"""
cache = InMemoryCache()
assert cache.exists("nonexistent") is False
cache.set("existing", "value")
assert cache.exists("existing") is True
def test_clear(self):
"""Test clearing all cache"""
cache = InMemoryCache()
cache.set("key1", "value1")
cache.set("key2", "value2")
cache.clear()
assert cache.get("key1") is None
assert cache.get("key2") is None
class TestRedisCache:
"""Tests for Redis cache implementation"""
@patch("app.utils.cache.redis")
def test_redis_connection_success(self, mock_redis):
"""Test successful Redis connection"""
mock_client = MagicMock()
mock_client.ping.return_value = True
mock_redis.Redis.return_value = mock_client
cache = RedisCache("redis://localhost:6379/0")
assert cache._connected is True
mock_client.ping.assert_called_once()
@patch("app.utils.cache.redis")
def test_redis_connection_failure(self, mock_redis):
"""Test Redis connection failure falls back to in-memory"""
mock_redis.Redis.side_effect = Exception("Connection failed")
cache = RedisCache("redis://localhost:6379/0")
assert cache._connected is False
assert hasattr(cache, "_fallback")
@patch("app.utils.cache.redis")
def test_redis_get_set(self, mock_redis):
"""Test Redis get and set operations"""
import pickle
mock_client = MagicMock()
mock_client.ping.return_value = True
mock_redis.Redis.return_value = mock_client
cache = RedisCache("redis://localhost:6379/0")
# Test set
cache.set("test_key", "test_value", ttl=3600)
mock_client.setex.assert_called_once()
args, kwargs = mock_client.setex.call_args
assert args[0] == "test_key"
assert args[1] == 3600
assert pickle.loads(args[2]) == "test_value"
# Test get
mock_client.get.return_value = pickle.dumps("test_value")
result = cache.get("test_key")
assert result == "test_value"
mock_client.get.assert_called_with("test_key")
@patch("app.utils.cache.redis")
def test_redis_fallback_on_error(self, mock_redis):
"""Test that Redis errors fall back to in-memory cache"""
mock_client = MagicMock()
mock_client.ping.return_value = True
mock_client.get.side_effect = Exception("Redis error")
mock_redis.Redis.return_value = mock_client
cache = RedisCache("redis://localhost:6379/0")
# Should not raise, but return None
result = cache.get("test_key")
assert result is None
class TestCacheIntegration:
"""Integration tests for cache utilities"""
@patch("app.utils.cache.current_app")
def test_get_cache_with_redis_enabled(self, mock_app):
"""Test get_cache when Redis is enabled"""
mock_config = {"REDIS_ENABLED": True, "REDIS_URL": "redis://localhost:6379/0", "REDIS_DEFAULT_TTL": 3600}
mock_app.config = mock_config
with patch("app.utils.cache.RedisCache") as mock_redis_cache:
mock_instance = MagicMock()
mock_instance._connected = True
mock_redis_cache.return_value = mock_instance
cache = get_cache()
assert cache is not None
@patch("app.utils.cache.current_app")
def test_get_cache_fallback_to_memory(self, mock_app):
"""Test get_cache falls back to in-memory when Redis unavailable"""
mock_config = {"REDIS_ENABLED": False, "REDIS_DEFAULT_TTL": 3600}
mock_app.config = mock_config
# Reset global cache
import app.utils.cache
app.utils.cache._cache = None
cache = get_cache()
assert isinstance(cache, InMemoryCache)
def test_cache_decorator(self):
"""Test the @cached decorator"""
from app.utils.cache import cached
call_count = [0]
@cached(ttl=60, key_prefix="test")
def expensive_function(x, y):
call_count[0] += 1
return x + y
# First call should execute function
result1 = expensive_function(1, 2)
assert result1 == 3
assert call_count[0] == 1
# Second call should use cache
result2 = expensive_function(1, 2)
assert result2 == 3
assert call_count[0] == 1 # Should not increment
# Different args should execute again
result3 = expensive_function(2, 3)
assert result3 == 5
assert call_count[0] == 2