Skip to content

Commit 8dddecf

Browse files
authored
Merge branch 'main' into issues/659/alias
2 parents 6c12d16 + c141523 commit 8dddecf

File tree

4 files changed

+134
-7
lines changed

4 files changed

+134
-7
lines changed

health_check/checks.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
from django import db
1212
from django.conf import settings
1313
from django.core.cache import CacheKeyWarning, caches
14+
from django.core.cache.backends.base import InvalidCacheBackendError
1415
from django.core.files.base import ContentFile
16+
from django.core.files.storage import InvalidStorageError, storages
1517
from django.core.files.storage import Storage as DjangoStorage
16-
from django.core.files.storage import storages
1718
from django.core.mail import get_connection
1819
from django.core.mail.backends.base import BaseEmailBackend
1920
from django.db import connections
2021
from django.db.models import Expression
22+
from django.utils.connection import ConnectionDoesNotExist
2123

2224
from health_check.base import HealthCheck
2325
from health_check.exceptions import (
@@ -62,7 +64,10 @@ class Cache(HealthCheck):
6264
)
6365

6466
async def run(self):
65-
cache = caches[self.alias]
67+
try:
68+
cache = caches[self.alias]
69+
except InvalidCacheBackendError as e:
70+
raise ServiceUnavailable("Cache alias does not exist") from e
6671
# Use an isolated key per probe run to avoid cross-process write races.
6772
cache_key = f"{self.key_prefix}:{uuid.uuid4().hex}"
6873
cache_value = f"itworks-{datetime.datetime.now().timestamp()}"
@@ -109,7 +114,10 @@ class Database(HealthCheck):
109114
alias: str = "default"
110115

111116
def run(self):
112-
connection = connections[self.alias]
117+
try:
118+
connection = connections[self.alias]
119+
except ConnectionDoesNotExist as e:
120+
raise ServiceUnavailable("Database alias does not exist") from e
113121
result = None
114122
try:
115123
compiler = connection.ops.compiler("SQLCompiler")(
@@ -237,7 +245,10 @@ class Storage(HealthCheck):
237245

238246
@property
239247
def storage(self) -> DjangoStorage:
240-
return storages[self.alias]
248+
try:
249+
return storages[self.alias]
250+
except InvalidStorageError as e:
251+
raise ServiceUnavailable("Storage alias does not exist") from e
241252

242253
def get_file_name(self):
243254
return f"health_check_storage_test/test-{uuid.uuid4()}.txt"

health_check/views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ def _render_feed(self, feed_class):
249249
"""Generate RSS or Atom feed with health check results."""
250250
feed = feed_class(
251251
title="Health Check Status",
252-
link=self.request.build_absolute_uri(),
252+
link=self.request.build_absolute_uri(self.request.path),
253253
description="Current status of system health checks",
254254
feed_url=self.request.build_absolute_uri(),
255255
)
@@ -262,8 +262,8 @@ def _render_feed(self, feed_class):
262262
)
263263
feed.add_item(
264264
title=repr(result.check),
265-
link=self.request.build_absolute_uri(),
266-
description=f"{result.check!r}\nResponse time: {result.time_taken:.3f}s",
265+
link=self.request.build_absolute_uri(self.request.path),
266+
description=str(result.error) if result.error else "OK",
267267
pubdate=published_at,
268268
updateddate=published_at,
269269
author_name=self.feed_author,

tests/test_checks.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@
1919
class TestCache:
2020
"""Test the Cache health check."""
2121

22+
@pytest.mark.asyncio
23+
async def test_run_check__invalid_alias(self):
24+
"""Raise ServiceUnavailable when cache alias does not exist."""
25+
check = Cache(alias="nonexistent-alias")
26+
result = await check.get_result()
27+
assert isinstance(result.error, ServiceUnavailable), (
28+
"Expected ServiceUnavailable for a non-existent cache alias"
29+
)
30+
assert "Cache alias does not exist" in str(result.error)
31+
2232
@pytest.mark.asyncio
2333
async def test_run_check__cache_working(self):
2434
"""Cache backend successfully sets and retrieves values."""
@@ -112,6 +122,17 @@ async def _aget(_key):
112122
class TestDatabase:
113123
"""Test the Database health check."""
114124

125+
@pytest.mark.django_db
126+
@pytest.mark.asyncio
127+
async def test_run_check__invalid_alias(self):
128+
"""Raise ServiceUnavailable when database alias does not exist."""
129+
check = Database(alias="nonexistent-alias")
130+
result = await check.get_result()
131+
assert isinstance(result.error, ServiceUnavailable), (
132+
"Expected ServiceUnavailable for a non-existent database alias"
133+
)
134+
assert "Database alias does not exist" in str(result.error)
135+
115136
@pytest.mark.django_db
116137
@pytest.mark.asyncio
117138
async def test_run_check__database_available(self):
@@ -146,6 +167,16 @@ async def test_run_check__locmem_backend(self):
146167
class TestStorage:
147168
"""Test the Storage health check."""
148169

170+
@pytest.mark.asyncio
171+
async def test_run_check__invalid_alias(self):
172+
"""Raise ServiceUnavailable when storage alias does not exist."""
173+
check = Storage(alias="nonexistent-alias")
174+
result = await check.get_result()
175+
assert isinstance(result.error, ServiceUnavailable), (
176+
"Expected ServiceUnavailable for a non-existent storage alias"
177+
)
178+
assert "Storage alias does not exist" in str(result.error)
179+
149180
@pytest.mark.asyncio
150181
async def test_run_check__default_storage(self):
151182
"""Storage check completes without exceptions."""

tests/test_views.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,91 @@ async def run(self):
566566
"Failed check should use current timestamp in mixed feed"
567567
)
568568

