from datetime import timedelta 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, "