Permet de vérifier en un clic que le serveur et le topic ntfy
renseignés fonctionnent, avant d'attendre un vrai rappel. Le bouton
envoie une notification de test via la même chaîne que les rappels
réels (donc tracée dans /logs), et affiche le résultat inline.
La fonction send_ntfy retourne désormais un booléen de succès pour
permettre ce retour utilisateur.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
188 lines
7.2 KiB
Python
188 lines
7.2 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)
|