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
178 lines
5.5 KiB
Python
178 lines
5.5 KiB
Python
from datetime import UTC, datetime, timedelta
|
|
|
|
import pytest
|
|
|
|
from app.models.energy import Reading
|
|
from app.services.reading import (
|
|
FENETRE_MAXIMALE,
|
|
FENETRE_PAR_DEFAUT,
|
|
FenetreInverseeError,
|
|
FenetreTropLargeError,
|
|
ReadingService,
|
|
)
|
|
|
|
|
|
def reading(reading_id: int = 1, site_id: str = "site-1") -> Reading:
|
|
return Reading(
|
|
reading_id=reading_id,
|
|
site_id=site_id,
|
|
timestamp=datetime(2026, 9, 16, tzinfo=UTC),
|
|
source="api_current",
|
|
consumption_kw=10.0,
|
|
data_quality="good",
|
|
raw_data={},
|
|
)
|
|
|
|
|
|
class FakeRepository:
|
|
def __init__(self, readings: list[Reading]) -> None:
|
|
self._readings = readings
|
|
self.appels: list[tuple[str | None, datetime, datetime, int, int]] = []
|
|
self.site_interroge: str | None = None
|
|
|
|
async def latest_for_site(self, site_id: str) -> Reading | None:
|
|
self.site_interroge = site_id
|
|
return self._readings[0] if self._readings else None
|
|
|
|
async def list_history(
|
|
self,
|
|
*,
|
|
start: datetime,
|
|
end: datetime,
|
|
site_id: str | None = None,
|
|
limit: int,
|
|
offset: int,
|
|
) -> list[Reading]:
|
|
self.appels.append((site_id, start, end, limit, offset))
|
|
return self._readings
|
|
|
|
|
|
async def test_get_latest_relays_the_repository_reading() -> None:
|
|
depot = FakeRepository([reading(1)])
|
|
service = ReadingService(readings=depot)
|
|
|
|
lecture = await service.get_latest("site-1")
|
|
|
|
assert lecture is not None
|
|
assert lecture.reading_id == 1
|
|
assert depot.site_interroge == "site-1"
|
|
|
|
|
|
async def test_get_latest_returns_none_when_the_site_has_no_reading() -> None:
|
|
service = ReadingService(readings=FakeRepository([]))
|
|
|
|
lecture = await service.get_latest("site-1")
|
|
|
|
assert lecture is None
|
|
|
|
|
|
async def test_list_history_returns_the_repository_readings() -> None:
|
|
service = ReadingService(readings=FakeRepository([reading(1), reading(2)]))
|
|
|
|
lectures = await service.list_history(limit=500, offset=0)
|
|
|
|
assert [r.reading_id for r in lectures] == [1, 2]
|
|
|
|
|
|
async def test_list_history_relays_the_site_id_limit_and_offset() -> None:
|
|
depot = FakeRepository([])
|
|
service = ReadingService(readings=depot)
|
|
debut = datetime(2026, 9, 1, tzinfo=UTC)
|
|
fin = datetime(2026, 9, 2, tzinfo=UTC)
|
|
|
|
await service.list_history(site_id="site-1", start=debut, end=fin, limit=50, offset=10)
|
|
|
|
assert depot.appels == [("site-1", debut, fin, 50, 10)]
|
|
|
|
|
|
async def test_list_history_defaults_to_the_last_24_hours_when_no_window_is_given() -> None:
|
|
depot = FakeRepository([])
|
|
service = ReadingService(readings=depot)
|
|
avant = datetime.now(UTC)
|
|
|
|
await service.list_history(limit=500, offset=0)
|
|
|
|
apres = datetime.now(UTC)
|
|
_, debut, fin, _, _ = depot.appels[0]
|
|
assert avant <= fin <= apres
|
|
assert fin - debut == FENETRE_PAR_DEFAUT
|
|
|
|
|
|
async def test_list_history_defaults_end_to_now_when_only_start_is_given() -> None:
|
|
depot = FakeRepository([])
|
|
service = ReadingService(readings=depot)
|
|
debut = datetime.now(UTC) - timedelta(hours=1)
|
|
avant = datetime.now(UTC)
|
|
|
|
await service.list_history(start=debut, limit=500, offset=0)
|
|
|
|
apres = datetime.now(UTC)
|
|
_, debut_transmis, fin, _, _ = depot.appels[0]
|
|
assert debut_transmis == debut
|
|
assert avant <= fin <= apres
|
|
|
|
|
|
async def test_list_history_defaults_start_to_24_hours_before_end_when_only_end_is_given() -> None:
|
|
depot = FakeRepository([])
|
|
service = ReadingService(readings=depot)
|
|
fin = datetime(2026, 9, 16, tzinfo=UTC)
|
|
|
|
await service.list_history(end=fin, limit=500, offset=0)
|
|
|
|
_, debut, fin_transmise, _, _ = depot.appels[0]
|
|
assert fin_transmise == fin
|
|
assert debut == fin - FENETRE_PAR_DEFAUT
|
|
|
|
|
|
async def test_list_history_normalizes_naive_datetimes_to_utc() -> None:
|
|
depot = FakeRepository([])
|
|
service = ReadingService(readings=depot)
|
|
|
|
await service.list_history(
|
|
start=datetime(2026, 9, 1), end=datetime(2026, 9, 2), limit=500, offset=0
|
|
)
|
|
|
|
_, debut, fin, _, _ = depot.appels[0]
|
|
assert debut == datetime(2026, 9, 1, tzinfo=UTC)
|
|
assert fin == datetime(2026, 9, 2, tzinfo=UTC)
|
|
|
|
|
|
async def test_list_history_raises_when_start_is_after_end() -> None:
|
|
service = ReadingService(readings=FakeRepository([]))
|
|
|
|
with pytest.raises(FenetreInverseeError):
|
|
await service.list_history(
|
|
start=datetime(2026, 9, 2, tzinfo=UTC),
|
|
end=datetime(2026, 9, 1, tzinfo=UTC),
|
|
limit=500,
|
|
offset=0,
|
|
)
|
|
|
|
|
|
async def test_list_history_raises_when_start_equals_end() -> None:
|
|
service = ReadingService(readings=FakeRepository([]))
|
|
instant = datetime(2026, 9, 1, tzinfo=UTC)
|
|
|
|
with pytest.raises(FenetreInverseeError):
|
|
await service.list_history(start=instant, end=instant, limit=500, offset=0)
|
|
|
|
|
|
async def test_list_history_raises_when_the_window_exceeds_the_maximum_span() -> None:
|
|
service = ReadingService(readings=FakeRepository([]))
|
|
debut = datetime(2026, 1, 1, tzinfo=UTC)
|
|
fin = debut + FENETRE_MAXIMALE + timedelta(seconds=1)
|
|
|
|
with pytest.raises(FenetreTropLargeError):
|
|
await service.list_history(start=debut, end=fin, limit=500, offset=0)
|
|
|
|
|
|
async def test_list_history_accepts_a_window_exactly_at_the_maximum_span() -> None:
|
|
depot = FakeRepository([])
|
|
service = ReadingService(readings=depot)
|
|
debut = datetime(2026, 1, 1, tzinfo=UTC)
|
|
fin = debut + FENETRE_MAXIMALE
|
|
|
|
await service.list_history(start=debut, end=fin, limit=500, offset=0)
|
|
|
|
assert depot.appels == [(None, debut, fin, 500, 0)]
|