Les tests d'API remplacaient tous leur service par un double : rien ne prouvait que `endpoint -> service -> repository -> SQL` rende ce que l'endpoint serialise. Seules l'authentification et la matrice de roles traversaient vraiment la base. `tests/api/conftest.py` seme un jeu metier valide en base et nettoie derriere lui. Il valide ses ecritures, contrairement aux fixtures de `tests/repositories` : un endpoint ouvre sa propre session et ne verrait pas une transaction en cours. L'isolation vient de la marque portee par chaque `site_id`, jamais d'un total : ces routes listent toute la base. Les recommandations d'abord, parce que `POST /generate` est la seule route d'ecriture : son idempotence tient a une contrainte d'unicite et a un `on_conflict_do_nothing`, invérifiables hors base, et sa relecture par une seconde requete HTTP est la seule assertion du depot qui prouve que la validation atteint le disque. Cote sites, le depart des ex aequo par `reading_id` quand deux sources ecrivent la meme heure ne peut se demontrer qu'ainsi.
81 lines
2.7 KiB
Python
81 lines
2.7 KiB
Python
from collections.abc import Callable
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
from app.core.roles import Role
|
|
from app.db.session import get_session_factory
|
|
from tests.api.conftest import JeuMetier
|
|
from tests.repositories.test_reading import creer_lecture
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
async def test_list_sites_returns_the_seeded_site_with_its_stored_attributes(
|
|
jeu_metier: JeuMetier, principal_injecte: Callable[[Role], None], client: AsyncClient
|
|
) -> None:
|
|
principal_injecte(Role.LECTEUR)
|
|
|
|
reponse = await client.get("/api/v1/sites")
|
|
|
|
assert reponse.status_code == 200
|
|
mien = next(site for site in reponse.json() if site["site_id"] == jeu_metier.site_id)
|
|
assert mien["capacity_kw"] == 100.0
|
|
assert mien["site_name"] == "Site de test"
|
|
|
|
|
|
async def test_get_site_returns_404_when_the_identifier_is_absent_from_the_database(
|
|
principal_injecte: Callable[[Role], None], client: AsyncClient
|
|
) -> None:
|
|
principal_injecte(Role.LECTEUR)
|
|
|
|
reponse = await client.get("/api/v1/sites/SITE-JAMAIS-INSERE")
|
|
|
|
assert reponse.status_code == 404
|
|
assert reponse.json()["detail"] == "Site introuvable"
|
|
|
|
|
|
async def test_get_current_returns_the_most_recent_reading_when_several_hours_are_stored(
|
|
jeu_metier: JeuMetier, principal_injecte: Callable[[Role], None], client: AsyncClient
|
|
) -> None:
|
|
principal_injecte(Role.LECTEUR)
|
|
|
|
reponse = await client.get(f"/api/v1/sites/{jeu_metier.site_id}/current")
|
|
|
|
assert reponse.status_code == 200
|
|
corps = reponse.json()
|
|
assert corps["consumption_kw"] == 10.0
|
|
assert corps["timestamp"].startswith("2026-09-16T12:00")
|
|
|
|
|
|
async def test_get_current_keeps_the_highest_reading_id_when_two_sources_share_the_timestamp(
|
|
jeu_metier: JeuMetier, principal_injecte: Callable[[Role], None], client: AsyncClient
|
|
) -> None:
|
|
principal_injecte(Role.LECTEUR)
|
|
async with get_session_factory()() as session:
|
|
await creer_lecture(
|
|
session,
|
|
site_id=jeu_metier.site_id,
|
|
timestamp=jeu_metier.instant,
|
|
source="api_history",
|
|
consumption_kw=999.0,
|
|
)
|
|
await session.commit()
|
|
|
|
reponse = await client.get(f"/api/v1/sites/{jeu_metier.site_id}/current")
|
|
|
|
assert reponse.json()["consumption_kw"] == 999.0
|
|
|
|
|
|
async def test_get_current_reports_a_critical_quality_when_the_site_has_no_reading(
|
|
site_nu: str, principal_injecte: Callable[[Role], None], client: AsyncClient
|
|
) -> None:
|
|
principal_injecte(Role.LECTEUR)
|
|
|
|
reponse = await client.get(f"/api/v1/sites/{site_nu}/current")
|
|
|
|
assert reponse.status_code == 200
|
|
corps = reponse.json()
|
|
assert corps["timestamp"] is None
|
|
assert corps["data_quality"] == "critical"
|