Ajoute une page d'aide en ligne pour configurer ntfy et Tasker
La nouvelle route /aide regroupe dans l'app les étapes d'installation
de l'app ntfy (Play Store/F-Droid, abonnement au topic, piège
optimisation batterie, option auto-hébergé) et de Tasker (permissions
localisation, profils Enter/Exit avec HTTP Request), plus une section
vérification via /logs et un dépannage. Elle est accessible depuis la
nav et depuis les deux cartes ntfy/Tasker de /settings. Les 11 tests
de test_logs.py couvrent en plus les routes /logs et /aide, le logging
présence, l'isolation par user et la rotation.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
148
app/tests/test_logs.py
Normal file
148
app/tests/test_logs.py
Normal file
@ -0,0 +1,148 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user