Skip to content

Commit 3f10276

Browse files
authored
Merge pull request #136 from DevanshuNEU/fix/134-cache-get-set
fix(#134): Add generic get/set methods to CacheService
2 parents 3359365 + e79ad55 commit 3f10276

2 files changed

Lines changed: 140 additions & 0 deletions

File tree

backend/services/cache.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,47 @@ def _make_key(self, prefix: str, *args) -> str:
5656
hash_input = ":".join(parts)
5757
hash_val = hashlib.md5(hash_input.encode()).hexdigest()[:12]
5858
return f"{prefix}:{hash_val}"
59+
60+
def get(self, key: str) -> Optional[Dict]:
61+
"""
62+
Get value by key (generic cache access).
63+
64+
Used for arbitrary key-value caching like repo validation results.
65+
"""
66+
if not self.redis:
67+
return None
68+
69+
try:
70+
cached = self.redis.get(key)
71+
if cached:
72+
metrics.increment("cache_hits")
73+
return json.loads(cached)
74+
metrics.increment("cache_misses")
75+
except Exception as e:
76+
logger.error("Cache read error", operation="get", key=key[:50], error=str(e))
77+
metrics.increment("cache_errors")
78+
79+
return None
80+
81+
def set(self, key: str, value: Dict, ttl: int = 300) -> bool:
82+
"""
83+
Set value by key (generic cache access).
84+
85+
Used for arbitrary key-value caching like repo validation results.
86+
Default TTL is 5 minutes.
87+
88+
Returns True if successful, False otherwise.
89+
"""
90+
if not self.redis:
91+
return False
92+
93+
try:
94+
self.redis.setex(key, ttl, json.dumps(value))
95+
return True
96+
except Exception as e:
97+
logger.error("Cache write error", operation="set", key=key[:50], error=str(e))
98+
metrics.increment("cache_errors")
99+
return False
59100

60101
def get_search_results(self, query: str, repo_id: str) -> Optional[List[Dict]]:
61102
"""Get cached search results"""
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""
2+
Tests for CacheService - specifically the generic get/set methods.
3+
"""
4+
import pytest
5+
from unittest.mock import MagicMock, patch
6+
import json
7+
8+
9+
class TestCacheServiceGenericMethods:
10+
"""Test the generic get() and set() methods added for #134."""
11+
12+
@pytest.fixture
13+
def mock_redis(self):
14+
"""Create a mock Redis client."""
15+
return MagicMock()
16+
17+
@pytest.fixture
18+
def cache_service(self, mock_redis):
19+
"""Create CacheService with mocked Redis."""
20+
with patch('services.cache.redis.Redis', return_value=mock_redis):
21+
with patch('services.cache.redis.from_url', return_value=mock_redis):
22+
mock_redis.ping.return_value = True
23+
from services.cache import CacheService
24+
service = CacheService()
25+
service.redis = mock_redis
26+
return service
27+
28+
def test_get_returns_cached_value(self, cache_service, mock_redis):
29+
"""get() returns parsed JSON when key exists."""
30+
test_data = {"valid": True, "repo_name": "flask"}
31+
mock_redis.get.return_value = json.dumps(test_data).encode()
32+
33+
result = cache_service.get("validate:https://github.com/pallets/flask")
34+
35+
assert result == test_data
36+
mock_redis.get.assert_called_once()
37+
38+
def test_get_returns_none_when_key_missing(self, cache_service, mock_redis):
39+
"""get() returns None when key doesn't exist."""
40+
mock_redis.get.return_value = None
41+
42+
result = cache_service.get("validate:nonexistent")
43+
44+
assert result is None
45+
46+
def test_get_returns_none_when_redis_unavailable(self, cache_service):
47+
"""get() returns None when Redis is not available."""
48+
cache_service.redis = None
49+
50+
result = cache_service.get("any_key")
51+
52+
assert result is None
53+
54+
def test_get_handles_redis_error(self, cache_service, mock_redis):
55+
"""get() returns None and logs error on Redis exception."""
56+
mock_redis.get.side_effect = Exception("Redis connection error")
57+
58+
result = cache_service.get("validate:test")
59+
60+
assert result is None
61+
62+
def test_set_stores_value_with_ttl(self, cache_service, mock_redis):
63+
"""set() stores JSON value with TTL."""
64+
test_data = {"valid": True, "can_index": True}
65+
66+
result = cache_service.set("validate:test", test_data, ttl=300)
67+
68+
assert result is True
69+
mock_redis.setex.assert_called_once_with(
70+
"validate:test",
71+
300,
72+
json.dumps(test_data)
73+
)
74+
75+
def test_set_uses_default_ttl(self, cache_service, mock_redis):
76+
"""set() uses 300s (5 min) as default TTL."""
77+
test_data = {"valid": True}
78+
79+
cache_service.set("validate:test", test_data)
80+
81+
# Check that TTL is 300 (default)
82+
call_args = mock_redis.setex.call_args
83+
assert call_args[0][1] == 300 # TTL is second positional arg
84+
85+
def test_set_returns_false_when_redis_unavailable(self, cache_service):
86+
"""set() returns False when Redis is not available."""
87+
cache_service.redis = None
88+
89+
result = cache_service.set("any_key", {"data": "value"})
90+
91+
assert result is False
92+
93+
def test_set_handles_redis_error(self, cache_service, mock_redis):
94+
"""set() returns False on Redis exception."""
95+
mock_redis.setex.side_effect = Exception("Redis write error")
96+
97+
result = cache_service.set("validate:test", {"data": "value"})
98+
99+
assert result is False

0 commit comments

Comments
 (0)