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, "