Files
pointeuse-optimisator/app/tests/test_logs.py
Jacquin Antoine 076b5a4fa2 Ajoute le support d'un jeton d'accès pour les serveurs ntfy protégés
Certains serveurs ntfy auto-hébergés (ex: ntfy.arkel.fr) renvoient
403 Forbidden sans authentification. Un nouveau champ "Jeton d'accès"
(optionnel, type password) sur /settings permet de renseigner un token
qui sera envoyé comme "Authorization: Bearer ..." à chaque publication,
pour les rappels réels comme pour le bouton de test. La page /aide
documente le cas 403 et la marche à suivre.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-20 00:01:12 +02:00

253 lines
9.5 KiB
Python

"""Smoke tests pour les nouvelles routes /logs et /aide + journalisation présence."""
import sys
import tempfile
from pathlib import Path
# Prépare un DATA_DIR temporaire AVANT l'import de models
_TMP = Path(tempfile.mkdtemp())
import models
models.DATA_DIR = _TMP
models.CONFIG_FILE = models.DATA_DIR / "config.json"
models.CONFIG_FILE.write_text('{"heures_jour": 7.8, "allowed_email_domain": "x.fr"}')
# Recharge main dans un état propre
sys.path.insert(0, ".")
import main
from fastapi.testclient import TestClient
client = TestClient(main.app)
def _login_as(uid: str):
"""Crée un user avec mdp puis se connecte (renvoie les cookies)."""
import bcrypt
models.save_auth(uid, email=f"{uid}@x.fr",
password_hash=bcrypt.hashpw(b"x" * 12, bcrypt.gensalt()).decode())
r = client.post("/login/password", data={"email": f"{uid}@x.fr", "password": "x" * 12},
follow_redirects=False)
assert r.status_code == 302, r.text
return {"user_id": uid}
COOKIES = _login_as("alice")
# ── Route /logs ──────────────────────────────────────────────────────────────
def test_logs_page_requires_auth():
# Client frais, sans cookie, doit être redirigé vers /login
unauth = TestClient(main.app)
r = unauth.get("/logs", follow_redirects=False)
assert r.status_code == 302
assert "/login" in r.headers["location"]
def test_logs_page_empty_for_new_user():
r = client.get("/logs", cookies=COOKIES)
assert r.status_code == 200
assert "Journaux" in r.text
assert "Aucun événement" in r.text # état initial
def test_logs_page_shows_entries_after_presence():
token = models.load_notif_config("alice")["token"]
# Simule une arrivée Tasker
client.get(f"/presence/{token}/arrivee")
# Simule un départ
client.get(f"/presence/{token}/depart")
r = client.get("/logs", cookies=COOKIES)
assert r.status_code == 200
assert "arrivée" in r.text
assert "départ" in r.text
assert "Présence" in r.text
assert "Aucun événement" not in r.text
def test_logs_filter_query_param():
# Toutes les entrées sont de type "presence" à ce stade
r_all = client.get("/logs", cookies=COOKIES)
r_notif = client.get("/logs?filter=notif", cookies=COOKIES)
r_pres = client.get("/logs?filter=presence", cookies=COOKIES)
# Le filtre "notif" ne doit PAS montrer les entrées presence
assert "arrivée" not in r_notif.text
# Le filtre "presence" DOIT les montrer
assert "arrivée" in r_pres.text
assert "départ" in r_pres.text
# Le total Tout inclut tout
assert "arrivée" in r_all.text
# ── Route /aide ──────────────────────────────────────────────────────────────
def test_aide_page_requires_auth():
unauth = TestClient(main.app)
r = unauth.get("/aide", follow_redirects=False)
assert r.status_code == 302
assert "/login" in r.headers["location"]
def test_aide_page_content():
r = client.get("/aide", cookies=COOKIES)
assert r.status_code == 200
# Vérifie la présence des sections clés
for needle in ["ntfy", "Tasker", "Aide", "topic", "géofence", "/logs"]:
assert needle in r.text, f"missing {needle!r}"
# ── Logging via notifications.send_ntfy (échec) ─────────────────────────────
def test_send_ntfy_failure_logs_entry():
# Serveur injoignable → échec → entrée de log "échec"
import notifications
notifications.send_ntfy(
"http://127.0.0.1:1", "topic-injoignable",
"msg test", "titre test", user_id="alice",
)
logs = models.load_logs("alice")
assert any(l["event"] == "échec" and l["type"] == "notif" for l in logs)
# ── Navigation ──────────────────────────────────────────────────────────────
def test_nav_links_present():
r = client.get("/aide", cookies=COOKIES)
assert 'href="/logs"' in r.text
assert 'href="/aide"' in r.text
assert 'href="/settings"' in r.text
# ── Isolation par user ──────────────────────────────────────────────────────
def test_logs_isolated_per_user():
# Bob ne doit pas voir les logs d'alice
bob_cookies = _login_as("bob")
r = client.get("/logs", cookies=bob_cookies)
assert "arrivée" not in r.text
assert "Aucun événement" in r.text
# ── Rotation des logs ───────────────────────────────────────────────────────
def test_log_rotation_caps_at_max():
# Alice a déjà quelques entrées ; on en ajoute largement > MAX_LOGS
for i in range(models.MAX_LOGS + 50):
models.append_log("alice", "notif", "envoyée", f"msg {i}")
logs = models.load_logs("alice")
assert len(logs) == models.MAX_LOGS
# Le plus récent est bien le dernier inséré
assert logs[0]["message"] == f"msg {models.MAX_LOGS + 49}"
def test_invalid_log_type_rejected():
try:
models.append_log("alice", "invalid_type", "x", "y")
assert False, "doit lever une AssertionError"
except AssertionError:
pass
# ── Bouton de test ntfy (/settings/test-ntfy) ────────────────────────────────
def test_test_ntfy_requires_auth():
unauth = TestClient(main.app)
r = unauth.post("/settings/test-ntfy", follow_redirects=False)
assert r.status_code == 302
assert "/login" in r.headers["location"]
def test_test_ntfy_without_topic_returns_error():
# Alice n'a pas de topic configuré → message d'erreur
models.save_notif_config("alice", ntfy_topic="", ntfy_server="https://ntfy.sh")
r = client.post("/settings/test-ntfy", cookies=COOKIES)
assert r.status_code == 200
assert "settings-error" in r.text
assert "topic" in r.text.lower()
def test_test_ntfy_with_bad_server_returns_failure():
# Topic renseigné mais serveur injoignable → échec
models.save_notif_config("alice", ntfy_topic="topic-test", ntfy_server="http://127.0.0.1:1")
r = client.post("/settings/test-ntfy", cookies=COOKIES)
assert r.status_code == 200
assert "settings-error" in r.text
assert "Échec" in r.text
# L'entrée de log d'échec doit être présente
logs = models.load_logs("alice")
assert any(l["event"] == "échec" and l["type"] == "notif" for l in logs)
def test_test_ntfy_logs_to_journal():
"""Le bouton de test doit écrire dans le journal (même en échec)."""
models.save_notif_config("alice", ntfy_topic="topic-test-2", ntfy_server="http://127.0.0.1:1")
client.post("/settings/test-ntfy", cookies=COOKIES)
logs = models.load_logs("alice")
# L'entrée la plus récente doit être l'échec du test (message contient "Test pointeuse")
assert any(l["event"] == "échec" and "Test pointeuse" in l["message"] for l in logs)
# ── Jeton d'accès ntfy (serveur protégé) ─────────────────────────────────────
def test_send_ntfy_passes_authorization_header(monkeypatch):
"""Quand un token est fourni, send_ntfy doit envoyer un header Authorization: Bearer."""
import notifications
captured = {}
class _DummyReq:
def __init__(self, url, data, headers, method):
captured["url"] = url
captured["headers"] = headers
captured["method"] = method
class _DummyResp:
def close(self): pass
def _fake_urlopen(req, timeout):
captured["timeout"] = timeout
return _DummyResp()
monkeypatch.setattr(notifications.urllib.request, "Request", _DummyReq)
monkeypatch.setattr(notifications.urllib.request, "urlopen", _fake_urlopen)
ok = notifications.send_ntfy(
"https://ntfy.example", "topic-x",
"msg", "titre", user_id="alice", token="tk_abc123",
)
assert ok is True
assert captured["headers"]["Authorization"] == "Bearer tk_abc123"
def test_send_ntfy_omits_authorization_when_no_token(monkeypatch):
"""Sans token, pas de header Authorization."""
import notifications
captured = {}
class _DummyReq:
def __init__(self, url, data, headers, method):
captured["headers"] = headers
class _DummyResp:
def close(self): pass
monkeypatch.setattr(notifications.urllib.request, "Request", _DummyReq)
monkeypatch.setattr(notifications.urllib.request, "urlopen", lambda req, timeout: _DummyResp())
notifications.send_ntfy("https://x", "t", "m", "ti", user_id=None, token="")
assert "Authorization" not in captured["headers"]
def test_settings_save_persists_ntfy_token():
"""Le token saisi dans /settings doit être persisté."""
models.save_notif_config("alice", ntfy_topic="t", ntfy_server="https://ntfy.sh")
client.post("/settings", cookies=COOKIES, data={
"ntfy_topic": "topic-persisted",
"ntfy_server": "https://ntfy.arkel.fr",
"ntfy_token": "tk_persisted_123",
"rappel_debut_h": "8",
"rappel_fin_h": "19",
})
cfg = models.load_notif_config("alice")
assert cfg["ntfy_token"] == "tk_persisted_123"
assert cfg["ntfy_topic"] == "topic-persisted"