feat: pwa notifications
This commit is contained in:
+197
-4
@@ -1,13 +1,24 @@
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from pywebpush import WebPushException
|
||||
|
||||
from accounts.models import User
|
||||
from tracker.models import DoseLog, Medication, PulseReading, ScheduledDose, TimeOfDay
|
||||
from accounts.models import PushSubscription, User
|
||||
from tracker.models import (
|
||||
DoseLog,
|
||||
Medication,
|
||||
PulseReading,
|
||||
ReminderSent,
|
||||
ScheduledDose,
|
||||
TimeOfDay,
|
||||
)
|
||||
|
||||
|
||||
class TrackerTestCase(TestCase):
|
||||
@@ -265,3 +276,185 @@ class PwaTests(TrackerTestCase):
|
||||
response = self.client.get("/admin/login/")
|
||||
|
||||
self.assertContains(response, 'rel="manifest"')
|
||||
|
||||
|
||||
@override_settings(
|
||||
VAPID_PUBLIC_KEY="BMn-test-public-key",
|
||||
VAPID_PRIVATE_KEY="DiXsYTkxHVb68y2-9CGUZheAe7Iah9cLZ_vXc9DbJ5w",
|
||||
)
|
||||
class PushTests(TrackerTestCase):
|
||||
def subscribe(self, endpoint="https://push.example/abc") -> PushSubscription:
|
||||
return PushSubscription.objects.create(
|
||||
user=self.user,
|
||||
endpoint=endpoint,
|
||||
p256dh="p256dh-key",
|
||||
auth="auth-secret",
|
||||
)
|
||||
|
||||
def test_a_device_can_subscribe_and_unsubscribe(self):
|
||||
self.client.force_login(self.user)
|
||||
body = {
|
||||
"endpoint": "https://push.example/xyz",
|
||||
"keys": {"p256dh": "key", "auth": "secret"},
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
"/api/push/subscribe", body, content_type="application/json"
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(self.user.push_subscriptions.count(), 1)
|
||||
|
||||
# Subscribing the same endpoint twice must not duplicate the device.
|
||||
self.client.post("/api/push/subscribe", body, content_type="application/json")
|
||||
self.assertEqual(self.user.push_subscriptions.count(), 1)
|
||||
|
||||
self.client.post(
|
||||
"/api/push/unsubscribe",
|
||||
{"endpoint": body["endpoint"]},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(self.user.push_subscriptions.count(), 0)
|
||||
|
||||
def test_the_test_button_needs_a_subscribed_device(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post("/api/push/test")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
@patch("tracker.push.webpush")
|
||||
def test_the_test_button_pushes_to_every_device(self, webpush):
|
||||
self.subscribe("https://push.example/phone")
|
||||
self.subscribe("https://push.example/tablet")
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.post("/api/push/test")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(webpush.call_count, 2)
|
||||
payload = json.loads(webpush.call_args.kwargs["data"])
|
||||
self.assertIn("test", payload["title"])
|
||||
|
||||
@patch("tracker.push.webpush")
|
||||
def test_a_dead_subscription_is_dropped_rather_than_retried(self, webpush):
|
||||
subscription = self.subscribe()
|
||||
response = type("R", (), {"status_code": 410})()
|
||||
webpush.side_effect = WebPushException("gone", response=response)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
self.client.post("/api/push/test")
|
||||
|
||||
self.assertFalse(
|
||||
PushSubscription.objects.filter(pk=subscription.pk).exists(),
|
||||
"A 410 means the browser discarded the subscription; the row is dead.",
|
||||
)
|
||||
|
||||
|
||||
@override_settings(
|
||||
VAPID_PUBLIC_KEY="BMn-test-public-key",
|
||||
VAPID_PRIVATE_KEY="DiXsYTkxHVb68y2-9CGUZheAe7Iah9cLZ_vXc9DbJ5w",
|
||||
)
|
||||
class SendRemindersTests(TrackerTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
PushSubscription.objects.create(
|
||||
user=self.user,
|
||||
endpoint="https://push.example/phone",
|
||||
p256dh="key",
|
||||
auth="secret",
|
||||
)
|
||||
|
||||
def run_command(self, at: str, **kwargs) -> str:
|
||||
"""Run send_reminders as if the clock read `at` (local time, today)."""
|
||||
hour, minute = (int(part) for part in at.split(":"))
|
||||
now = timezone.make_aware(
|
||||
datetime.combine(timezone.localdate(), datetime.min.time()).replace(
|
||||
hour=hour, minute=minute
|
||||
)
|
||||
)
|
||||
out = StringIO()
|
||||
with patch("django.utils.timezone.localtime", return_value=now):
|
||||
call_command("send_reminders", stdout=out, stderr=out, **kwargs)
|
||||
return out.getvalue()
|
||||
|
||||
def set_reminder(self, dose: ScheduledDose, at: str):
|
||||
hour, minute = (int(part) for part in at.split(":"))
|
||||
dose.reminder_time = datetime.min.time().replace(hour=hour, minute=minute)
|
||||
dose.save()
|
||||
|
||||
@patch("tracker.push.webpush")
|
||||
def test_a_due_dose_is_reminded_once(self, webpush):
|
||||
self.set_reminder(self.morning, "08:00")
|
||||
|
||||
self.run_command("08:05")
|
||||
|
||||
self.assertEqual(webpush.call_count, 1)
|
||||
payload = json.loads(webpush.call_args.kwargs["data"])
|
||||
self.assertIn("Fortekor 1/4", payload["body"])
|
||||
self.assertEqual(ReminderSent.objects.count(), 1)
|
||||
|
||||
# A second run the same day must stay quiet.
|
||||
self.run_command("08:30")
|
||||
self.assertEqual(webpush.call_count, 1)
|
||||
|
||||
@patch("tracker.push.webpush")
|
||||
def test_doses_due_at_the_same_time_share_one_notification(self, webpush):
|
||||
self.set_reminder(self.morning, "08:00")
|
||||
self.set_reminder(self.morning_only, "08:00")
|
||||
|
||||
self.run_command("08:01")
|
||||
|
||||
self.assertEqual(webpush.call_count, 1, "One push, not one per pill.")
|
||||
payload = json.loads(webpush.call_args.kwargs["data"])
|
||||
self.assertIn("Fortekor", payload["body"])
|
||||
self.assertIn("Vetmedin", payload["body"])
|
||||
self.assertEqual(ReminderSent.objects.count(), 2)
|
||||
|
||||
@patch("tracker.push.webpush")
|
||||
def test_a_dose_already_given_is_not_reminded(self, webpush):
|
||||
self.set_reminder(self.morning, "08:00")
|
||||
DoseLog.objects.create(scheduled_dose=self.morning, taken=True)
|
||||
|
||||
self.run_command("08:05")
|
||||
|
||||
webpush.assert_not_called()
|
||||
|
||||
@patch("tracker.push.webpush")
|
||||
def test_nothing_fires_before_the_reminder_time(self, webpush):
|
||||
self.set_reminder(self.evening, "19:00")
|
||||
|
||||
self.run_command("18:59")
|
||||
|
||||
webpush.assert_not_called()
|
||||
|
||||
@patch("tracker.push.webpush")
|
||||
def test_a_long_missed_reminder_is_not_fired_late(self, webpush):
|
||||
# The server was down all morning; do not buzz at 23:00 about the 08:00 dose.
|
||||
self.set_reminder(self.morning, "08:00")
|
||||
|
||||
self.run_command("23:00")
|
||||
|
||||
webpush.assert_not_called()
|
||||
self.assertEqual(ReminderSent.objects.count(), 0)
|
||||
|
||||
@patch("tracker.push.webpush")
|
||||
def test_dry_run_sends_and_records_nothing(self, webpush):
|
||||
self.set_reminder(self.morning, "08:00")
|
||||
|
||||
output = self.run_command("08:05", dry_run=True)
|
||||
|
||||
webpush.assert_not_called()
|
||||
self.assertEqual(ReminderSent.objects.count(), 0)
|
||||
self.assertIn("dry-run", output)
|
||||
|
||||
@patch("tracker.push.webpush")
|
||||
def test_a_failed_delivery_is_retried_on_the_next_run(self, webpush):
|
||||
self.set_reminder(self.morning, "08:00")
|
||||
webpush.side_effect = WebPushException("push service down")
|
||||
|
||||
self.run_command("08:05")
|
||||
self.assertEqual(
|
||||
ReminderSent.objects.count(), 0, "Nothing recorded, so it can retry."
|
||||
)
|
||||
|
||||
webpush.side_effect = None
|
||||
self.run_command("08:10")
|
||||
self.assertEqual(ReminderSent.objects.count(), 1)
|
||||
|
||||
Reference in New Issue
Block a user