167 lines
5.9 KiB
Python
167 lines
5.9 KiB
Python
import json
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from src import main
|
|
from src.push_notifications import PushConfiguration, dispatch_unread_updates
|
|
from src.push_subscription_store import PushSubscriptionStore
|
|
|
|
|
|
def test_subscription_store_delivers_each_unread_thread_once_per_device(tmp_path):
|
|
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
|
subscription = {
|
|
"endpoint": "https://push.example/device-a",
|
|
"keys": {"p256dh": "public-key", "auth": "auth-secret"},
|
|
}
|
|
|
|
store.upsert("session-a", subscription)
|
|
|
|
first = store.claim_unseen({41, 42})
|
|
store.mark_delivered("session-a", first[0].thread_ids)
|
|
second = store.claim_unseen({41, 42})
|
|
|
|
assert [(item.session_id, item.thread_ids) for item in first] == [
|
|
("session-a", (41, 42))
|
|
]
|
|
assert second == []
|
|
|
|
|
|
def test_replacing_subscription_resets_delivery_cursor_and_revocation_removes_device(tmp_path):
|
|
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
|
original = {
|
|
"endpoint": "https://push.example/original",
|
|
"keys": {"p256dh": "old-key", "auth": "old-auth"},
|
|
}
|
|
replacement = {
|
|
"endpoint": "https://push.example/replacement",
|
|
"keys": {"p256dh": "new-key", "auth": "new-auth"},
|
|
}
|
|
store.upsert("session-a", original)
|
|
store.claim_unseen({9})
|
|
|
|
store.upsert("session-a", replacement)
|
|
delivery = store.claim_unseen({9})
|
|
store.delete_session("session-a")
|
|
|
|
assert delivery[0].subscription == replacement
|
|
assert delivery[0].thread_ids == (9,)
|
|
assert store.claim_unseen({10}) == []
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_dispatch_sends_one_privacy_safe_deep_link_per_new_thread(tmp_path):
|
|
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
|
store.upsert("session-a", {
|
|
"endpoint": "https://push.example/device-a",
|
|
"keys": {"p256dh": "public-key", "auth": "auth-secret"},
|
|
})
|
|
sent = []
|
|
|
|
async def unread():
|
|
return {"items": [
|
|
{"notification_id": 42, "repository": "private/repo", "title": "Secret title"},
|
|
]}
|
|
|
|
async def send(subscription, payload):
|
|
sent.append((subscription, json.loads(payload)))
|
|
|
|
config = PushConfiguration("public-vapid", "private-vapid", "mailto:ops@example.com")
|
|
assert await dispatch_unread_updates(store, config, unread, send) == 1
|
|
assert await dispatch_unread_updates(store, config, unread, send) == 0
|
|
assert sent[0][1] == {
|
|
"title": "New work update",
|
|
"body": "Tap to review it in Stackchain.",
|
|
"route": "#/my-work/update/42",
|
|
"tag": "stackchain-update-42",
|
|
}
|
|
assert "private/repo" not in json.dumps(sent)
|
|
assert "Secret title" not in json.dumps(sent)
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_dispatch_removes_an_expired_push_endpoint(tmp_path):
|
|
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
|
store.upsert("session-a", {
|
|
"endpoint": "https://push.example/expired",
|
|
"keys": {"p256dh": "public-key", "auth": "auth-secret"},
|
|
})
|
|
|
|
class Expired(Exception):
|
|
response = SimpleNamespace(status_code=410)
|
|
|
|
async def unread():
|
|
return {"items": [{"notification_id": 8}]}
|
|
|
|
async def send(_subscription, _payload):
|
|
raise Expired()
|
|
|
|
config = PushConfiguration("public", "private", "mailto:ops@example.com")
|
|
assert await dispatch_unread_updates(store, config, unread, send) == 0
|
|
assert store.is_subscribed("session-a") is False
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_authenticated_device_can_subscribe_report_status_and_unsubscribe(tmp_path, monkeypatch):
|
|
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
|
monkeypatch.setattr(main, "_push_subscription_store", store)
|
|
monkeypatch.setattr(
|
|
main,
|
|
"_push_configuration",
|
|
lambda: PushConfiguration("public-vapid", "private-vapid", "mailto:ops@example.com"),
|
|
)
|
|
async def management_id(_session):
|
|
return "session-a"
|
|
|
|
monkeypatch.setattr(main.dashboard_auth, "session_management_id", management_id)
|
|
|
|
async def no_unread():
|
|
return {"items": []}
|
|
|
|
monkeypatch.setattr(main, "notifications", no_unread)
|
|
request = SimpleNamespace(state=SimpleNamespace(
|
|
dashboard_session=SimpleNamespace(session_id="session-a")
|
|
))
|
|
payload = main.PushSubscriptionPayload(
|
|
endpoint="https://push.example/device-a",
|
|
keys={"p256dh": "public-key", "auth": "auth-secret"},
|
|
)
|
|
|
|
assert await main.push_status(request) == {
|
|
"available": True, "subscribed": False, "public_key": "public-vapid"
|
|
}
|
|
assert await main.subscribe_push(payload, request) == {"subscribed": True}
|
|
assert (await main.push_status(request))["subscribed"] is True
|
|
assert await main.unsubscribe_push(request) == {"subscribed": False}
|
|
assert (await main.push_status(request))["subscribed"] is False
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_subscription_fails_closed_when_existing_unread_baseline_is_unavailable(tmp_path, monkeypatch):
|
|
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
|
monkeypatch.setattr(main, "_push_subscription_store", store)
|
|
monkeypatch.setattr(
|
|
main, "_push_configuration",
|
|
lambda: PushConfiguration("public", "private", "mailto:ops@example.com"),
|
|
)
|
|
|
|
async def management_id(_session):
|
|
return "device-a"
|
|
|
|
async def unavailable():
|
|
raise RuntimeError("Gitea unavailable")
|
|
|
|
monkeypatch.setattr(main.dashboard_auth, "session_management_id", management_id)
|
|
monkeypatch.setattr(main, "notifications", unavailable)
|
|
request = SimpleNamespace(state=SimpleNamespace(dashboard_session=object()))
|
|
payload = main.PushSubscriptionPayload(
|
|
endpoint="https://push.example/device-a",
|
|
keys={"p256dh": "public-key", "auth": "auth-secret"},
|
|
)
|
|
|
|
with pytest.raises(main.HTTPException) as raised:
|
|
await main.subscribe_push(payload, request)
|
|
|
|
assert raised.value.status_code == 503
|
|
assert store.is_subscribed("device-a") is False
|