]> littlesong.place Git - littlesongplace.git/commitdiff
Intial work on notification tests
authorChris Fulljames <christianfulljames@gmail.com>
Sun, 24 Aug 2025 18:51:26 +0000 (14:51 -0400)
committerChris Fulljames <christianfulljames@gmail.com>
Sun, 24 Aug 2025 18:51:39 +0000 (14:51 -0400)
src/littlesongplace/push_notifications.py
src/littlesongplace/static/service.js
src/littlesongplace/templates/activity.html
test/test_push_notifications.py [new file with mode: 0644]

index 585626bf34f833a773f6d42760aea9e244c3a6d7..ac03333e363701f68d1e760727d9eecc61e30f2f 100644 (file)
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone
 
 import click
 import pywebpush
-from flask import Blueprint, current_app, g, request
+from flask import abort, Blueprint, current_app, g, request
 
 from . import auth, datadir, db, songs
 
@@ -54,27 +54,6 @@ def subscribe():
 
     return {"status": "success", "subid": row["subid"]}
 
-@bp.post("/update-subscription/<int:subid>")
-@auth.requires_login
-def update_subscription(subid):
-    if not request.json:
-        # Request must contain valid subscription JSON
-        abort(400)
-
-    row = db.query(
-            """
-            UPDATE users_push_subscriptions
-            SET subscription = ?
-            WHERE subid = ? AND userid = ?
-            RETURNING subid
-            """,
-            [json.dumps(request.json), subid, g.userid], expect_one=True)
-    db.commit()
-
-    current_app.logger.info(f"{g.username} updated push subscription")
-
-    return {"status": "success", "subid": row["subid"]}
-
 @bp.get("/vapid-public-key")
 def vapid_public_key():
     try:
@@ -100,7 +79,7 @@ def get_settings():
 
     return {"comments": comments, "songs": songs}
 
-@bp.post("/update-settings")
+@bp.post("/settings")
 @auth.requires_login
 def update_settings():
     if not request.json:
@@ -125,8 +104,9 @@ def update_settings():
             UPDATE users_push_subscriptions
             SET settings = ?
             WHERE subid = ? AND userid = ?
