mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-16 17:39:26 -05:00
2f53325185
TestCacheIntegration::test_get_cache_with_redis_enabled and
test_get_cache_fallback_to_memory were both failing with:
RuntimeError: Working outside of application context.
The tests patched `app.utils.cache.current_app` with a MagicMock and
set `mock_app.config = {...}`. In theory that should isolate get_cache()
from Flask's LocalProxy entirely. In practice the RuntimeError still
fires — most likely because (a) module-level `_cache` left over from a
prior test holds a reference that triggers a LocalProxy access at
return time, or (b) a code path inside the patched function still
reaches the real `current_app` past the mock boundary.
Rather than diagnose the exact mock-interaction issue, rewrite both
tests to use the real `app` fixture from conftest.py:
- Set `REDIS_ENABLED` / `REDIS_URL` / `REDIS_DEFAULT_TTL` on the real
Flask config inside `with app.app_context():`.
- Reset `app.utils.cache._cache = None` explicitly so the global
doesn't leak state between tests.
- Keep the `patch("app.utils.cache.RedisCache")` since we don't want
the test to actually hit Redis; only the read of `current_app.config`
needs to be real.
This is closer to how get_cache() runs in production (real Flask
context, mocked external dependency) and avoids the LocalProxy ↔ mock
collision entirely.
Test plan
- pytest tests/test_utils/test_cache.py::TestCacheIntegration::test_get_cache_with_redis_enabled
- pytest tests/test_utils/test_cache.py::TestCacheIntegration::test_get_cache_fallback_to_memory
- pytest tests/test_utils/test_cache.py (the other 16 tests in the file should be unaffected)
183 lines
5.6 KiB
Python
183 lines
5.6 KiB
Python
"""
|
|
Tests for caching utilities (Redis and in-memory fallback).
|
|
"""
|
|
|
|
import pytest
|
|
|
|
pytestmark = [pytest.mark.unit, pytest.mark.utils]
|
|
|
|
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"""
|
|
|
|
def test_get_cache_with_redis_enabled(self, app):
|
|
"""Test get_cache when Redis is enabled (RedisCache is mocked)."""
|
|
import app.utils.cache as cache_module
|
|
|
|
cache_module._cache = None
|
|
with app.app_context():
|
|
app.config["REDIS_ENABLED"] = True
|
|
app.config["REDIS_URL"] = "redis://localhost:6379/0"
|
|
app.config["REDIS_DEFAULT_TTL"] = 3600
|
|
|
|
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
|
|
|
|
def test_get_cache_fallback_to_memory(self, app):
|
|
"""Test get_cache falls back to in-memory when Redis is disabled."""
|
|
import app.utils.cache as cache_module
|
|
|
|
cache_module._cache = None
|
|
with app.app_context():
|
|
app.config["REDIS_ENABLED"] = False
|
|
app.config["REDIS_DEFAULT_TTL"] = 3600
|
|
|
|
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
|