"""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