|
10 | 10 | degrades to "unknown" instead of raising. This code runs inside |
11 | 11 | ``each_context``, so an exception would take the entire admin down — the exact |
12 | 12 | opposite of what a backup-health feature should do. |
| 13 | +
|
| 14 | +``BackupStatusFileTests``/``BackupWarningSuppressionTests``/``FormatBytesTests`` |
| 15 | +exercise ``get_backup_status()`` directly. ``AdminBackupWarningTests`` below |
| 16 | +goes one layer further and proves the wiring itself -- that |
| 17 | +``MakeabilityLabAdminSite.each_context`` actually reaches the rendered |
| 18 | +``/admin/`` and Data Health pages -- mirroring ``AdminLoggingWarningTests`` in |
| 19 | +``test_logging_config.py`` for the sibling ``LOG_TO_FILE`` feature. |
13 | 20 | """ |
14 | 21 |
|
15 | 22 | import json |
16 | 23 | import os |
17 | 24 | import tempfile |
18 | 25 | from datetime import datetime, timedelta, timezone |
19 | 26 |
|
| 27 | +from django.contrib.auth import get_user_model |
20 | 28 | from django.test import SimpleTestCase, override_settings |
| 29 | +from django.urls import reverse |
21 | 30 |
|
| 31 | +from website.tests.base import DatabaseTestCase |
22 | 32 | from website.utils.backup_status import format_bytes, get_backup_status |
23 | 33 |
|
24 | 34 | NOW = datetime(2026, 8, 7, 12, 0, 0, tzinfo=timezone.utc) |
@@ -202,6 +212,82 @@ def test_missing_file_is_quiet_in_local_dev(self): |
202 | 212 | self.assertFalse(get_backup_status(self.missing, NOW)['should_warn']) |
203 | 213 |
|
204 | 214 |
|
| 215 | +class AdminBackupWarningTests(DatabaseTestCase): |
| 216 | + """ |
| 217 | + The admin dashboard callout and Data Health panel that surface backup |
| 218 | + health (#1443), rendered through a real request rather than calling |
| 219 | + ``get_backup_status()`` in isolation. |
| 220 | +
|
| 221 | + ``get_backup_status()`` being correct doesn't prove |
| 222 | + ``MakeabilityLabAdminSite.each_context`` actually wires ``BACKUP_STATUS`` |
| 223 | + into the templates, or that the superuser gate in ``each_context`` is |
| 224 | + doing its job at the HTTP layer -- that's what these tests are for. |
| 225 | + """ |
| 226 | + |
| 227 | + def setUp(self): |
| 228 | + super().setUp() |
| 229 | + User = get_user_model() |
| 230 | + self.superuser = User.objects.create_superuser( |
| 231 | + username="backupadmin", email="backupadmin@example.com", password="pw-for-test" |
| 232 | + ) |
| 233 | + self.editor = User.objects.create_user( |
| 234 | + username="backupeditor", |
| 235 | + email="backupeditor@example.com", |
| 236 | + password="pw-for-test", |
| 237 | + is_staff=True, |
| 238 | + ) |
| 239 | + self.tmp = tempfile.TemporaryDirectory() |
| 240 | + self.addCleanup(self.tmp.cleanup) |
| 241 | + self.status_file = os.path.join(self.tmp.name, 'status.json') |
| 242 | + |
| 243 | + def _write_status(self, **overrides): |
| 244 | + with open(self.status_file, 'w', encoding='utf-8') as handle: |
| 245 | + json.dump(_status_payload(**overrides), handle) |
| 246 | + return self.status_file |
| 247 | + |
| 248 | + def test_warning_shown_to_superuser_when_backup_unhealthy(self): |
| 249 | + self._write_status(last_attempt_ok=False, |
| 250 | + error='pg_dump failed (exit 1): connection refused') |
| 251 | + with override_settings(BACKUP_STATUS_FILE=self.status_file): |
| 252 | + self.client.force_login(self.superuser) |
| 253 | + response = self.client.get("/admin/") |
| 254 | + self.assertEqual(response.status_code, 200) |
| 255 | + self.assertContains(response, "database backups are not healthy") |
| 256 | + self.assertContains(response, "connection refused") |
| 257 | + |
| 258 | + def test_no_warning_when_backup_healthy(self): |
| 259 | + self._write_status() |
| 260 | + with override_settings(BACKUP_STATUS_FILE=self.status_file): |
| 261 | + self.client.force_login(self.superuser) |
| 262 | + response = self.client.get("/admin/") |
| 263 | + self.assertEqual(response.status_code, 200) |
| 264 | + self.assertNotContains(response, "database backups are not healthy") |
| 265 | + |
| 266 | + def test_warning_hidden_from_non_superusers(self): |
| 267 | + """Only the maintainer can act on a backup failure, so don't alarm editors.""" |
| 268 | + self._write_status(last_attempt_ok=False, error='connection refused') |
| 269 | + with override_settings(BACKUP_STATUS_FILE=self.status_file): |
| 270 | + self.client.force_login(self.editor) |
| 271 | + response = self.client.get("/admin/") |
| 272 | + self.assertEqual(response.status_code, 200) |
| 273 | + self.assertNotContains(response, "database backups are not healthy") |
| 274 | + |
| 275 | + def test_data_health_panel_always_shown_even_when_healthy(self): |
| 276 | + """ |
| 277 | + Unlike the ``/admin/`` callout, the Data Health panel is rendered |
| 278 | + unconditionally -- "when did it last succeed?" is worth showing even |
| 279 | + when nothing is wrong. |
| 280 | + """ |
| 281 | + self._write_status() |
| 282 | + with override_settings(BACKUP_STATUS_FILE=self.status_file): |
| 283 | + self.client.force_login(self.superuser) |
| 284 | + response = self.client.get(reverse("admin:data_health_dashboard")) |
| 285 | + self.assertEqual(response.status_code, 200) |
| 286 | + self.assertContains(response, "Database backups") |
| 287 | + self.assertContains(response, "Healthy") |
| 288 | + self.assertContains(response, "makeability-2026-08-07.sql.gz") |
| 289 | + |
| 290 | + |
205 | 291 | class FormatBytesTests(SimpleTestCase): |
206 | 292 | def test_formats_common_sizes(self): |
207 | 293 | self.assertEqual(format_bytes(512), '512 B') |
|
0 commit comments