"""Tests for the Push Notifications endpoints (ROADMAP #67). Covers: - GET /api/push/vapid-public-key - POST /api/push/subscribe (auth + vault permission) - DELETE /api/push/subscribe - GET /api/push/subscriptions - send_push_notification() internals """ import json import os from pathlib import Path import pytest @pytest.fixture(autouse=True) def _reset_push_state(): """Reset backend.push module globals between tests to avoid cross-test pollution.""" import backend.push as push push._push_subscriptions = [] push._vapid_keys = {} # Remove any persisted subscription/vapid files from previous tests for f in (push.PUSH_SUBSCRIPTIONS_FILE, push.VAPID_KEYS_FILE): try: if f.exists(): f.unlink() except OSError: pass yield push._push_subscriptions = [] push._vapid_keys = {} def _login(client, username="admin", password="chab30"): resp = client.post( "/api/auth/login", json={"username": username, "password": password}, ) assert resp.status_code == 200, resp.text return resp.json()["access_token"] def _bearer(token): return {"Authorization": f"Bearer {token}"} # ═════════════════════════════════════════════════════════════════════ # /api/push/vapid-public-key # ═════════════════════════════════════════════════════════════════════ class TestVapidPublicKey: def test_returns_public_key(self, admin_client): resp = admin_client.get("/api/push/vapid-public-key") assert resp.status_code == 200 data = resp.json() assert "public_key" in data assert data["public_key"] # non-empty def test_no_auth_required(self, admin_client): # Public endpoint should work without a bearer token resp = admin_client.get("/api/push/vapid-public-key") assert resp.status_code == 200 assert "public_key" in resp.json() # ═════════════════════════════════════════════════════════════════════ # /api/push/subscribe # ═════════════════════════════════════════════════════════════════════ class TestSubscribe: VALID_SUB = { "subscription": { "endpoint": "https://fcm.googleapis.com/fcm/send/test123", "keys": { "p256dh": "BGO6V2xE5U_bL5vZcGpZQpWRJ1Oea9QwkrfGzBVAqBiY", "auth": "cHVsU2hpbmVzQXV0aEtleQ", }, }, "vault": "*", } def test_subscribe_ok(self, admin_client): token = _login(admin_client) resp = admin_client.post( "/api/push/subscribe", json=self.VALID_SUB, headers=_bearer(token), ) assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert data["subscription_id"] def test_subscribe_requires_auth(self, admin_client): resp = admin_client.post("/api/push/subscribe", json=self.VALID_SUB) assert resp.status_code in (401, 403) def test_subscribe_idempotent_same_endpoint(self, admin_client): token = _login(admin_client) resp1 = admin_client.post( "/api/push/subscribe", json=self.VALID_SUB, headers=_bearer(token) ) resp2 = admin_client.post( "/api/push/subscribe", json=self.VALID_SUB, headers=_bearer(token) ) assert resp1.json()["subscription_id"] == resp2.json()["subscription_id"] def test_subscribe_no_vault_access(self, admin_client): token = _login(admin_client, username="normaluser", password="normal123") sub = { "subscription": { "endpoint": "https://fcm.google.com/fcm/send/nope", "keys": {"p256dh": "abcd", "auth": "efgh"}, }, "vault": "OtherVault", } resp = admin_client.post( "/api/push/subscribe", json=sub, headers=_bearer(token) ) assert resp.status_code == 403 # ═════════════════════════════════════════════════════════════════════ # /api/push/subscriptions (list) # ═════════════════════════════════════════════════════════════════════ class TestListSubscriptions: def test_list_empty_first(self, admin_client): token = _login(admin_client) resp = admin_client.get("/api/push/subscriptions", headers=_bearer(token)) assert resp.status_code == 200 assert resp.json()["subscriptions"] == [] def test_list_after_subscribe(self, admin_client): token = _login(admin_client) sub = { "subscription": { "endpoint": "https://fcm.googleapis.com/fcm/send/listme", "keys": {"p256dh": "abcd", "auth": "efgh"}, }, "vault": "*", } admin_client.post( "/api/push/subscribe", json=sub, headers=_bearer(token) ) resp = admin_client.get("/api/push/subscriptions", headers=_bearer(token)) assert resp.status_code == 200 data = resp.json() assert len(data["subscriptions"]) == 1 assert data["subscriptions"][0]["vault"] == "*" # endpoint truncated for privacy assert data["subscriptions"][0]["endpoint"].endswith("...") # ═════════════════════════════════════════════════════════════════════ # DELETE /api/push/subscribe # ═════════════════════════════════════════════════════════════════════ class TestUnsubscribe: def test_unsubscribe_ok(self, admin_client): token = _login(admin_client) sub = { "subscription": { "endpoint": "https://fcm.googleapis.com/fcm/send/bye", "keys": {"p256dh": "abcd", "auth": "efgh"}, }, "vault": "*", } admin_client.post("/api/push/subscribe", json=sub, headers=_bearer(token)) resp = admin_client.delete( "/api/push/subscribe", params={"endpoint": sub["subscription"]["endpoint"]}, headers=_bearer(token), ) assert resp.status_code == 200 assert resp.json()["success"] is True # Now list should be empty resp = admin_client.get("/api/push/subscriptions", headers=_bearer(token)) assert resp.json()["subscriptions"] == [] def test_unsubscribe_unknown(self, admin_client): token = _login(admin_client) resp = admin_client.delete( "/api/push/subscribe", params={"endpoint": "https://fcm.google/fcm/send/unknown"}, headers=_bearer(token), ) assert resp.status_code == 200 assert resp.json()["success"] is False