268 lines
10 KiB
Python
268 lines
10 KiB
Python
import json
|
|
from datetime import timedelta
|
|
|
|
from django.conf import settings
|
|
from django.test import TestCase
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
|
|
from accounts.models import User
|
|
from tracker.models import DoseLog, Medication, PulseReading, ScheduledDose, TimeOfDay
|
|
|
|
|
|
class TrackerTestCase(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create_superuser("loic", password="hunter2hunter2")
|
|
|
|
# Medication 1: 1/4 every morning.
|
|
self.vetmedin = Medication.objects.create(name="Vetmedin")
|
|
self.morning_only = ScheduledDose.objects.create(
|
|
medication=self.vetmedin, time_of_day=TimeOfDay.MORNING, amount="1/4"
|
|
)
|
|
|
|
# Medication 2: 1/4 morning and 1/4 evening.
|
|
self.fortekor = Medication.objects.create(name="Fortekor")
|
|
self.morning = ScheduledDose.objects.create(
|
|
medication=self.fortekor, time_of_day=TimeOfDay.MORNING, amount="1/4"
|
|
)
|
|
self.evening = ScheduledDose.objects.create(
|
|
medication=self.fortekor, time_of_day=TimeOfDay.EVENING, amount="1/4"
|
|
)
|
|
|
|
def token(self) -> str:
|
|
response = self.client.post(
|
|
"/api/auth/login",
|
|
{"username": "loic", "password": "hunter2hunter2"},
|
|
content_type="application/json",
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
return response.json()["token"]
|
|
|
|
def auth_header(self) -> dict:
|
|
return {"HTTP_AUTHORIZATION": f"Bearer {self.token()}"}
|
|
|
|
|
|
class AuthTests(TrackerTestCase):
|
|
def test_login_rejects_a_bad_password(self):
|
|
response = self.client.post(
|
|
"/api/auth/login",
|
|
{"username": "loic", "password": "wrong"},
|
|
content_type="application/json",
|
|
)
|
|
self.assertEqual(response.status_code, 401)
|
|
|
|
def test_endpoints_require_authentication(self):
|
|
self.assertEqual(self.client.get("/api/tracker/day").status_code, 401)
|
|
|
|
def test_health_is_public(self):
|
|
self.assertEqual(self.client.get("/api/health").status_code, 200)
|
|
|
|
|
|
class DoseApiTests(TrackerTestCase):
|
|
def test_day_lists_every_scheduled_dose_unrecorded(self):
|
|
response = self.client.get("/api/tracker/day", **self.auth_header())
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
doses = response.json()["doses"]
|
|
self.assertEqual(len(doses), 3)
|
|
self.assertTrue(all(dose["taken"] is None for dose in doses))
|
|
# Morning doses come before the evening one.
|
|
self.assertEqual([d["time_of_day"] for d in doses], [1, 1, 3])
|
|
|
|
def test_logging_a_dose_marks_only_that_dose(self):
|
|
response = self.client.post(
|
|
"/api/tracker/doses",
|
|
{"scheduled_dose_id": self.morning.id, "taken": True},
|
|
content_type="application/json",
|
|
**self.auth_header(),
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
doses = {d["id"]: d["taken"] for d in response.json()["doses"]}
|
|
self.assertIs(doses[self.morning.id], True)
|
|
self.assertIsNone(doses[self.evening.id])
|
|
self.assertIsNone(doses[self.morning_only.id])
|
|
|
|
def test_logging_the_same_dose_twice_updates_in_place(self):
|
|
for taken in (True, False):
|
|
self.client.post(
|
|
"/api/tracker/doses",
|
|
{"scheduled_dose_id": self.morning.id, "taken": taken},
|
|
content_type="application/json",
|
|
**self.auth_header(),
|
|
)
|
|
|
|
log = DoseLog.objects.get(scheduled_dose=self.morning)
|
|
self.assertFalse(log.taken)
|
|
self.assertIsNone(log.taken_at)
|
|
self.assertEqual(DoseLog.objects.count(), 1)
|
|
|
|
def test_a_dose_can_be_logged_for_a_past_day(self):
|
|
yesterday = timezone.localdate() - timedelta(days=1)
|
|
self.client.post(
|
|
"/api/tracker/doses",
|
|
{
|
|
"scheduled_dose_id": self.evening.id,
|
|
"date": yesterday.isoformat(),
|
|
"taken": True,
|
|
},
|
|
content_type="application/json",
|
|
**self.auth_header(),
|
|
)
|
|
|
|
self.assertTrue(DoseLog.objects.filter(date=yesterday, taken=True).exists())
|
|
self.assertFalse(DoseLog.objects.filter(date=timezone.localdate()).exists())
|
|
|
|
def test_inactive_medication_drops_out_of_the_day(self):
|
|
self.fortekor.is_active = False
|
|
self.fortekor.save()
|
|
|
|
response = self.client.get("/api/tracker/day", **self.auth_header())
|
|
self.assertEqual(len(response.json()["doses"]), 1)
|
|
|
|
|
|
class PulseApiTests(TrackerTestCase):
|
|
def test_recording_the_pulse_twice_in_a_day_overwrites(self):
|
|
for bpm in (180, 192):
|
|
response = self.client.post(
|
|
"/api/tracker/pulse",
|
|
{"bpm": bpm},
|
|
content_type="application/json",
|
|
**self.auth_header(),
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
self.assertEqual(PulseReading.objects.count(), 1)
|
|
self.assertEqual(PulseReading.objects.get().bpm, 192)
|
|
|
|
def test_an_implausible_pulse_is_rejected(self):
|
|
response = self.client.post(
|
|
"/api/tracker/pulse",
|
|
{"bpm": 5},
|
|
content_type="application/json",
|
|
**self.auth_header(),
|
|
)
|
|
self.assertEqual(response.status_code, 422)
|
|
|
|
def test_stats_report_a_rising_trend(self):
|
|
today = timezone.localdate()
|
|
for offset, bpm in enumerate([160, 170, 180, 190]):
|
|
PulseReading.objects.create(
|
|
date=today - timedelta(days=3 - offset), bpm=bpm
|
|
)
|
|
|
|
stats = self.client.get("/api/tracker/pulse/stats", **self.auth_header()).json()
|
|
|
|
self.assertEqual(stats["count"], 4)
|
|
self.assertEqual(stats["minimum"], 160)
|
|
self.assertEqual(stats["maximum"], 190)
|
|
self.assertEqual(stats["average"], 175.0)
|
|
self.assertEqual(stats["trend_bpm_per_day"], 10.0)
|
|
|
|
def test_stats_are_empty_without_readings(self):
|
|
stats = self.client.get("/api/tracker/pulse/stats", **self.auth_header()).json()
|
|
|
|
self.assertEqual(stats["count"], 0)
|
|
self.assertIsNone(stats["average"])
|
|
self.assertIsNone(stats["trend_bpm_per_day"])
|
|
|
|
|
|
class TodayPageTests(TrackerTestCase):
|
|
def test_the_page_requires_login(self):
|
|
response = self.client.get(reverse("tracker:today"))
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn("/admin/login/", response["Location"])
|
|
|
|
def test_the_page_lists_the_doses(self):
|
|
self.client.force_login(self.user)
|
|
response = self.client.get(reverse("tracker:today"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Vetmedin")
|
|
self.assertContains(response, "Fortekor")
|
|
self.assertContains(response, "Morning")
|
|
self.assertContains(response, "Evening")
|
|
# Nothing ticked yet, so every dose reads as unrecorded rather than missed.
|
|
self.assertContains(response, "not recorded", count=3)
|
|
self.assertNotContains(response, "checked")
|
|
|
|
def test_saving_ticks_doses_and_records_the_pulse(self):
|
|
self.client.force_login(self.user)
|
|
today = timezone.localdate()
|
|
|
|
response = self.client.post(
|
|
reverse("tracker:today"),
|
|
{
|
|
"date": today.isoformat(),
|
|
f"dose-{self.morning.id}": "on",
|
|
f"dose-{self.morning_only.id}": "on",
|
|
# The evening checkbox is absent: not given.
|
|
"bpm": "188",
|
|
},
|
|
)
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
self.assertTrue(DoseLog.objects.get(scheduled_dose=self.morning).taken)
|
|
self.assertTrue(DoseLog.objects.get(scheduled_dose=self.morning_only).taken)
|
|
self.assertFalse(DoseLog.objects.get(scheduled_dose=self.evening).taken)
|
|
self.assertEqual(PulseReading.objects.get(date=today).bpm, 188)
|
|
|
|
def test_clearing_the_pulse_field_removes_the_reading(self):
|
|
self.client.force_login(self.user)
|
|
today = timezone.localdate()
|
|
PulseReading.objects.create(date=today, bpm=200)
|
|
|
|
self.client.post(
|
|
reverse("tracker:today"), {"date": today.isoformat(), "bpm": ""}
|
|
)
|
|
|
|
self.assertFalse(PulseReading.objects.filter(date=today).exists())
|
|
|
|
def test_the_graph_appears_once_there_are_two_readings(self):
|
|
self.client.force_login(self.user)
|
|
today = timezone.localdate()
|
|
|
|
response = self.client.get(reverse("tracker:today"))
|
|
self.assertContains(response, "at least two days")
|
|
|
|
PulseReading.objects.create(date=today - timedelta(days=1), bpm=170)
|
|
PulseReading.objects.create(date=today, bpm=190)
|
|
|
|
response = self.client.get(reverse("tracker:today"))
|
|
self.assertContains(response, "<polyline")
|
|
self.assertContains(response, "180.0 bpm") # average
|
|
|
|
|
|
class PwaTests(TrackerTestCase):
|
|
def test_the_service_worker_is_served_from_the_root(self):
|
|
# Scope: under /static/ it could not control the /admin/ pages it caches.
|
|
response = self.client.get("/sw.js")
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response["Content-Type"], "application/javascript")
|
|
|
|
def test_the_page_links_the_manifest_and_registers_the_worker(self):
|
|
self.client.force_login(self.user)
|
|
response = self.client.get(reverse("tracker:today"))
|
|
|
|
self.assertContains(response, 'rel="manifest"')
|
|
self.assertContains(response, "serviceWorker.register")
|
|
|
|
def test_the_manifest_is_valid_json_pointing_at_real_icons(self):
|
|
response = self.client.get(reverse("manifest"))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response["Content-Type"], "application/manifest+json")
|
|
|
|
manifest = json.loads(response.content)
|
|
self.assertEqual(manifest["start_url"], reverse("tracker:today"))
|
|
self.assertEqual(len(manifest["icons"]), 3)
|
|
for icon in manifest["icons"]:
|
|
self.assertTrue(icon["src"].startswith(settings.STATIC_URL))
|
|
|
|
def test_the_login_page_is_installable_too(self):
|
|
# The app opens on the login screen when the session lapses.
|
|
response = self.client.get("/admin/login/")
|
|
|
|
self.assertContains(response, 'rel="manifest"')
|