Files
ENI-projet-piscine/apps/backend/tests/api/test_openapi.py
T
Johan LEROY e50921c907 feat(backend): expose GET /api/v1/alerts
Consultation des alertes de consommation, filtrable par site_id et
severity a l'identique du contrat GET /alerts de l'API Mock. Reprend
le gabarit endpoints -> services -> repositories -> models pose par
sites, sur la table alert deja creee par la revision Alembic
e6d2026091501.

Generalise aussi le garde-fou OpenAPI du 403 (ROUTES_A_ROLE) au-dela
du seul tag users, pour que l'ajout d'alerts a la liste des routes
protegees par role soit reellement verifie.

Closes #59
2026-09-16 13:06:37 +02:00

121 lines
4.1 KiB
Python

# Pourquoi : `openapi.json` est versionné, donc une route qui change son contrat public le montre
# dans la diff d'une pull request. `test_the_committed_contract_matches_the_generated_one` est ce
# qui empêche le fichier de dériver du code sans que personne ne le voie.
import json
from typing import Any
import pytest
from app import cli
METHODES = {"get", "post", "patch", "put", "delete"}
# `/auth/logout` lit le cookie mais ne le réclame pas : sans session elle répond 204, et un 401
# documenté y serait faux.
SANS_REFUS = {("POST", "/api/v1/auth/logout")}
ORIGINE_VERIFIEE = {
("POST", "/api/v1/auth/refresh"),
("POST", "/api/v1/auth/logout"),
("POST", "/api/v1/auth/logout-all"),
("POST", "/api/v1/auth/password"),
}
# Toute route derrière `require_role` (LecteurDep, OperateurDep, AdminDep) peut rendre 403 pour
# `password_change_required`, pas seulement les routes `admin`.
ROUTES_A_ROLE = {
("GET", "/api/v1/users"),
("POST", "/api/v1/users"),
("PATCH", "/api/v1/users/{id}"),
("POST", "/api/v1/users/{id}/password-reset"),
("GET", "/api/v1/sites"),
("GET", "/api/v1/sites/{site_id}"),
("GET", "/api/v1/alerts"),
}
@pytest.fixture(scope="module")
def schema() -> dict[str, Any]:
return cli.schema_du_contrat()
def operations(schema: dict[str, Any]) -> list[tuple[str, str, dict[str, Any]]]:
return [
(methode.upper(), chemin, operation)
for chemin, operations_du_chemin in schema["paths"].items()
for methode, operation in operations_du_chemin.items()
if methode in METHODES
]
def test_the_committed_contract_matches_the_generated_one(schema: dict[str, Any]) -> None:
publie = json.loads(cli.CHEMIN_CONTRAT.read_text(encoding="utf-8"))
assert publie == schema, "lancer `make openapi` et versionner le fichier obtenu"
def test_every_route_demanding_an_identity_says_how_it_refuses(schema: dict[str, Any]) -> None:
muettes = [
(methode, chemin)
for methode, chemin, operation in operations(schema)
if operation.get("security")
and (methode, chemin) not in SANS_REFUS
and "401" not in operation["responses"]
]
assert muettes == []
def test_every_role_guarded_route_documents_the_role_refusal(schema: dict[str, Any]) -> None:
sans_403 = [
(methode, chemin)
for methode, chemin, operation in operations(schema)
if (methode, chemin) in ROUTES_A_ROLE and "403" not in operation["responses"]
]
assert sans_403 == []
def test_every_origin_checked_route_documents_the_csrf_refusal(schema: dict[str, Any]) -> None:
sans_403 = [
(methode, chemin)
for methode, chemin, operation in operations(schema)
if (methode, chemin) in ORIGINE_VERIFIEE and "403" not in operation["responses"]
]
assert sans_403 == []
def test_the_validation_model_matches_what_the_handler_returns(schema: dict[str, Any]) -> None:
modeles = {
operation["responses"]["422"]["content"]["application/json"]["schema"]["$ref"]
for _, _, operation in operations(schema)
if "422" in operation["responses"]
}
assert modeles == {"#/components/schemas/ValidationErrorResponse"}
assert "HTTPValidationError" not in schema["components"]["schemas"]
def test_the_rate_limit_documents_the_delay_header(schema: dict[str, Any]) -> None:
trop_de_tentatives = schema["paths"]["/api/v1/auth/login"]["post"]["responses"]["429"]
assert "Retry-After" in trop_de_tentatives["headers"]
def test_the_refresh_cookie_appears_in_the_security_schemes(schema: dict[str, Any]) -> None:
schemes = schema["components"]["securitySchemes"]
assert schemes["Cookie de rafraîchissement"]["in"] == "cookie"
assert schemes["Cookie de rafraîchissement"]["name"] == "ev_refresh"
def test_each_tag_used_by_a_route_is_described(schema: dict[str, Any]) -> None:
decrits = {tag["name"] for tag in schema["tags"]}
for methode, chemin, operation in operations(schema):
poses = operation.get("tags", [])
assert len(poses) == len(set(poses)), f"tag en double sur {methode} {chemin}"
assert set(poses) <= decrits, f"tag non décrit sur {methode} {chemin}"