import asyncio from datetime import datetime, timezone from unittest.mock import AsyncMock import pytest from fastapi import HTTPException from app.api.v1.endpoints.timeseries import scada as scada_endpoint def _reading(device_id: str) -> scada_endpoint.ScadaReadingBatchItem: return scada_endpoint.ScadaReadingBatchItem( time=datetime(2026, 8, 27, tzinfo=timezone.utc), device_id=device_id, monitored_value=12.5, ) def test_scada_batch_rejects_devices_missing_from_bizdb(monkeypatch): monkeypatch.setattr( scada_endpoint.ScadaInfoRepository, "get_existing_device_ids", AsyncMock(return_value={"known"}), ) insert = AsyncMock() monkeypatch.setattr(scada_endpoint.ScadaRepository, "insert_scada_batch", insert) with pytest.raises(HTTPException, match="missing") as exc_info: asyncio.run( scada_endpoint.insert_scada_data( [_reading("known"), _reading("missing")], conn=object(), postgres_conn=object(), ) ) assert exc_info.value.status_code == 422 insert.assert_not_awaited() def test_scada_batch_writes_only_after_bizdb_validation(monkeypatch): monkeypatch.setattr( scada_endpoint.ScadaInfoRepository, "get_existing_device_ids", AsyncMock(return_value={"known"}), ) insert = AsyncMock() monkeypatch.setattr(scada_endpoint.ScadaRepository, "insert_scada_batch", insert) result = asyncio.run( scada_endpoint.insert_scada_data( [_reading(" known ")], conn=object(), postgres_conn=object(), ) ) assert result == {"message": "Inserted 1 records"} assert insert.await_args.args[1][0]["device_id"] == "known" def test_latest_scada_readings_join_device_metadata_with_latest_rows(monkeypatch): observed_at = datetime(2026, 10, 22, 1, 2, tzinfo=timezone.utc) monkeypatch.setattr( scada_endpoint.ScadaInfoRepository, "get_scadas", AsyncMock(return_value=[ { "device_id": "PRESSURE-1", "device_type": "pressure", "measurement_unit": "MPa", "transmission_mode": "realtime", "transmission_frequency": "15min", }, { "device_id": "FLOW-2", "device_type": "pipe_flow", "measurement_unit": "m3/h", "transmission_mode": "realtime", "transmission_frequency": "15min", }, ]), ) latest = AsyncMock(return_value={ "PRESSURE-1": { "time": observed_at, "device_id": "PRESSURE-1", "monitored_value": 0.31, "cleaned_value": 0.3, } }) monkeypatch.setattr( scada_endpoint.ScadaRepository, "get_latest_scada_readings", latest, ) result = asyncio.run( scada_endpoint.get_latest_scada_readings( limit=100, offset=0, conn=object(), postgres_conn=object(), ) ) assert result.total == 2 assert result.observed_count == 1 assert result.latest_observed_at == observed_at assert result.items[0].monitored_value == pytest.approx(0.31) assert result.items[1].observed_at is None assert latest.await_args.args[1] == ["PRESSURE-1", "FLOW-2"] def test_latest_scada_readings_applies_device_page_before_timeseries_query(monkeypatch): monkeypatch.setattr( scada_endpoint.ScadaInfoRepository, "get_scadas", AsyncMock(return_value=[ { "device_id": f"D-{index}", "device_type": "pressure", "measurement_unit": "MPa", "transmission_mode": "realtime", "transmission_frequency": "15min", } for index in range(4) ]), ) latest = AsyncMock(return_value={}) monkeypatch.setattr( scada_endpoint.ScadaRepository, "get_latest_scada_readings", latest, ) result = asyncio.run( scada_endpoint.get_latest_scada_readings( limit=2, offset=1, conn=object(), postgres_conn=object(), ) ) assert result.total == 4 assert [item.device_id for item in result.items] == ["D-1", "D-2"] assert latest.await_args.args[1] == ["D-1", "D-2"]