From 746a9edaf3303e4086c09dddf684bfdb2ec09e6b Mon Sep 17 00:00:00 2001 From: Chris Fulljames Date: Sun, 24 Aug 2025 14:51:26 -0400 Subject: [PATCH] Intial work on notification tests --- src/littlesongplace/push_notifications.py | 29 +-- src/littlesongplace/static/service.js | 2 +- src/littlesongplace/templates/activity.html | 2 +- test/test_push_notifications.py | 234 ++++++++++++++++++++ 4 files changed, 241 insertions(+), 26 deletions(-) create mode 100644 test/test_push_notifications.py diff --git a/src/littlesongplace/push_notifications.py b/src/littlesongplace/push_notifications.py index 585626b..ac03333 100644 --- a/src/littlesongplace/push_notifications.py +++ b/src/littlesongplace/push_notifications.py @@ -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/") -@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) + diff --git a/src/littlesongplace/static/service.js b/src/littlesongplace/static/service.js index 8f7ee6e..7bd5ce5 100644 --- a/src/littlesongplace/static/service.js +++ b/src/littlesongplace/static/service.js @@ -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) diff --git a/src/littlesongplace/templates/activity.html b/src/littlesongplace/templates/activity.html index 1bb12c9..5d98d66 100644 --- a/src/littlesongplace/templates/activity.html +++ b/src/littlesongplace/templates/activity.html @@ -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 index 0000000..73a1f63 --- /dev/null +++ b/test/test_push_notifications.py @@ -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 -- 2.39.5