Files
benevoles/backend/tests/test_request_logging.py
AdamThomasandClaude Opus 5.5 d0fa1e109c
ci-cd / build-backend (push) Successful in 38s
ci-cd / build-frontend (push) Successful in 20s
ci-cd / deploy (push) Successful in 13s
Logs de diagnostic et rapprochement SumUp ↔ bénévole
- Une ligne de log par requête (route modèle, statut, durée, code d'erreur,
  compte), identifiant X-Request-ID repris dans chaque log et affiché à
  l'utilisateur sous une erreur, exceptions non gérées tracées (500 JSON)
- Journal d'accès d'uvicorn coupé : il exposait le jeton du formulaire public
- Logs métier : paiement SumUp, connexion, import, plannings, solveur
- Nom du bénévole dans la description du checkout SumUp
- Code et identifiant de transaction SumUp enregistrés au paiement
  (migration 0023), affichés sur la fiche et dans l'export Excel

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:54:35 +02:00

79 lines
2.8 KiB
Python

"""Journal des requêtes (`app/request_logging.py`) : référence de requête,
route modèle sans jeton ni chaîne de requête, erreurs détaillées sans
valeur saisie."""
import logging
import pytest
from fastapi.testclient import TestClient
from app.main import app
def test_each_response_carries_a_request_id(client):
resp = client.get("/health")
assert len(resp.headers["X-Request-ID"]) >= 8
def test_a_well_formed_incoming_request_id_is_kept(client):
resp = client.get("/health", headers={"X-Request-ID": "caddy-1234abcd"})
assert resp.headers["X-Request-ID"] == "caddy-1234abcd"
def test_a_malformed_incoming_request_id_is_replaced(client):
resp = client.get("/health", headers={"X-Request-ID": "x\nforged log line"})
assert resp.headers["X-Request-ID"] != "x\nforged log line"
def test_access_line_uses_the_route_template_and_hides_tokens(raw_client, edition, caplog):
with caplog.at_level(logging.INFO, logger="app.request"):
raw_client.get(f"/editions/{edition.id}/form/public?token=secret-access-token")
line = next(r.getMessage() for r in caplog.records if r.name == "app.request")
assert "/editions/{edition_id}/form/public" in line
assert f"edition_id={edition.id}" in line
assert "secret-access-token" not in caplog.text
def test_business_error_code_is_logged(client, edition, caplog):
with caplog.at_level(logging.INFO, logger="app.request"):
resp = client.get(f"/editions/{edition.id}/volunteers/00000000-0000-0000-0000-000000000000")
assert resp.status_code == 404
record = next(r for r in caplog.records if r.name == "app.request")
assert record.levelno == logging.WARNING
assert "code=error.not_found" in record.getMessage()
def test_validation_error_logs_the_field_but_not_the_value(client, edition, caplog):
with caplog.at_level(logging.INFO, logger="app.request"):
resp = client.post(
f"/editions/{edition.id}/volunteers",
json={"first_name": "Alexandra-Secret", "last_name": 12345678},
)
assert resp.status_code == 422
assert "last_name" in caplog.text
assert "Alexandra-Secret" not in caplog.text
@pytest.fixture
def boom_route():
def boom():
raise RuntimeError("panne")
app.add_api_route("/__boom", boom)
yield
app.router.routes = [r for r in app.router.routes if getattr(r, "path", None) != "/__boom"]
def test_unhandled_exception_returns_a_reference(boom_route, caplog):
with caplog.at_level(logging.ERROR, logger="app.request"):
resp = TestClient(app).get("/__boom")
assert resp.status_code == 500
body = resp.json()
assert body["detail"]["code"] == "error.internal"
assert body["detail"]["request_id"] == resp.headers["X-Request-ID"]
assert "RuntimeError" in caplog.text