225 lines
6.8 KiB
Python
225 lines
6.8 KiB
Python
from dataclasses import dataclass, field
|
|
from datetime import UTC, datetime
|
|
|
|
from app.services.sensor import SensorService
|
|
|
|
TIMESTAMP = datetime(2026, 9, 16, 12, 0, tzinfo=UTC)
|
|
|
|
|
|
@dataclass
|
|
class FauxSite:
|
|
site_id: str
|
|
site_name: str
|
|
|
|
|
|
@dataclass
|
|
class FauxLecture:
|
|
site_id: str
|
|
timestamp: datetime
|
|
data_quality: str | None
|
|
null_reasons: list[str] | None = field(default_factory=list)
|
|
consumption_kw: float | None = 10.0
|
|
voltage_v: float | None = 230.0
|
|
current_a: float | None = 5.0
|
|
power_factor: float | None = 0.95
|
|
temperature_celsius: float | None = 21.0
|
|
humidity_percent: float | None = 40.0
|
|
|
|
|
|
class FauxDepotSites:
|
|
def __init__(self, sites: list[FauxSite]) -> None:
|
|
self._sites = sites
|
|
|
|
async def list_all(self) -> list[FauxSite]:
|
|
return self._sites
|
|
|
|
|
|
class FauxDepotLectures:
|
|
def __init__(self, lectures: list[FauxLecture]) -> None:
|
|
self._lectures = lectures
|
|
|
|
async def latest_by_site(self) -> list[FauxLecture]:
|
|
return self._lectures
|
|
|
|
|
|
async def test_status_marks_a_site_without_any_reading_as_critical_with_every_sensor_failing() -> (
|
|
None
|
|
):
|
|
service = SensorService(
|
|
sites=FauxDepotSites([FauxSite("A", "Site A")]), # type: ignore[arg-type]
|
|
readings=FauxDepotLectures([]), # type: ignore[arg-type]
|
|
)
|
|
|
|
etat = await service.status()
|
|
|
|
site = etat.sites[0]
|
|
assert site.overall == "critical"
|
|
for capteur in (
|
|
site.sensors.consumption,
|
|
site.sensors.electrical,
|
|
site.sensors.temperature,
|
|
site.sensors.humidity,
|
|
site.sensors.network,
|
|
):
|
|
assert capteur.status == "failing"
|
|
assert capteur.since is None
|
|
|
|
|
|
async def test_status_marks_every_sensor_ok_on_a_good_quality_reading_with_no_null_field() -> None:
|
|
service = SensorService(
|
|
sites=FauxDepotSites([FauxSite("A", "Site A")]), # type: ignore[arg-type]
|
|
readings=FauxDepotLectures([FauxLecture("A", TIMESTAMP, "good")]), # type: ignore[arg-type]
|
|
)
|
|
|
|
etat = await service.status()
|
|
|
|
site = etat.sites[0]
|
|
assert site.overall == "ok"
|
|
for capteur in (
|
|
site.sensors.consumption,
|
|
site.sensors.electrical,
|
|
site.sensors.temperature,
|
|
site.sensors.humidity,
|
|
site.sensors.network,
|
|
):
|
|
assert capteur.status == "ok"
|
|
assert capteur.since is None
|
|
|
|
|
|
async def test_status_flags_the_sensor_named_in_null_reasons() -> None:
|
|
service = SensorService(
|
|
sites=FauxDepotSites([FauxSite("A", "Site A")]), # type: ignore[arg-type]
|
|
readings=FauxDepotLectures( # type: ignore[arg-type]
|
|
[
|
|
FauxLecture(
|
|
"A",
|
|
TIMESTAMP,
|
|
"partial",
|
|
null_reasons=["temperature_sensor_failure"],
|
|
temperature_celsius=None,
|
|
)
|
|
]
|
|
),
|
|
)
|
|
|
|
etat = await service.status()
|
|
|
|
site = etat.sites[0]
|
|
assert site.overall == "degraded"
|
|
assert site.sensors.temperature.status == "failing"
|
|
assert site.sensors.temperature.since == TIMESTAMP
|
|
assert site.sensors.consumption.status == "ok"
|
|
assert site.sensors.electrical.status == "ok"
|
|
assert site.sensors.humidity.status == "ok"
|
|
assert site.sensors.network.status == "ok"
|
|
|
|
|
|
async def test_status_flags_a_sensor_from_a_null_field_even_without_a_null_reason() -> None:
|
|
service = SensorService(
|
|
sites=FauxDepotSites([FauxSite("A", "Site A")]), # type: ignore[arg-type]
|
|
readings=FauxDepotLectures( # type: ignore[arg-type]
|
|
[FauxLecture("A", TIMESTAMP, "partial", null_reasons=[], humidity_percent=None)]
|
|
),
|
|
)
|
|
|
|
etat = await service.status()
|
|
|
|
site = etat.sites[0]
|
|
assert site.sensors.humidity.status == "failing"
|
|
assert site.sensors.humidity.since == TIMESTAMP
|
|
|
|
|
|
async def test_status_flags_electrical_as_failing_when_any_of_its_three_fields_is_null() -> None:
|
|
service = SensorService(
|
|
sites=FauxDepotSites([FauxSite("A", "Site A")]), # type: ignore[arg-type]
|
|
readings=FauxDepotLectures( # type: ignore[arg-type]
|
|
[FauxLecture("A", TIMESTAMP, "partial", null_reasons=[], power_factor=None)]
|
|
),
|
|
)
|
|
|
|
etat = await service.status()
|
|
|
|
site = etat.sites[0]
|
|
assert site.sensors.electrical.status == "failing"
|
|
|
|
|
|
async def test_status_forces_every_sensor_to_failing_when_overall_is_critical() -> None:
|
|
service = SensorService(
|
|
sites=FauxDepotSites([FauxSite("A", "Site A")]), # type: ignore[arg-type]
|
|
readings=FauxDepotLectures([FauxLecture("A", TIMESTAMP, "critical", null_reasons=[])]), # type: ignore[arg-type]
|
|
)
|
|
|
|
etat = await service.status()
|
|
|
|
site = etat.sites[0]
|
|
assert site.overall == "critical"
|
|
for capteur in (
|
|
site.sensors.consumption,
|
|
site.sensors.electrical,
|
|
site.sensors.temperature,
|
|
site.sensors.humidity,
|
|
site.sensors.network,
|
|
):
|
|
assert capteur.status == "failing"
|
|
assert capteur.since == TIMESTAMP
|
|
|
|
|
|
async def test_status_treats_an_unknown_data_quality_as_critical() -> None:
|
|
service = SensorService(
|
|
sites=FauxDepotSites([FauxSite("A", "Site A")]), # type: ignore[arg-type]
|
|
readings=FauxDepotLectures([FauxLecture("A", TIMESTAMP, None, null_reasons=[])]), # type: ignore[arg-type]
|
|
)
|
|
|
|
etat = await service.status()
|
|
|
|
assert etat.sites[0].overall == "critical"
|
|
|
|
|
|
async def test_status_ignores_an_unknown_null_reason() -> None:
|
|
service = SensorService(
|
|
sites=FauxDepotSites([FauxSite("A", "Site A")]), # type: ignore[arg-type]
|
|
readings=FauxDepotLectures( # type: ignore[arg-type]
|
|
[FauxLecture("A", TIMESTAMP, "good", null_reasons=["something_else"])]
|
|
),
|
|
)
|
|
|
|
etat = await service.status()
|
|
|
|
site = etat.sites[0]
|
|
assert site.overall == "ok"
|
|
for capteur in (
|
|
site.sensors.consumption,
|
|
site.sensors.electrical,
|
|
site.sensors.temperature,
|
|
site.sensors.humidity,
|
|
site.sensors.network,
|
|
):
|
|
assert capteur.status == "ok"
|
|
|
|
|
|
async def test_status_flags_network_from_null_reasons_only() -> None:
|
|
service = SensorService(
|
|
sites=FauxDepotSites([FauxSite("A", "Site A")]), # type: ignore[arg-type]
|
|
readings=FauxDepotLectures( # type: ignore[arg-type]
|
|
[
|
|
FauxLecture(
|
|
"A",
|
|
TIMESTAMP,
|
|
"partial",
|
|
null_reasons=["network_loss"],
|
|
)
|
|
]
|
|
),
|
|
)
|
|
|
|
etat = await service.status()
|
|
|
|
site = etat.sites[0]
|
|
assert site.overall == "degraded"
|
|
assert site.sensors.network.status == "failing"
|
|
assert site.sensors.network.since == TIMESTAMP
|
|
assert site.sensors.consumption.status == "ok"
|
|
assert site.sensors.electrical.status == "ok"
|
|
assert site.sensors.temperature.status == "ok"
|
|
assert site.sensors.humidity.status == "ok"
|