Skip to content

Commit 64e3e10

Browse files
Copilotcodingjoe
andcommitted
fix: add timestamp to HealthCheckException base, cover atlassian.py L93
- HealthCheckException now accepts optional `timestamp` kwarg defaulting to datetime.datetime.now(tz=utc) so every error always carries a timestamp - views.py can now use result.error.timestamp directly (no getattr), and the unused django.utils.timezone import is removed - StatusPageWarning simplified to just super().__init__(message, timestamp=timestamp) - Add test_check_status__incident_with_invalid_timestamp in TestFlyIo to cover the except ValueError branch in _parse_incident_timestamp (atlassian.py L93) - Update exception tests: timestamp defaults to now, not None Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
1 parent c5f9526 commit 64e3e10

File tree

4 files changed

+63
-10
lines changed

4 files changed

+63
-10
lines changed

health_check/exceptions.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import datetime
2+
3+
14
class HealthCheckException(Exception):
25
message_type: str = "Unknown Error"
36

4-
def __init__(self, message):
7+
def __init__(self, message, *, timestamp=None):
58
self.message = message
9+
self.timestamp = timestamp or datetime.datetime.now(tz=datetime.timezone.utc)
610

711
def __str__(self):
812
return f"{self.message_type}: {self.message}"
@@ -26,5 +30,4 @@ class StatusPageWarning(ServiceWarning):
2630
"""Warning from an external status page, carrying the source incident timestamp."""
2731

2832
def __init__(self, message, *, timestamp=None):
29-
super().__init__(message)
30-
self.timestamp = timestamp
33+
super().__init__(message, timestamp=timestamp)

health_check/views.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from django.db import transaction
77
from django.http import HttpResponse, JsonResponse
8-
from django.utils import timezone
98
from django.utils.cache import patch_vary_headers
109
from django.utils.decorators import method_decorator
1110
from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
@@ -256,7 +255,7 @@ def _render_feed(self, feed_class):
256255

257256
for result in self.results:
258257
published_at = (
259-
(getattr(result.error, "timestamp", None) or timezone.now())
258+
result.error.timestamp
260259
if result.error
261260
else datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
262261
)

tests/contrib/test_atlassian.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
from unittest import mock
23

34
import pytest
@@ -119,8 +120,6 @@ async def test_check_status__multiple_incidents(self):
119120
@pytest.mark.asyncio
120121
async def test_check_status__incident_carries_source_timestamp(self):
121122
"""StatusPageWarning carries the most recent incident created_at as its timestamp."""
122-
import datetime
123-
124123
api_response = {
125124
"page": {"id": "test"},
126125
"incidents": [
@@ -161,6 +160,43 @@ async def test_check_status__incident_carries_source_timestamp(self):
161160
"StatusPageWarning should carry the most recent incident timestamp"
162161
)
163162

163+
@pytest.mark.asyncio
164+
async def test_check_status__incident_with_invalid_timestamp(self):
165+
"""Incidents with unparseable timestamps are treated as having no timestamp."""
166+
api_response = {
167+
"page": {"id": "test"},
168+
"incidents": [
169+
{
170+
"name": "Incident with bad date",
171+
"shortlink": "https://stspg.io/xyz",
172+
"created_at": "not-a-date",
173+
}
174+
],
175+
}
176+
177+
with mock.patch(
178+
"health_check.contrib.atlassian.httpx.AsyncClient"
179+
) as mock_client:
180+
mock_response = mock.MagicMock()
181+
mock_response.json.return_value = api_response
182+
mock_response.raise_for_status = mock.MagicMock()
183+
184+
mock_context = mock.AsyncMock()
185+
mock_context.__aenter__.return_value.get = mock.AsyncMock(
186+
return_value=mock_response
187+
)
188+
mock_client.return_value = mock_context
189+
190+
before = datetime.datetime.now(tz=datetime.timezone.utc)
191+
check = FlyIo()
192+
result = await check.get_result()
193+
after = datetime.datetime.now(tz=datetime.timezone.utc)
194+
assert result.error is not None
195+
assert isinstance(result.error, StatusPageWarning)
196+
assert before <= result.error.timestamp <= after, (
197+
"Unparseable incident timestamp should fall back to current time"
198+
)
199+
164200
@pytest.mark.asyncio
165201
async def test_check_status__http_error(self):
166202
"""Raise ServiceUnavailable on HTTP error."""

tests/test_exceptions.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ def test_init__store_message(self):
1919
exc = HealthCheckException("test message")
2020
assert exc.message == "test message"
2121

22+
def test_init__timestamp_defaults_to_now(self):
23+
"""Default timestamp to current UTC time when not provided."""
24+
before = datetime.datetime.now(tz=datetime.timezone.utc)
25+
exc = HealthCheckException("test message")
26+
after = datetime.datetime.now(tz=datetime.timezone.utc)
27+
assert before <= exc.timestamp <= after
28+
29+
def test_init__store_timestamp(self):
30+
"""Store explicit timestamp passed to constructor."""
31+
ts = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
32+
exc = HealthCheckException("test message", timestamp=ts)
33+
assert exc.timestamp == ts
34+
2235
def test_str__format_with_type(self):
2336
"""Format string with message type and message."""
2437
exc = HealthCheckException("foo")
@@ -111,10 +124,12 @@ def test_init__store_message(self):
111124
exc = StatusPageWarning("incident detected")
112125
assert exc.message == "incident detected"
113126

114-
def test_init__timestamp_defaults_to_none(self):
115-
"""Default timestamp to None when not provided."""
127+
def test_init__timestamp_defaults_to_now(self):
128+
"""Default timestamp to current UTC time when not provided."""
129+
before = datetime.datetime.now(tz=datetime.timezone.utc)
116130
exc = StatusPageWarning("incident detected")
117-
assert exc.timestamp is None
131+
after = datetime.datetime.now(tz=datetime.timezone.utc)
132+
assert before <= exc.timestamp <= after
118133

119134
def test_init__store_timestamp(self):
120135
"""Store timestamp passed to constructor."""

0 commit comments

Comments
 (0)