Files
ENI-projet-piscine/apps/backend/tests/api/test_sites.py
T
Johan LEROYandClaude Sonnet 5 5d921a9b1e feat: vue détail d'un site (#51)
Ajoute la page de détail d'un site (fiche, mesure instantanée, jauge de
consommation, historique) en remplacement du placeholder. Les champs
null sont affichés explicitement avec leur raison plutôt que masqués.

Ajoute l'endpoint GET /sites/{id}/current côté backend, qui retourne la
dernière mesure connue d'un site sans filtre temporel, conformément au
contrat de l'issue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012i5NteMLRZgfTAKB5GD37X
2026-09-17 16:43:15 +02:00

218 lines
6.0 KiB
Python

from collections.abc import Callable, Iterator
from datetime import UTC, datetime
from uuid import uuid4
import pytest
from fastapi import FastAPI
from httpx import AsyncClient
from app.api.deps import get_current_principal, get_reading_service, get_site_service
from app.core.principal import Principal
from app.core.roles import AccountKind, Role
from app.models.energy import Reading, Site
from app.services.site import SiteNotFoundError
def principal(role: Role = Role.LECTEUR) -> Principal:
return Principal(
id=uuid4(),
email=f"{role.value}@enervision.fr",
role=role,
kind=AccountKind.HUMAIN,
must_change_password=False,
)
def site(site_id: str = "site-1") -> Site:
return Site(
site_id=site_id,
site_name="Site de test",
site_type="industriel",
location="Toulouse",
capacity_kw=42.0,
status="actif",
)
class FauxService:
def __init__(self, erreur: Exception | None = None) -> None:
self._erreur = erreur
self.site = site()
async def list_all(self) -> list[Site]:
return [self.site]
async def get_by_id(self, site_id: str) -> Site:
if self._erreur is not None:
raise self._erreur
return self.site
def reading(site_id: str = "site-1") -> Reading:
return Reading(
reading_id=1,
site_id=site_id,
timestamp=datetime(2026, 9, 16, tzinfo=UTC),
source="csv",
consumption_kw=42.5,
data_quality="good",
raw_data={},
)
class FauxReadingService:
def __init__(self, derniere: Reading | None) -> None:
self._derniere = derniere
async def get_latest(self, site_id: str) -> Reading | None:
return self._derniere
@pytest.fixture
def lecteur_connecte(app: FastAPI) -> Iterator[None]:
app.dependency_overrides[get_current_principal] = lambda: principal()
yield
app.dependency_overrides.pop(get_current_principal, None)
@pytest.fixture
def servi(
app: FastAPI, lecteur_connecte: None
) -> Iterator[Callable[[Exception | None], FauxService]]:
def installe(erreur: Exception | None = None) -> FauxService:
service = FauxService(erreur)
app.dependency_overrides[get_site_service] = lambda: service
return service
yield installe
app.dependency_overrides.pop(get_site_service, None)
@pytest.fixture
def readings_servis(
app: FastAPI, lecteur_connecte: None
) -> Iterator[Callable[[Reading | None], FauxReadingService]]:
def installe(derniere: Reading | None) -> FauxReadingService:
service = FauxReadingService(derniere)
app.dependency_overrides[get_reading_service] = lambda: service
return service
yield installe
app.dependency_overrides.pop(get_reading_service, None)
async def test_list_sites_returns_the_sites(
servi: Callable[..., FauxService], client: AsyncClient
) -> None:
servi()
response = await client.get("/api/v1/sites")
assert response.status_code == 200
corps = response.json()
assert corps == [
{
"site_id": "site-1",
"site_name": "Site de test",
"site_type": "industriel",
"location": "Toulouse",
"capacity_kw": 42.0,
"status": "actif",
}
]
async def test_get_site_returns_the_matching_site(
servi: Callable[..., FauxService], client: AsyncClient
) -> None:
servi()
response = await client.get("/api/v1/sites/site-1")
assert response.status_code == 200
assert response.json()["site_id"] == "site-1"
async def test_get_site_returns_404_for_an_unknown_site(
servi: Callable[..., FauxService], client: AsyncClient
) -> None:
servi(SiteNotFoundError("site-inconnu"))
response = await client.get("/api/v1/sites/site-inconnu")
assert response.status_code == 404
async def test_list_sites_reaches_the_repository_through_the_session(
lecteur_connecte: None, fake_session: Callable[..., None], client: AsyncClient
) -> None:
fake_session(result=[site("a"), site("b")])
response = await client.get("/api/v1/sites")
assert response.status_code == 200
assert [s["site_id"] for s in response.json()] == ["a", "b"]
async def test_get_site_reaches_the_repository_through_the_session(
lecteur_connecte: None, fake_session: Callable[..., None], client: AsyncClient
) -> None:
fake_session(result=site("a"))
response = await client.get("/api/v1/sites/a")
assert response.status_code == 200
assert response.json()["site_id"] == "a"
async def test_get_site_returns_404_when_the_session_finds_nothing(
lecteur_connecte: None, fake_session: Callable[..., None], client: AsyncClient
) -> None:
fake_session(result=None)
response = await client.get("/api/v1/sites/inconnu")
assert response.status_code == 404
async def test_get_current_returns_the_latest_reading_regardless_of_its_age(
servi: Callable[..., FauxService],
readings_servis: Callable[[Reading | None], FauxReadingService],
client: AsyncClient,
) -> None:
servi()
readings_servis(reading())
response = await client.get("/api/v1/sites/site-1/current")
assert response.status_code == 200
corps = response.json()
assert corps["consumption_kw"] == 42.5
async def test_get_current_returns_null_when_the_site_has_no_reading(
servi: Callable[..., FauxService],
readings_servis: Callable[[Reading | None], FauxReadingService],
client: AsyncClient,
) -> None:
servi()
readings_servis(None)
response = await client.get("/api/v1/sites/site-1/current")
assert response.status_code == 200
assert response.json() is None
async def test_get_current_returns_404_for_an_unknown_site(
servi: Callable[..., FauxService],
readings_servis: Callable[[Reading | None], FauxReadingService],
client: AsyncClient,
) -> None:
servi(SiteNotFoundError("site-inconnu"))
readings_servis(None)
response = await client.get("/api/v1/sites/site-inconnu/current")
assert response.status_code == 404