+            RETURNING subid
             """,
-            [bitfield, subid, g.userid])
+            [bitfield, subid, g.userid], expect_one=True)
     db.commit()
 
     current_app.logger.info(f"{g.username} updated push subscription settings: ({subid}) {bitfield:04x}")
@@ -239,3 +219,4 @@ def notify_new_songs_cmd():
 
 def init_app(app):
     app.cli.add_command(notify_new_songs_cmd)
+
index 8f7ee6ec19193c113052fdbc8ee50574a3e65885..7bd5ce58786f9d7ff2e5d4fb2d6c3e3908827073 100644 (file)
@@ -13,7 +13,7 @@ self.addEventListener("pushsubscriptionchanged", (event) => {
         .then((subscription) => {
             console.log("Register new subscription");
             const subid = window.localStorage.getItem("subid");
-            return fetch(`/push-notifications/update-subscription/${subid}`, {
+            return fetch(`/push-notifications/subscribe?subid=${subid}`, {
                 method: "post",
                 headers: {"Content-Type": "application/json"},
                 body: JSON.stringify(subscription)
index 1bb12c9e63752394fb1d5e5420d0d3dc4586866c..5d98d667e6a09d8adb17cdd918ce521173736e0f 100644 (file)
@@ -61,7 +61,7 @@ async function updateSettings() {
 
     // Update notification settings
     const response = await fetch(
-        "/push-notifications/update-settings", {
+        "/push-notifications/settings", {
             method: "post",
             headers: {"Content-Type": "application/json"},
             body: JSON.stringify({
diff --git a/test/test_push_notifications.py b/test/test_push_notifications.py
new file mode 100644 (file)
index 0000000..73a1f63
--- /dev/null
@@ -0,0 +1,234 @@
+import json
+import subprocess
+from unittest import mock
+
+import pytest
+
+from .utils import create_user, create_user_and_song
+
+import littlesongplace as lsp
+
+SUBDATA = json.dumps({
+    "endpoint": "https://example.com",
+    "expirationTime": None,
+    "keys": {"auth": "auth_str", "p256dh": "p256dh_str"}
+})
+
+@pytest.fixture
+def subid(client, user):
+    response = client.post(
+            "/push-notifications/subscribe",
+            headers={"Content-Type": "application/json"},
+            data=SUBDATA)
+    yield response.json["subid"]
+
+@pytest.fixture
+def vapid_keys(app):
+    with open(lsp.datadir.get_vapid_public_key_path(), "w") as keyfile:
+        keyfile.write("vapid-public-key")
+    with open(lsp.datadir.get_vapid_private_key_path(), "w") as keyfile:
+        keyfile.write("vapid-private-key")
+
+################################################################################
+# Subscribe
+
+def test_subscribe_new(client, user):
+    response = client.post(
+            "/push-notifications/subscribe",
+            headers={"Content-Type": "application/json"},
+            data=SUBDATA)
+    assert response.json["status"] == "success"
+    assert response.json["subid"] == 1
+
+def test_subscribe_update_existing(client, user):
+    # Create new
+    for i in range(1, 4):
+        response = client.post(
+                "/push-notifications/subscribe",
+                headers={"Content-Type": "application/json"},
+                data=SUBDATA)
+        assert response.json["subid"] == i
+
+    # Update subid 1
+    response = client.post(
+            "/push-notifications/subscribe?subid=1",
+            headers={"Content-Type": "application/json"},
+            data=SUBDATA)
+
+    assert response.json["status"] == "success"
+    assert response.json["subid"] == 1
+
+def test_subscribe_not_logged_in(client):
+    response = client.post(
+            "/push-notifications/subscribe",
+            headers={"Content-Type": "application/json"},
+            data=SUBDATA)
+    assert response.status_code == 302
+    assert response.headers["Location"] == "/login"
+
+def test_subscribe_other_users_subid(client, user):
+    # Create subid1 for user
+    response = client.post(
+            "/push-notifications/subscribe",
+            headers={"Content-Type": "application/json"},
+            data=SUBDATA)
+
+    # Try to modify as user2
+    create_user(client, "user2", login=True)
+    response = client.post(
+            "/push-notifications/subscribe?subid=1",
+            headers={"Content-Type": "application/json"},
+            data=SUBDATA)
+
+    # Subid ignored, created new one
+    assert response.json["subid"] == 2
+
+def test_subscribe_other_users_subid(client, user):
+    # Create subid1 for user, requesting invalid subid
+    response = client.post(
+            "/push-notifications/subscribe?subid=100",
+            headers={"Content-Type": "application/json"},
+            data=SUBDATA)
+
+    # Subid ignored, created new one
+    assert response.json["subid"] == 1
+
+################################################################################
+# VAPID Key
+
+def test_vapid_public_key(client, vapid_keys):
+    response = client.get("/push-notifications/vapid-public-key")
+    assert response.json["status"] == "ok"
+    assert response.json["public_key"] == "vapid-public-key"
+
+def test_vapid_public_key_no_key(client):
+    response = client.get("/push-notifications/vapid-public-key")
+    assert response.json["status"] == "error"
+
+################################################################################
+# Get Settings
+
+def test_get_settings_defaults(client, user, subid):
+    response = client.get(f"/push-notifications/settings?subid={subid}")
+    assert response.json["comments"] == False
+    assert response.json["songs"] == False
+
+def test_get_settings_not_logged_in(client):
+    response = client.get("/push-notifications/settings?subid=1")
+    assert response.status_code == 302
+    assert response.headers["Location"] == "/login"
+
+def test_get_settings_invalid_subid(client, user):
+    response = client.get("/push-notifications/settings?subid=1")
+    assert response.status_code == 404
+
+def test_get_settings_other_users_subid(client, user, subid):
+    create_user(client, "user2", login=True)
+    response = client.get(f"/push-notifications/settings?subid={subid}")
+    assert response.status_code == 404
+
+################################################################################
+# Update Settings
+
+def test_update_settings_ok(client, user, subid):
+    # Update
+    response = client.post(
+            "/push-notifications/settings",
+            headers={"Content-Type": "application/json"},
+            data=json.dumps({"subid": subid, "comments": True, "songs": True}))
+    assert response.json["status"] == "success"
+
+    # Verify settings applied
+    response = client.get(f"/push-notifications/settings?subid={subid}")
+    assert response.json["comments"] == True
+    assert response.json["songs"] == True
+    
+def test_update_settings_not_logged_in(client):
+    response = client.post(
+            "/push-notifications/settings",
+            headers={"Content-Type": "application/json"},
+            data=json.dumps({"subid": 1, "comments": True, "songs": True}))
+    assert response.status_code == 302
+    assert response.headers["Location"] == "/login"
+
+def test_update_settings_invalid_subid(client, user):
+    response = client.post(
+            "/push-notifications/settings",
+            headers={"Content-Type": "application/json"},
+            data=json.dumps({"subid": 1, "comments": True, "songs": True}))
+    assert response.status_code == 404
+
+def test_update_settings_other_users_subid(client, user, subid):
+    create_user(client, "user2", login=True)
+    response = client.post(
+            "/push-notifications/settings",
+            headers={"Content-Type": "application/json"},
+            data=json.dumps({"subid": subid, "comments": True, "songs": True}))
+    assert response.status_code == 404
+
+def test_update_settings_missing_value(client, user, subid):
+    response = client.post(
+            "/push-notifications/settings",
+            headers={"Content-Type": "application/json"},
+            data=json.dumps({"subid": subid, "comments": True}))  # Missing songs
+    assert response.status_code == 400
+
+    response = client.post(
+            "/push-notifications/settings",
+            headers={"Content-Type": "application/json"},
+            data=json.dumps({"subid": subid, "songs": True}))  # Missing comments
+    assert response.status_code == 400
+
+################################################################################
+# Notification delivery
+
+def _enable_notifications(client, subid, **kwargs):
+    response = client.post(
+            "/push-notifications/settings",
+            headers={"Content-Type": "application/json"},
+            data=json.dumps(dict(subid=subid, **kwargs)))
+    assert response.status_code == 200
+
+@mock.patch("pywebpush.webpush")
+def test_notification_for_new_songs_enabled(pushmock, app, client, user, subid, vapid_keys):
+    _enable_notifications(client, subid, comments=False, songs=True)
+    create_user_and_song(client, "user2")
+
+    with app.app_context():
+        lsp.push_notifications.notify_new_songs_cmd([], standalone_mode=False)
+        lsp.push_notifications.wait_all()
+
+    pushmock.assert_called_once_with(
+        json.loads(SUBDATA),
+        '{"title": "New song from user2", "body": null, "url": "/"}',
+        vapid_private_key="vapid-private-key",
+        vapid_claims={"sub": "mailto:littlesongplace@gmail.com"})
+
+@mock.patch("pywebpush.webpush")
+def test_notification_for_new_songs_disabled(pushmock, app, client, user, subid, vapid_keys):
+    _enable_notifications(client, subid, comments=False, songs=False)
+    create_user_and_song(client, "user2")
+
+    with app.app_context():
+        lsp.push_notifications.notify_new_songs_cmd([], standalone_mode=False)
+        lsp.push_notifications.wait_all()
+
+    pushmock.assert_not_called()
+
+# notification for activity when enabled
+# no notification for activity when disabled
+
+# notification for jam when enabled
+# no notification for jam when disabled
+
+
+
+################################################################################
+# Subscription expiry
+
+# @mock.patch("pywebpush.webpush")
+# def test_subscription_deleted_on_410(client, user, subid):
+#     ...
+
+# subscription deleted on 410
+# subscription not deleted on 500