import json import asyncio import os import sqlite3 import time from types import SimpleNamespace import httpx import pytest from src import dashboard_auth, gitea_proxy, main from src.push_notifications import PushConfiguration, dispatch_unread_updates from src.push_subscription_store import PushSubscriptionStore from src.push_endpoint_policy import UnsafePushEndpoint @pytest.fixture(autouse=True) def resolve_test_push_endpoints_publicly(monkeypatch): async def accept(endpoint): return endpoint monkeypatch.setattr(main, "validate_public_push_endpoint", accept) def test_dispatch_lease_is_exclusive_recoverable_and_owner_fenced(tmp_path): path = tmp_path / "push.sqlite3" first = PushSubscriptionStore(path) second = PushSubscriptionStore(path) assert first.acquire_dispatch_lease("worker-a", now=100, lease_seconds=30) is True assert second.acquire_dispatch_lease("worker-b", now=100, lease_seconds=30) is False assert second.acquire_dispatch_lease("worker-b", now=131, lease_seconds=30) is True assert first.release_dispatch_lease("worker-a") is False assert second.release_dispatch_lease("worker-b") is True def test_subscription_store_uses_private_filesystem_permissions(tmp_path): state_dir = tmp_path / "push-state" previous_umask = os.umask(0) try: database = state_dir / "push.sqlite3" PushSubscriptionStore(database) finally: os.umask(previous_umask) assert state_dir.stat().st_mode & 0o777 == 0o700 assert database.stat().st_mode & 0o777 == 0o600 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_subscription_store_reopens_a_delivered_thread_when_its_revision_changes(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"}, }) first = store.claim_unseen({42: "2026-08-11T12:00:00Z"}) store.mark_delivered("session-a", first[0].thread_revisions) unchanged = store.claim_unseen({42: "2026-08-11T12:00:00Z"}) updated = store.claim_unseen({42: "2026-08-11T12:05:00Z"}) assert unchanged == [] assert updated[0].thread_revisions == ((42, "2026-08-11T12:05:00Z"),) def test_subscription_store_does_not_realert_for_an_older_snapshot_revision(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"}, }) store.mark_delivered("session-a", {42: "2026-08-11T12:05:00Z"}) assert store.claim_unseen({42: "2026-08-11T12:00:00Z"}) == [] def test_complete_unread_snapshot_reconciles_delivery_and_digest_checkpoints(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"}, }) revisions = {1: "r1", 2: "r2", 3: "r3"} store.mark_delivered("session-a", revisions) store.mark_digest_pending("session-a", {3: "r3"}) store.reconcile_unread({2}) delivery = store.claim_unseen(revisions)[0] assert delivery.thread_ids == (1, 3) assert delivery.digest_ids == () @pytest.mark.anyio async def test_dispatch_reconciles_stale_checkpoints_after_a_complete_snapshot(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"}, }) store.mark_delivered("session-a", {1: "r1"}) async def unread(): return {"items": [{"id": 2, "updated_at": "r2"}], "complete": True} async def send(_subscription, _payload): return None config = PushConfiguration("public", "private", "mailto:ops@example.com") assert await dispatch_unread_updates(store, config, unread, send) == 1 remaining = store.claim_unseen({1: "r1", 2: "r2"}) assert remaining[0].thread_ids == (1,) @pytest.mark.anyio async def test_dispatch_sends_nothing_and_preserves_checkpoints_for_an_incomplete_snapshot(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"}, }) store.mark_delivered("session-a", {1: "r1"}) sent = [] async def unread(): return {"items": [{"id": 2, "updated_at": "r2"}], "complete": False} async def send(_subscription, payload): sent.append(payload) config = PushConfiguration("public", "private", "mailto:ops@example.com") assert await dispatch_unread_updates(store, config, unread, send) == 0 assert sent == [] assert store.claim_unseen({1: "r1"}) == [] def test_subscription_store_migrates_legacy_delivery_without_replaying_it(tmp_path): path = tmp_path / "push.sqlite3" subscription = { "endpoint": "https://push.example/device-a", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, } with sqlite3.connect(path) as connection: connection.executescript(""" CREATE TABLE push_subscriptions ( session_id TEXT PRIMARY KEY, endpoint TEXT NOT NULL UNIQUE, subscription_json TEXT NOT NULL ); CREATE TABLE push_deliveries ( session_id TEXT NOT NULL, thread_id INTEGER NOT NULL, PRIMARY KEY (session_id, thread_id) ); """) connection.execute( "INSERT INTO push_subscriptions VALUES (?, ?, ?)", ("session-a", subscription["endpoint"], json.dumps(subscription)), ) connection.execute("INSERT INTO push_deliveries VALUES (?, ?)", ("session-a", 42)) store = PushSubscriptionStore(path) assert store.claim_unseen({42: "2026-08-11T12:00:00Z"}) == [] updated = store.claim_unseen({42: "2026-08-11T12:05:00Z"}) assert updated[0].thread_revisions == ((42, "2026-08-11T12:05:00Z"),) @pytest.mark.anyio async def test_managed_session_active_applies_configured_idle_deadline(monkeypatch): calls = [] class Store: def managed_status(self, management_id, *, idle_timeout_seconds): calls.append((management_id, idle_timeout_seconds)) return "active" if management_id == "active-device" else "idle" monkeypatch.setenv("STACKCHAIN_DASHBOARD_IDLE_TIMEOUT_SECONDS", "321") monkeypatch.setattr(dashboard_auth, "_session_store", lambda: Store()) assert await dashboard_auth.managed_session_active("active-device") is True assert await dashboard_auth.managed_session_active("idle-device") is False assert calls == [("active-device", 321), ("idle-device", 321)] @pytest.mark.anyio async def test_push_poll_authorizes_delivery_against_managed_session(monkeypatch): captured = {} async def no_wait(_seconds): return None async def stop_after_capture(*args, **kwargs): captured["unread"] = args[2] captured.update(kwargs) raise asyncio.CancelledError monkeypatch.setenv("STACKCHAIN_PUSH_MAX_CONCURRENCY", "3") monkeypatch.setenv("STACKCHAIN_PUSH_MAX_INDIVIDUAL_NOTIFICATIONS", "4") monkeypatch.setattr(main.asyncio, "sleep", no_wait) monkeypatch.setattr(main, "dispatch_unread_updates", stop_after_capture) with pytest.raises(asyncio.CancelledError): await main._push_poll_loop() assert captured["unread"] is main.gitea_proxy.unread_notification_snapshot assert captured["session_active"] is dashboard_auth.managed_session_active assert captured["max_concurrency"] == 3 assert captured["max_individual_notifications"] == 4 @pytest.mark.anyio async def test_push_poll_still_dispatches_deadlines_when_unread_dispatch_fails(monkeypatch): calls = [] sleeps = 0 async def no_wait(_seconds): nonlocal sleeps sleeps += 1 if sleeps > 1: raise asyncio.CancelledError async def fail_unread(*_args, **_kwargs): calls.append("unread") raise RuntimeError("unread unavailable") async def dispatch_deadlines(*_args, **_kwargs): calls.append("deadline") monkeypatch.setattr(main.asyncio, "sleep", no_wait) monkeypatch.setattr(main, "dispatch_unread_updates", fail_unread) monkeypatch.setattr(main, "dispatch_deadline_reminders", dispatch_deadlines) with pytest.raises(asyncio.CancelledError): await main._push_poll_loop() assert calls == ["unread", "deadline"] def test_dispatch_leases_are_isolated_by_channel(tmp_path): path = tmp_path / "push.sqlite3" first = PushSubscriptionStore(path) second = PushSubscriptionStore(path) assert first.acquire_dispatch_lease( "unread-owner", channel="unread", now=100, lease_seconds=30 ) is True assert second.acquire_dispatch_lease( "deadline-owner", channel="deadline", now=100, lease_seconds=30 ) is True assert second.acquire_dispatch_lease( "other-unread-owner", channel="unread", now=100, lease_seconds=30 ) is False 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": [ {"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", "notification_id": 42, } assert "private/repo" not in json.dumps(sent) assert "Secret title" not in json.dumps(sent) @pytest.mark.anyio async def test_dispatch_realerts_when_the_same_thread_has_a_newer_revision(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"}, }) revision = "2026-08-11T12:00:00Z" sent = [] async def unread(): return {"items": [{ "id": 42, "updated_at": revision, "repository": "private/repo", "title": "Secret follow-up", }]} async def send(_subscription, payload): sent.append(json.loads(payload)) config = PushConfiguration("public", "private", "mailto:ops@example.com") assert await dispatch_unread_updates(store, config, unread, send) == 1 assert await dispatch_unread_updates(store, config, unread, send) == 0 revision = "2026-08-11T12:05:00Z" assert await dispatch_unread_updates(store, config, unread, send) == 1 assert [payload["notification_id"] for payload in sent] == [42, 42] assert "updated_at" not in json.dumps(sent) assert "Secret follow-up" not in json.dumps(sent) @pytest.mark.anyio async def test_update_burst_sends_bounded_individual_pushes_and_one_private_digest(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": [ { "id": notification_id, "repository": "private/repo", "title": f"Secret update {notification_id}", } for notification_id in range(41, 46) ]} async def send(_subscription, payload): sent.append(json.loads(payload)) config = PushConfiguration("public", "private", "mailto:ops@example.com") delivered = await dispatch_unread_updates( store, config, unread, send, max_individual_notifications=2, ) assert delivered == 3 assert [payload["tag"] for payload in sent] == [ "stackchain-update-41", "stackchain-update-42", "stackchain-update-digest", ] assert sent[-1] == { "title": "3 new work updates", "body": "Tap to review them in Stackchain.", "route": "#/my-work/updates", "tag": "stackchain-update-digest", "update_count": 3, } assert "private/repo" not in json.dumps(sent) assert "Secret update" not in json.dumps(sent) assert await dispatch_unread_updates( store, config, unread, send, max_individual_notifications=2, ) == 0 @pytest.mark.anyio async def test_failed_digest_retries_only_overflow_after_individual_checkpoints(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"}, }) attempts = [] async def unread(): return {"items": [{"id": notification_id} for notification_id in range(51, 56)]} async def fail_digest(_subscription, payload): decoded = json.loads(payload) attempts.append(decoded["tag"]) if decoded["tag"] == "stackchain-update-digest": raise RuntimeError("push provider unavailable") config = PushConfiguration("public", "private", "mailto:ops@example.com") assert await dispatch_unread_updates( store, config, unread, fail_digest, max_individual_notifications=2, ) == 2 assert store.claim_unseen(range(51, 56))[0].thread_ids == (53, 54, 55) async def succeed(_subscription, payload): attempts.append(json.loads(payload)["tag"]) assert await dispatch_unread_updates( store, config, unread, succeed, max_individual_notifications=2, ) == 1 assert attempts == [ "stackchain-update-51", "stackchain-update-52", "stackchain-update-digest", "stackchain-update-digest", ] assert store.claim_unseen(range(51, 56)) == [] @pytest.mark.anyio async def test_production_notification_page_dispatches_one_unread_push(tmp_path): def upstream(_request): return httpx.Response(200, json=[{ "id": 42, "unread": True, "repository": {"full_name": "private/repo"}, "subject": { "title": "Secret title", "type": "Issue", "html_url": "https://forge.example/private/repo/issues/7", }, }]) gitea_proxy.start_client(transport=httpx.MockTransport(upstream)) try: page = await gitea_proxy.notification_page(1) finally: await gitea_proxy.stop_client() 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 page async def send(_subscription, payload): sent.append(json.loads(payload)) config = PushConfiguration("public", "private", "mailto:ops@example.com") assert await dispatch_unread_updates(store, config, unread, send) == 1 assert sent == [{ "title": "New work update", "body": "Tap to review it in Stackchain.", "route": "#/my-work/update/42", "tag": "stackchain-update-42", "notification_id": 42, }] assert "private/repo" not in json.dumps(sent) assert "Secret title" not in json.dumps(sent) @pytest.mark.anyio async def test_concurrent_workers_do_not_dispatch_the_same_update(tmp_path): path = tmp_path / "push.sqlite3" first = PushSubscriptionStore(path) second = PushSubscriptionStore(path) first.upsert("session-a", { "endpoint": "https://push.example/device-a", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) sending = asyncio.Event() finish = asyncio.Event() sent = [] async def unread(): return {"items": [{"id": 42}]} async def send(_subscription, payload): sent.append(json.loads(payload)["tag"]) sending.set() await finish.wait() config = PushConfiguration("public", "private", "mailto:ops@example.com") active = asyncio.create_task(dispatch_unread_updates(first, config, unread, send)) await sending.wait() competing = await asyncio.wait_for( dispatch_unread_updates(second, config, unread, send), timeout=0.1 ) finish.set() assert competing == 0 assert await active == 1 assert sent == ["stackchain-update-42"] @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": [{"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_dispatch_removes_an_endpoint_that_rebinds_private_without_contacting_it(tmp_path): store = PushSubscriptionStore(tmp_path / "push.sqlite3") store.upsert("unsafe-device", { "endpoint": "https://push.example/rebound", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) contacted = [] async def unread(): return {"items": [{"id": 8}]} async def reject_rebound(_endpoint): raise UnsafePushEndpoint("Endpoint must resolve to a public Web Push service") async def send(subscription, _payload): contacted.append(subscription["endpoint"]) config = PushConfiguration("public", "private", "mailto:ops@example.com") assert await dispatch_unread_updates( store, config, unread, send, endpoint_validator=reject_rebound ) == 0 assert contacted == [] assert store.is_subscribed("unsafe-device") is False @pytest.mark.anyio async def test_transient_endpoint_failure_does_not_block_healthy_devices(tmp_path): store = PushSubscriptionStore(tmp_path / "push.sqlite3") for session_id in ("session-a", "session-b"): store.upsert(session_id, { "endpoint": f"https://push.example/{session_id}", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) sent = [] async def unread(): return {"items": [{"id": 8}]} async def send(subscription, _payload): if subscription["endpoint"].endswith("session-a"): raise RuntimeError("provider unavailable") sent.append(subscription["endpoint"]) config = PushConfiguration("public", "private", "mailto:ops@example.com") assert await dispatch_unread_updates(store, config, unread, send) == 1 assert sent == ["https://push.example/session-b"] assert store.claim_unseen({8})[0].session_id == "session-a" @pytest.mark.anyio async def test_unexpected_device_failure_waits_for_siblings_and_retries_only_that_device(tmp_path): path = tmp_path / "push.sqlite3" store = PushSubscriptionStore(path) competitor = PushSubscriptionStore(path) for session_id in ("failing-device", "healthy-device"): store.upsert(session_id, { "endpoint": f"https://push.example/{session_id}", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) healthy_started = asyncio.Event() release_healthy = asyncio.Event() sent = [] async def unread(): return {"items": [{"id": 8, "updated_at": "r1"}], "complete": True} async def validate(endpoint): if endpoint.endswith("failing-device"): raise TimeoutError("endpoint validation unavailable") return endpoint async def send(subscription, _payload): sent.append(subscription["endpoint"]) healthy_started.set() await release_healthy.wait() config = PushConfiguration("public", "private", "mailto:ops@example.com") dispatch = asyncio.create_task(dispatch_unread_updates( store, config, unread, send, endpoint_validator=validate, )) await asyncio.wait_for(healthy_started.wait(), timeout=0.2) await asyncio.sleep(0) assert dispatch.done() is False assert competitor.acquire_dispatch_lease( "competing-worker", now=time.time(), lease_seconds=60 ) is False release_healthy.set() assert await dispatch == 1 remaining = store.claim_unseen({8: "r1"}) assert [(item.session_id, item.thread_ids) for item in remaining] == [ ("failing-device", (8,)) ] retried = [] async def validate_recovered(endpoint): return endpoint async def send_retry(subscription, _payload): retried.append(subscription["endpoint"]) assert await dispatch_unread_updates( store, config, unread, send_retry, endpoint_validator=validate_recovered, ) == 1 assert retried == ["https://push.example/failing-device"] assert sent == ["https://push.example/healthy-device"] @pytest.mark.anyio async def test_transient_failure_stops_that_device_until_the_next_poll(tmp_path): store = PushSubscriptionStore(tmp_path / "push.sqlite3") for session_id in ("failing-device", "healthy-device"): store.upsert(session_id, { "endpoint": f"https://push.example/{session_id}", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) attempts = [] async def unread(): return {"items": [ {"id": 8}, {"id": 9}, ]} async def send(subscription, payload): endpoint = subscription["endpoint"] thread_id = int(json.loads(payload)["route"].rsplit("/", 1)[1]) attempts.append((endpoint, thread_id)) if endpoint.endswith("failing-device"): raise RuntimeError("provider unavailable") config = PushConfiguration("public", "private", "mailto:ops@example.com") assert await dispatch_unread_updates(store, config, unread, send) == 2 assert [item for item in attempts if item[0].endswith("failing-device")] == [ ("https://push.example/failing-device", 8) ] remaining = {item.session_id: item.thread_ids for item in store.claim_unseen({8, 9})} assert remaining == {"failing-device": (8, 9)} @pytest.mark.anyio async def test_timed_out_endpoint_does_not_stall_push_fanout(tmp_path): store = PushSubscriptionStore(tmp_path / "push.sqlite3") for session_id in ("session-a", "session-b"): store.upsert(session_id, { "endpoint": f"https://push.example/{session_id}", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) sent = [] async def unread(): return {"items": [{"id": 13}]} async def send(subscription, _payload): if subscription["endpoint"].endswith("session-a"): await asyncio.Event().wait() sent.append(subscription["endpoint"]) config = PushConfiguration("public", "private", "mailto:ops@example.com") delivered = await asyncio.wait_for( dispatch_unread_updates( store, config, unread, send, send_timeout_seconds=0.01 ), timeout=0.2, ) assert delivered == 1 assert sent == ["https://push.example/session-b"] @pytest.mark.anyio async def test_dispatches_devices_concurrently_with_a_strict_bound(tmp_path): store = PushSubscriptionStore(tmp_path / "push.sqlite3") for index in range(4): store.upsert(f"session-{index}", { "endpoint": f"https://push.example/session-{index}", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) active = 0 peak = 0 bound_reached = asyncio.Event() release = asyncio.Event() async def unread(): return {"items": [{"id": 13}]} async def send(_subscription, _payload): nonlocal active, peak active += 1 peak = max(peak, active) if active == 2: bound_reached.set() await release.wait() active -= 1 config = PushConfiguration("public", "private", "mailto:ops@example.com") dispatch = asyncio.create_task(dispatch_unread_updates( store, config, unread, send, max_concurrency=2 )) await asyncio.wait_for(bound_reached.wait(), timeout=0.2) assert peak == 2 assert dispatch.done() is False release.set() assert await dispatch == 4 assert peak == 2 @pytest.mark.anyio async def test_dispatch_removes_inactive_sessions_without_blocking_active_devices(tmp_path): store = PushSubscriptionStore(tmp_path / "push.sqlite3") for session_id in ("expired-device", "active-device"): store.upsert(session_id, { "endpoint": f"https://push.example/{session_id}", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) sent = [] async def unread(): return {"items": [{"id": 21}]} async def session_active(management_id): return management_id == "active-device" async def send(subscription, _payload): sent.append(subscription["endpoint"]) config = PushConfiguration("public", "private", "mailto:ops@example.com") assert await dispatch_unread_updates( store, config, unread, send, session_active=session_active ) == 1 assert store.is_subscribed("expired-device") is False assert store.is_subscribed("active-device") is True assert sent == ["https://push.example/active-device"] @pytest.mark.anyio async def test_dispatch_fails_closed_and_retains_subscriptions_when_session_registry_fails(tmp_path): store = PushSubscriptionStore(tmp_path / "push.sqlite3") store.upsert("active-device", { "endpoint": "https://push.example/active-device", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) sent = [] async def unread(): return {"items": [{"id": 22}]} async def registry_unavailable(_management_id): raise RuntimeError("session registry unavailable") async def send(subscription, _payload): sent.append(subscription) config = PushConfiguration("public", "private", "mailto:ops@example.com") assert await dispatch_unread_updates( store, config, unread, send, session_active=registry_unavailable ) == 0 assert store.is_subscribed("active-device") is True assert sent == [] @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(**_kwargs): return {"items": [], "complete": True} monkeypatch.setattr(main.gitea_proxy, "unread_notification_snapshot", 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", "deadline_enabled": False, "timezone": "UTC", "reminder_hour": 9, } 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_authenticated_device_can_enable_deadline_reminders(tmp_path, monkeypatch): store = PushSubscriptionStore(tmp_path / "push.sqlite3") store.upsert("session-a", { "endpoint": "https://push.example/device-a", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) monkeypatch.setattr(main, "_push_subscription_store", store) async def management_id(_session): return "session-a" monkeypatch.setattr(main.dashboard_auth, "session_management_id", management_id) request = SimpleNamespace(state=SimpleNamespace(dashboard_session=object())) payload = main.DeadlineReminderPayload( enabled=True, timezone="America/New_York", reminder_hour=9 ) assert await main.update_deadline_reminders(payload, request) == { "deadline_enabled": True, "timezone": "America/New_York", "reminder_hour": 9, } assert store.deadline_preferences("session-a")["enabled"] is True @pytest.mark.anyio async def test_subscription_rejects_an_unsafe_endpoint_before_persistence(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 "unsafe-device" async def reject_private(_endpoint): raise UnsafePushEndpoint("Endpoint must resolve to a public Web Push service") monkeypatch.setattr(main.dashboard_auth, "session_management_id", management_id) monkeypatch.setattr(main, "validate_public_push_endpoint", reject_private) 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 rejected: await main.subscribe_push(payload, request) assert rejected.value.status_code == 422 assert rejected.value.detail == "Endpoint must resolve to a public Web Push service" assert store.is_subscribed("unsafe-device") is False @pytest.mark.anyio async def test_subscription_baselines_every_revision_from_the_complete_snapshot(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" revisions = { thread_id: f"2026-08-11T12:{thread_id % 60:02d}:00Z" for thread_id in range(1, 126) } async def complete_snapshot(**_kwargs): return { "items": [ {"id": thread_id, "updated_at": revision} for thread_id, revision in revisions.items() ], "total": 125, "complete": True, } async def first_page_must_not_be_used(): raise AssertionError("subscription used the first-page inbox endpoint") monkeypatch.setattr(main.dashboard_auth, "session_management_id", management_id) monkeypatch.setattr(main.gitea_proxy, "unread_notification_snapshot", complete_snapshot) monkeypatch.setattr(main, "notifications", first_page_must_not_be_used) request = SimpleNamespace(state=SimpleNamespace(dashboard_session=object())) payload = main.PushSubscriptionPayload( endpoint="https://push.example/device-a", keys={"p256dh": "public-key", "auth": "auth-secret"}, ) assert await main.subscribe_push(payload, request) == {"subscribed": True} assert store.claim_unseen(revisions) == [] @pytest.mark.anyio async def test_subscription_baselines_production_notification_ids(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 current_unread(**_kwargs): return {"items": [{"id": 42}], "complete": True} monkeypatch.setattr(main.dashboard_auth, "session_management_id", management_id) monkeypatch.setattr(main.gitea_proxy, "unread_notification_snapshot", current_unread) request = SimpleNamespace(state=SimpleNamespace(dashboard_session=object())) payload = main.PushSubscriptionPayload( endpoint="https://push.example/device-a", keys={"p256dh": "public-key", "auth": "auth-secret"}, ) assert await main.subscribe_push(payload, request) == {"subscribed": True} assert store.claim_unseen({42}) == [] assert store.claim_unseen({42, 43})[0].thread_ids == (43,) @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(**_kwargs): raise RuntimeError("Gitea unavailable") monkeypatch.setattr(main.dashboard_auth, "session_management_id", management_id) monkeypatch.setattr(main.gitea_proxy, "unread_notification_snapshot", 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