Files
ObsiGate/tests/test_push.py
T
bruno ac16fc1f0e
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
feat: #78 Excalidraw finitions + #67 push + #68 health-detailed + #77 desktop jumplist
#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.
2026-09-09 23:18:29 -04:00

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