Skip to content

Commit a5fce9a

Browse files
Merge pull request #78 from GabrielPalmar/test
fix(app): Trying fixes
2 parents ac31425 + 0e14bce commit a5fce9a

5 files changed

Lines changed: 96 additions & 34 deletions

File tree

‎.github/workflows/sonarqube.yml‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
name: SonarQube analysis
22

3-
on:
4-
push:
5-
branches:
6-
- main
7-
pull_request:
8-
branches:
9-
- main
3+
on:
4+
workflow_dispatch: {}
105

116
permissions:
127
pull-requests: read

‎app/config.py‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'''Shared configuration module'''
2+
from typing import Tuple, Optional
23
import os
34
import redis
45

@@ -8,20 +9,18 @@
89
REDIS_DB = int(os.environ.get('REDIS_DB', 0))
910
CACHE_TTL = int(os.environ.get('CACHE_TTL', 300))
1011

11-
def create_redis_client():
12-
'''Create and return Redis client with error handling'''
12+
def create_redis_client() -> Tuple[Optional[redis.Redis], bool]:
13+
"""Create Redis client and return it with availability status."""
1314
try:
14-
redis_client = redis.StrictRedis(
15+
client = redis.Redis(
1516
host=REDIS_HOST,
1617
port=REDIS_PORT,
1718
db=REDIS_DB,
18-
decode_responses=True,
19-
socket_connect_timeout=240,
20-
socket_timeout=240
19+
decode_responses=True
2120
)
22-
redis_client.ping()
21+
client.ping() # Test connection
2322
print("Connected to Redis successfully!")
24-
return redis_client, True
23+
return client, True
2524
except (redis.ConnectionError, redis.TimeoutError) as e:
2625
print(f"Could not connect to Redis: {e}")
2726
return None, False

‎app/opensense.py‎

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
from ijson.common import JSONError as IjsonJSONError
99
from app.config import create_redis_client, CACHE_TTL
1010

11-
redis_client, REDIS_AVAILABLE = create_redis_client()
11+
try:
12+
REDIS_CLIENT, REDIS_AVAILABLE = create_redis_client()
13+
except (redis.ConnectionError, redis.TimeoutError, OSError, ImportError) as e:
14+
print(f"Warning: Redis client creation failed - {e}")
15+
REDIS_CLIENT = None
16+
REDIS_AVAILABLE = False
1217

1318
_sensor_stats = {"total_sensors": 0, "null_count": 0}
1419

@@ -61,21 +66,25 @@ def _empty_stats() -> Dict[str, int]:
6166
return {"total_sensors": 0, "null_count": 0}
6267

6368
def _get_cached_temperature() -> Optional[str]:
69+
"""Return cached temperature string if present, else None."""
6470
if not REDIS_AVAILABLE:
6571
return None
6672
try:
67-
cached = redis_client.get("temperature_data")
68-
if cached:
69-
return cached.decode("utf-8") if isinstance(cached, (bytes, bytearray)) else cached
73+
cached_temp = REDIS_CLIENT.get("temperature_data")
74+
if cached_temp:
75+
return cached_temp.decode("utf-8") if isinstance(
76+
cached_temp, (bytes, bytearray)
77+
) else cached_temp
7078
except redis.RedisError:
7179
return None
7280
return None
7381

7482
def _set_cached_temperature(value: str) -> None:
83+
"""Store only the temperature string in cache."""
7584
if not REDIS_AVAILABLE:
7685
return
7786
try:
78-
redis_client.setex("temperature_data", CACHE_TTL, value)
87+
REDIS_CLIENT.setex("temperature_data", CACHE_TTL, value)
7988
except redis.RedisError:
8089
pass
8190

@@ -84,15 +93,19 @@ def _build_params() -> Dict[str, str]:
8493
return {"date": time_iso, "format": "json"}
8594

8695
def _request_boxes(params: Dict[str, str]):
96+
"""Perform a single request and return the response or an error string."""
8797
try:
8898
resp = requests.get(
8999
"https://api.opensensemap.org/boxes",
90100
params=params,
91101
stream=True,
92-
timeout=(60, 90)
102+
timeout=(5, 30),
103+
headers={
104+
'User-Agent': 'HiveBox-Project/1.0',
105+
'Accept': 'application/json'
106+
}
93107
)
94-
if hasattr(resp, "raise_for_status"):
95-
resp.raise_for_status()
108+
resp.raise_for_status()
96109
if hasattr(resp, "raw") and hasattr(resp.raw, "decode_content"):
97110
resp.raw.decode_content = True
98111
return resp, None
@@ -113,8 +126,9 @@ def _make_sensor_iter(response) -> Tuple[Optional[Iterable[dict]], Optional[str]
113126

114127
def get_temperature():
115128
'''Function to get the average temperature from OpenSenseMap API.'''
129+
116130
cached = _get_cached_temperature()
117-
if cached:
131+
if cached is not None:
118132
return cached, _empty_stats()
119133

120134
params = _build_params()
@@ -137,4 +151,4 @@ def get_temperature():
137151

138152
_set_cached_temperature(result)
139153
_sensor_stats.update(stats)
140-
return result, _sensor_stats
154+
return result, stats

‎app/readiness.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from app.opensense import get_temperature
66
from app.config import create_redis_client
77

8-
redis_client, REDIS_AVAILABLE = create_redis_client()
8+
REDIS_CLIENT, REDIS_AVAILABLE = create_redis_client()
99

1010
def check_caching():
1111
'''Check if caching content is older than 5 minutes'''
@@ -14,7 +14,7 @@ def check_caching():
1414

1515
try:
1616
cache_key = "temperature_data"
17-
ttl = redis_client.ttl(cache_key)
17+
ttl = REDIS_CLIENT.ttl(cache_key)
1818

1919
if ttl in (-2, -1):
2020
return True
@@ -64,3 +64,15 @@ def readiness_check():
6464
# If Redis is completely unavailable, still allow the service to be ready
6565
print(f"Redis error during readiness check: {e}")
6666
return 200
67+
68+
def check_redis():
69+
'''Function to check Redis is Up'''
70+
if REDIS_CLIENT:
71+
try:
72+
if REDIS_CLIENT.ping():
73+
return '<p>Redis is available &#10004;</p>', True
74+
return '<p>Redis ping failed &#10060;</p>', False
75+
except redis.RedisError as e:
76+
return f'<p>Redis connection failed &#10060;: {e}</p>', False
77+
else:
78+
return '<p>Redis is not configured &#10060;</p>', False

‎tests/test_modules.py‎

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import unittest.mock as mock
66
import io
77
import json
8+
import importlib
89
import requests
910
import redis
1011
from minio.error import S3Error, InvalidResponseError
@@ -82,6 +83,7 @@ class MockOpenSenseResponse:
8283
def __init__(self, temp_value):
8384
self.text = "mock response text"
8485
self.temp_value = temp_value
86+
self.status_code = 200 # Add this attribute
8587
payload = json.dumps([{
8688
'sensors': [
8789
{
@@ -174,7 +176,7 @@ def test_cache_hit(self):
174176
mock_redis_client.get.return_value = "cached_result"
175177

176178
with mock.patch('app.opensense.REDIS_AVAILABLE', True), \
177-
mock.patch('app.opensense.redis_client', mock_redis_client), \
179+
mock.patch('app.opensense.REDIS_CLIENT', mock_redis_client), \
178180
mock.patch('app.opensense.requests.get') as mock_requests:
179181

180182
result, _ = opensense.get_temperature()
@@ -188,7 +190,7 @@ def test_cache_miss_and_store(self):
188190
mock_redis_client.get.return_value = None
189191

190192
with mock.patch('app.opensense.REDIS_AVAILABLE', True), \
191-
mock.patch('app.opensense.redis_client', mock_redis_client), \
193+
mock.patch('app.opensense.REDIS_CLIENT', mock_redis_client), \
192194
mock.patch('app.opensense.requests.get',
193195
return_value=MockOpenSenseResponse(25)):
194196

@@ -205,7 +207,7 @@ def test_cache_hit_bytes_decoded(self):
205207
mock_redis_client = mock.MagicMock()
206208
mock_redis_client.get.return_value = b"cached_result"
207209
with mock.patch('app.opensense.REDIS_AVAILABLE', True), \
208-
mock.patch('app.opensense.redis_client', mock_redis_client), \
210+
mock.patch('app.opensense.REDIS_CLIENT', mock_redis_client), \
209211
mock.patch('app.opensense.requests.get') as req_get:
210212
result, stats = opensense.get_temperature()
211213
self.assertEqual(result, "cached_result")
@@ -404,7 +406,7 @@ def test_check_caching_key_not_exists(self):
404406
mock_redis_client.ttl.return_value = -2 # Key doesn't exist
405407

406408
with mock.patch('app.readiness.REDIS_AVAILABLE', True), \
407-
mock.patch('app.readiness.redis_client', mock_redis_client):
409+
mock.patch('app.readiness.REDIS_CLIENT', mock_redis_client):
408410
result = readiness.check_caching()
409411
self.assertTrue(result)
410412

@@ -414,7 +416,7 @@ def test_check_caching_key_no_expiry(self):
414416
mock_redis_client.ttl.return_value = -1 # Key exists but no expiry
415417

416418
with mock.patch('app.readiness.REDIS_AVAILABLE', True), \
417-
mock.patch('app.readiness.redis_client', mock_redis_client):
419+
mock.patch('app.readiness.REDIS_CLIENT', mock_redis_client):
418420
result = readiness.check_caching()
419421
self.assertTrue(result)
420422

@@ -424,7 +426,7 @@ def test_check_caching_fresh_cache(self):
424426
mock_redis_client.ttl.return_value = 150 # 2.5 minutes remaining
425427

426428
with mock.patch('app.readiness.REDIS_AVAILABLE', True), \
427-
mock.patch('app.readiness.redis_client', mock_redis_client):
429+
mock.patch('app.readiness.REDIS_CLIENT', mock_redis_client):
428430
result = readiness.check_caching()
429431
self.assertFalse(result) # Cache is fresh
430432

@@ -434,7 +436,7 @@ def test_check_caching_redis_error(self):
434436
mock_redis_client.ttl.side_effect = redis.RedisError("boom")
435437

436438
with mock.patch('app.readiness.REDIS_AVAILABLE', True), \
437-
mock.patch('app.readiness.redis_client', mock_redis_client), \
439+
mock.patch('app.readiness.REDIS_CLIENT', mock_redis_client), \
438440
mock.patch('builtins.print'):
439441
result = readiness.check_caching()
440442
self.assertTrue(result)
@@ -529,5 +531,45 @@ def test_readiness_check_partial_failure(self):
529531
self.assertEqual(result, 200)
530532

531533

534+
class TestOpenSenseExceptionBranches(unittest.TestCase):
535+
"""Covers exception branches in opensense module."""
536+
537+
def test_set_cached_temperature_redis_error_is_swallowed(self):
538+
"""_set_cached_temperature should swallow RedisError (pass)."""
539+
with mock.patch('app.opensense.REDIS_AVAILABLE', True), \
540+
mock.patch('app.opensense.REDIS_CLIENT') as mock_client:
541+
mock_client.setex.side_effect = redis.RedisError("boom")
542+
# Should not raise
543+
opensense._set_cached_temperature("value")
544+
mock_client.setex.assert_called_once()
545+
546+
def test_get_cached_temperature_redis_error_returns_none(self):
547+
"""_get_cached_temperature should return None on RedisError."""
548+
with mock.patch('app.opensense.REDIS_AVAILABLE', True), \
549+
mock.patch('app.opensense.REDIS_CLIENT') as mock_client:
550+
mock_client.get.side_effect = redis.RedisError("boom")
551+
result = opensense._get_cached_temperature()
552+
self.assertIsNone(result)
553+
mock_client.get.assert_called_once_with("temperature_data")
554+
555+
def test_module_level_redis_init_errors_are_caught_all_types(self):
556+
"""create_redis_client exceptions at import-time are handled (all types)."""
557+
for exc in (
558+
redis.ConnectionError("c"),
559+
redis.TimeoutError("t"),
560+
OSError("o"),
561+
ImportError("i")):
562+
with self.subTest(exc=type(exc).__name__), \
563+
mock.patch('app.config.create_redis_client', side_effect=exc), \
564+
mock.patch('builtins.print'):
565+
reloaded = importlib.reload(opensense)
566+
self.assertIsNone(reloaded.REDIS_CLIENT)
567+
self.assertFalse(reloaded.REDIS_AVAILABLE)
568+
# Restore module for other tests
569+
with mock.patch('app.config.create_redis_client', return_value=(None, False)), \
570+
mock.patch('builtins.print'):
571+
importlib.reload(opensense)
572+
573+
532574
if __name__ == '__main__':
533575
unittest.main()

0 commit comments

Comments
 (0)