CI / lint (push) Successful in 52s
CI / security (push) Successful in 36s
CI / test (push) Successful in 1m6s
CI / build (push) Successful in 1m15s
CI / e2e (push) Failing after 12m40s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
#78 Excalidraw — finitions B5/C8/F2/F3 - backend/indexer.py: port Python pur de lz-string decompressFromBase64 (bitsPerChar=6, resetValue=32) validé contre 4 fixtures JS truth (texte accentué, edge cases) — remplace le stub base64 non-fonctionnel - B5 indexation: .excalidraw.md (format plugin Obsidian) maintenant décompressé côté Python → texte indexé pour recherche TF-IDF - 7 nouveaux tests B5 dans tests/test_excalidraw.py (13/13 total) - F2: excalidraw-viewer.test.mjs enregistré dans CI (lint job) - F3: tests/e2e/excalidraw.spec.js (9 specs — ouverture .excalidraw/.excalidraw.md, création via modale/menu contextuel, toolbar, thème, pop-out, recherche) - Fixtures test_vault/diagram.excalidraw + diagram.excalidraw.md #67 Push notifications (Web Push API + VAPID) - backend/push.py: router + VAPID keys + send_push_notification (pywebpush>=2.3.0) - frontend/js/push.js: subscription UI + service worker integration - tests/test_push.py: 10 tests (subscribe/list/unsubscribe/vapid key) - requirements.txt + sw.js + locales push strings #68 Health check enrichi - backend/main.py: GET /api/health/detailed (admin) — memory/cpu/disk/backups/index/SSE connections - backend/indexer.py: _last_full_index_ts tracking - tests/conftest.py: admin_client fixture - tests/test_api_main.py: 3 nouveaux tests health detailed #77 Desktop jumplist - desktop/src/jumplist.rs: Windows jumplist integration - Cargo.toml/lock + capabilities + main.rs - frontend/js/desktop.js: Tauri bridge (isTauriEnv, invoke, getSystemTheme, syncSystemTheme, shouldShowWizard, crash banner) - tests/frontend/desktop.test.mjs: 21 tests JSDOM (détection, invoke degradation, theme gating, wizard, crash banner) - tests/frontend/unit.test.mjs: desktop.js whitelisted (standalone global reader) # Frontend & CI - frontend/sw.js: réécriture complète (precache + push event handlers + offline) - frontend/index.html: section push dans settings + about repositionné - frontend/js/app.js: initDesktopIntegration + initPush - CI .gitea/workflows/ci.yml: excalidraw-viewer.test.mjs ajouté au lint job All checks: ruff clean, 552 backend tests pass, 9 frontend unit, 5 excalidraw-viewer, 21 desktop, 9 pane-manager, validate-imports 33 modules.
190 lines
7.6 KiB
Python
190 lines
7.6 KiB
Python
"""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 |