Files
pointeuse-optimisator/app/tests/test_logs.py
Jacquin Antoine 1d8a273ffb Ajoute la régénèse du token de présence Tasker et élargit le bandeau de test ntfy
Le bouton "Régénérer le token" de la carte Tasker crée un nouveau token
secret et invalide immédiatement les anciennes URLs, au cas où elles
auraient fuité. La fonction regenerate_presence_token préserve les
autres réglages notif (topic, serveur, jeton d'accès, plage). Au passage
le message de retour du bouton de test ntfy occupe désormais toute la
largeur de la carte au lieu d'être limité à 420px.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-20 00:05:34 +02:00

302 lines
12 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"
# ── Régénération du token de présence (Tasker) ───────────────────────────────
def test_regenerate_presence_token_requires_auth():
unauth = TestClient(main.app)
r = unauth.post("/settings/regenerate-presence-token", follow_redirects=False)
assert r.status_code == 302
assert "/login" in r.headers["location"]
def test_regenerate_presence_token_invalidates_old_urls():
"""Le nouveau token remplace l'ancien : les anciennes URLs ne marchent plus."""
old_token = models.load_notif_config("alice")["token"]
r = client.post("/settings/regenerate-presence-token", cookies=COOKIES)
assert r.status_code == 200
# Le HTML retourné contient de nouvelles URLs avec un nouveau token
assert old_token not in r.text
assert "/presence/" in r.text
assert "Token régénéré" in r.text
# L'ancienne URL doit maintenant renvoyer 404 (token inconnu)
r_old = client.get(f"/presence/{old_token}/arrivee")
assert r_old.status_code == 404
# La nouvelle URL doit fonctionner
new_cfg = models.load_notif_config("alice")
r_new = client.get(f"/presence/{new_cfg['token']}/arrivee")
assert r_new.status_code == 200
assert r_new.json() == {"ok": True}
def test_regenerate_preserves_other_notif_fields():
"""La régénération ne doit pas effacer topic/server/token ntfy."""
models.save_notif_config(
"alice", ntfy_topic="topic-x", ntfy_server="https://ntfy.arkel.fr",
ntfy_token="tk_xyz",
)
before = models.load_notif_config("alice")
new_token = models.regenerate_presence_token("alice")
after = models.load_notif_config("alice")
assert after["token"] == new_token
assert before["token"] != new_token
# Les autres champs sont préservés
assert after["ntfy_topic"] == "topic-x"
assert after["ntfy_server"] == "https://ntfy.arkel.fr"
assert after["ntfy_token"] == "tk_xyz"