En-têtes de sécurité, CORS resserré, caviardage des journaux, `/metrics` derrière un jeton facultatif, documentation fermée en préproduction, et la sonde de disponibilité cesse de publier la version de TimescaleDB. HSTS et CSP sont volontairement absents : l'application ignore si TLS termine devant elle, et une CSP sur une API JSON ne protège presque rien. Les deux appartiennent au terminateur TLS, celle qui compte protège la page Angular. `/metrics` est gardé par un jeton statique et non par un rôle : coupler la supervision au modèle d'utilisateurs casserait la collecte à chaque panne d'authentification, c'est-à-dire quand on en a le plus besoin. Le contrôle principal reste le réseau. Le caviardage est la troisième ligne de défense, pas la première. On ne passe aucun secret au logger et aucun jeton dans une URL ; le filtre rattrape ce que personne n'a relu, à commencer par l'écho SQL qui publiait les empreintes Argon2 quand `debug` est actif. Corrige un défaut que le test a révélé : `create_app(settings)` ne pilotait que la construction, les dépendances continuaient de lire `get_settings()` depuis l'environnement. Un test « en production » ne testait donc pas la production, et `TESTING.md` promet le contraire.
80 lines
2.4 KiB
Python
80 lines
2.4 KiB
Python
from collections.abc import Callable
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
from sqlalchemy.exc import OperationalError
|
|
|
|
|
|
async def test_liveness_exposes_service_metadata(client: AsyncClient) -> None:
|
|
response = await client.get("/api/v1/health/live")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"status": "ok",
|
|
"service": "EnerVision API",
|
|
"version": "0.1.0",
|
|
"environment": "local",
|
|
}
|
|
|
|
|
|
async def test_readiness_confirms_the_extension_without_leaking_its_version(
|
|
fake_session: Callable[..., None], client: AsyncClient
|
|
) -> None:
|
|
fake_session(result="2.22.1")
|
|
|
|
response = await client.get("/api/v1/health/ready")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"status": "ready",
|
|
"database": "reachable",
|
|
"timescaledb": "loaded",
|
|
}
|
|
assert "2.22.1" not in response.text
|
|
|
|
|
|
async def test_readiness_returns_503_when_the_extension_is_missing(
|
|
fake_session: Callable[..., None], client: AsyncClient
|
|
) -> None:
|
|
fake_session(result=None)
|
|
|
|
response = await client.get("/api/v1/health/ready")
|
|
|
|
assert response.status_code == 503
|
|
assert response.json()["detail"] == "Extension TimescaleDB absente"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"failure",
|
|
[
|
|
OperationalError("SELECT 1", {}, Exception("connexion refusee")),
|
|
ConnectionRefusedError(111, "Connection refused"),
|
|
],
|
|
ids=["erreur_sqlalchemy", "erreur_reseau_asyncpg"],
|
|
)
|
|
async def test_readiness_returns_503_when_database_is_unreachable(
|
|
fake_session: Callable[..., None], client: AsyncClient, failure: Exception
|
|
) -> None:
|
|
fake_session(failure=failure)
|
|
|
|
response = await client.get("/api/v1/health/ready")
|
|
|
|
assert response.status_code == 503
|
|
assert response.json()["detail"] == "Base de données injoignable"
|
|
|
|
|
|
@pytest.mark.parametrize("path", ["/openapi.json", "/metrics"])
|
|
async def test_technical_endpoints_are_served(client: AsyncClient, path: str) -> None:
|
|
assert (await client.get(path)).status_code == 200
|
|
|
|
|
|
@pytest.mark.integration
|
|
async def test_readiness_reaches_the_real_database(client: AsyncClient) -> None:
|
|
response = await client.get("/api/v1/health/ready")
|
|
|
|
assert response.status_code == 200, response.text
|
|
body = response.json()
|
|
assert body["status"] == "ready"
|
|
assert body["database"] == "reachable"
|
|
assert body["timescaledb"] == "loaded"
|