569+
@pytest.mark.asyncio
570+
async def test_get__rss_feed_description_includes_status(self, health_check_view):
571+
"""RSS feed item description includes status message."""
572+
feedparser = pytest.importorskip("feedparser")
573+
574+
class FailingBackend(HealthCheck):
575+
async def run(self):
576+
raise HealthCheckException("Something went wrong")
577+
578+
response = await health_check_view([FailingBackend], format_param="rss")
579+
feed = feedparser.parse(response.content.decode("utf-8"))
580+
assert len(feed.entries) == 1
581+
entry = feed.entries[0]
582+
assert "Something went wrong" in entry.summary, (
583+
"Feed description should include the actual error message"
584+
)
585+
586+
@pytest.mark.asyncio
587+
async def test_get__rss_feed_description_healthy_shows_ok(self, health_check_view):
588+
"""RSS feed item description shows OK for healthy checks."""
589+
feedparser = pytest.importorskip("feedparser")
590+
591+
class SuccessBackend(HealthCheck):
592+
async def run(self):
593+
pass
594+
595+
response = await health_check_view([SuccessBackend], format_param="rss")
596+
feed = feedparser.parse(response.content.decode("utf-8"))
597+
assert len(feed.entries) == 1
598+
entry = feed.entries[0]
599+
assert "OK" in entry.summary, (
600+
"Feed description should show OK for healthy checks"
601+
)
602+
603+
@pytest.mark.asyncio
604+
async def test_get__rss_feed_link_excludes_format_param(self, health_check_view):
605+
"""RSS feed item link does not include the format query parameter."""
606+
feedparser = pytest.importorskip("feedparser")
607+
608+
class SuccessBackend(HealthCheck):
609+
async def run(self):
610+
pass
611+
612+
response = await health_check_view([SuccessBackend], format_param="rss")
613+
feed = feedparser.parse(response.content.decode("utf-8"))
614+
assert len(feed.entries) == 1
615+
entry = feed.entries[0]
616+
assert "format=rss" not in entry.link, (
617+
"Feed item link should not include the format query parameter"
618+
)
619+
620+
@pytest.mark.asyncio
621+
async def test_get__atom_feed_description_includes_status(self, health_check_view):
622+
"""Atom feed item description includes status message."""
623+
feedparser = pytest.importorskip("feedparser")
624+
625+
class FailingBackend(HealthCheck):
626+
async def run(self):
627+
raise HealthCheckException("Database unreachable")
628+
629+
response = await health_check_view([FailingBackend], format_param="atom")
630+
feed = feedparser.parse(response.content.decode("utf-8"))
631+
assert len(feed.entries) == 1
632+
entry = feed.entries[0]
633+
assert "Database unreachable" in entry.summary, (
634+
"Feed description should include the actual error message"
635+
)
636+
637+
@pytest.mark.asyncio
638+
async def test_get__atom_feed_link_excludes_format_param(self, health_check_view):
639+
"""Atom feed item link does not include the format query parameter."""
640+
feedparser = pytest.importorskip("feedparser")
641+
642+
class SuccessBackend(HealthCheck):
643+
async def run(self):
644+
pass
645+
646+
response = await health_check_view([SuccessBackend], format_param="atom")
647+
feed = feedparser.parse(response.content.decode("utf-8"))
648+
assert len(feed.entries) == 1
649+
entry = feed.entries[0]
650+
assert "format=atom" not in entry.link, (
651+
"Feed item link should not include the format query parameter"
652+
)
653+
569654
@pytest.mark.asyncio
570655
async def test_get_plugins__with_string_import(self):
571656
"""Import check from string path."""

0 commit comments

Comments
 (0)