Skip to content

Commit bc54f69

Browse files
Copilotcodingjoe
andauthored
Fix RSS/Atom feeds using static epoch for healthy checks (#642)
Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
1 parent 2be5503 commit bc54f69

File tree

2 files changed

+129
-2
lines changed

2 files changed

+129
-2
lines changed

health_check/views.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import datetime
23
import re
34
import typing
45

@@ -254,12 +255,17 @@ def _render_feed(self, feed_class):
254255
)
255256

256257
for result in self.results:
258+
published_at = (
259+
timezone.now()
260+
if result.error
261+
else datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
262+
)
257263
feed.add_item(
258264
title=repr(result.check),
259265
link=self.request.build_absolute_uri(),
260266
description=f"{result.check!r}\nResponse time: {result.time_taken:.3f}s",
261-
pubdate=timezone.now(),
262-
updateddate=timezone.now(),
267+
pubdate=published_at,
268+
updateddate=published_at,
263269
author_name=self.feed_author,
264270
categories=["error", "unhealthy"] if result.error else ["healthy"],
265271
)

tests/test_views.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,127 @@ async def run(self):
445445
assert "application/rss+xml" in response["content-type"]
446446
assert b"error" in response.content or b"unhealthy" in response.content
447447

448+
@pytest.mark.asyncio
449+
async def test_get__atom_feed_healthy_uses_epoch(self, health_check_view):
450+
"""Use epoch 0 for healthy checks in Atom feed."""
451+
feedparser = pytest.importorskip("feedparser")
452+
453+
class SuccessBackend(HealthCheck):
454+
async def run(self):
455+
pass
456+
457+
response = await health_check_view([SuccessBackend], format_param="atom")
458+
feed = feedparser.parse(response.content.decode("utf-8"))
459+
assert len(feed.entries) == 1
460+
entry = feed.entries[0]
461+
assert entry.published_parsed[:3] == (1970, 1, 1), (
462+
"Healthy check should use epoch (1970-01-01) as published date"
463+
)
464+
assert entry.updated_parsed[:3] == (1970, 1, 1), (
465+
"Healthy check should use epoch (1970-01-01) as updated date"
466+
)
467+
468+
@pytest.mark.asyncio
469+
async def test_get__atom_feed_error_uses_current_time(self, health_check_view):
470+
"""Use current timestamp for failed checks in Atom feed."""
471+
import datetime
472+
473+
feedparser = pytest.importorskip("feedparser")
474+
475+
class FailingBackend(HealthCheck):
476+
async def run(self):
477+
raise HealthCheckException("Check failed")
478+
479+
response = await health_check_view([FailingBackend], format_param="atom")
480+
feed = feedparser.parse(response.content.decode("utf-8"))
481+
assert len(feed.entries) == 1
482+
entry = feed.entries[0]
483+
now = datetime.datetime.now(tz=datetime.timezone.utc)
484+
published_at = datetime.datetime(
485+
*entry.published_parsed[:6], tzinfo=datetime.timezone.utc
486+
)
487+
assert (now - published_at).total_seconds() < 60, (
488+
"Failed check should use current timestamp (within last 60 seconds)"
489+
)
490+
491+
@pytest.mark.asyncio
492+
async def test_get__rss_feed_healthy_uses_epoch(self, health_check_view):
493+
"""Use epoch 0 for healthy checks in RSS feed."""
494+
feedparser = pytest.importorskip("feedparser")
495+
496+
class SuccessBackend(HealthCheck):
497+
async def run(self):
498+
pass
499+
500+
response = await health_check_view([SuccessBackend], format_param="rss")
501+
feed = feedparser.parse(response.content.decode("utf-8"))
502+
assert len(feed.entries) == 1
503+
entry = feed.entries[0]
504+
assert entry.published_parsed[:3] == (1970, 1, 1), (
505+
"Healthy check should use epoch (1970-01-01) as published date"
506+
)
507+
508+
@pytest.mark.asyncio
509+
async def test_get__rss_feed_error_uses_current_time(self, health_check_view):
510+
"""Use current timestamp for failed checks in RSS feed."""
511+
import datetime
512+
513+
feedparser = pytest.importorskip("feedparser")
514+
515+
class FailingBackend(HealthCheck):
516+
async def run(self):
517+
raise HealthCheckException("Check failed")
518+
519+
response = await health_check_view([FailingBackend], format_param="rss")
520+
feed = feedparser.parse(response.content.decode("utf-8"))
521+
assert len(feed.entries) == 1
522+
entry = feed.entries[0]
523+
now = datetime.datetime.now(tz=datetime.timezone.utc)
524+
published_at = datetime.datetime(
525+
*entry.published_parsed[:6], tzinfo=datetime.timezone.utc
526+
)
527+
assert (now - published_at).total_seconds() < 60, (
528+
"Failed check should use current timestamp (within last 60 seconds)"
529+
)
530+
531+
@pytest.mark.asyncio
532+
async def test_get__atom_feed_mixed_checks_uses_correct_dates(
533+
self, health_check_view
534+
):
535+
"""Use epoch for healthy and current time for failed checks in the same feed."""
536+
import datetime
537+
538+
feedparser = pytest.importorskip("feedparser")
539+
540+
class SuccessBackend(HealthCheck):
541+
async def run(self):
542+
pass
543+
544+
class FailingBackend(HealthCheck):
545+
async def run(self):
546+
raise HealthCheckException("Check failed")
547+
548+
response = await health_check_view(
549+
[SuccessBackend, FailingBackend], format_param="atom"
550+
)
551+
feed = feedparser.parse(response.content.decode("utf-8"))
552+
assert len(feed.entries) == 2
553+
554+
healthy_entry = next(e for e in feed.entries if "SuccessBackend" in e.title)
555+
failed_entry = next(e for e in feed.entries if "FailingBackend" in e.title)
556+
557+
assert healthy_entry.published_parsed[:3] == (1970, 1, 1), (
558+
"Healthy check should use epoch (1970-01-01) in mixed feed"
559+
)
560+
561+
now = datetime.datetime.now(tz=datetime.timezone.utc)
562+
published_at = datetime.datetime(
563+
*failed_entry.published_parsed[:6], tzinfo=datetime.timezone.utc
564+
)
565+
assert (now - published_at).total_seconds() < 60, (
566+
"Failed check should use current timestamp in mixed feed"
567+
)
568+
448569
@pytest.mark.asyncio
449570
async def test_get_plugins__with_string_import(self):
450571
"""Import check from string path."""

0 commit comments

Comments
 (0)