import asyncio from datetime import datetime, timezone from unittest.mock import AsyncMock import pandas as pd import pytest from fastapi import HTTPException from app.api.v1.endpoints.timeseries import composite as composite_endpoint from app.infra.db.timescaledb import composite_queries def test_clean_scada_uses_current_project_metadata(monkeypatch): """Fengyang data must not be classified with the global tjwater metadata.""" monkeypatch.setattr( composite_queries.ScadaInfoRepository, "get_scadas", AsyncMock( return_value=[{"id": "fengyang-pressure-1", "type": "pressure"}] ), ) monkeypatch.setattr( composite_queries.ScadaRepository, "get_scada_field_by_id_time_range", AsyncMock( return_value={ "fengyang-pressure-1": [ {"time": "2026-06-01T00:00:00+08:00", "value": 26.5} ] } ), ) update_mock = AsyncMock() monkeypatch.setattr( composite_queries.ScadaRepository, "update_scada_field", update_mock, ) monkeypatch.setattr( composite_queries, "clean_pressure_data_df_km", lambda frame: pd.DataFrame( { "time": frame["time"], "fengyang-pressure-1": frame["fengyang-pressure-1"], } ), ) result = asyncio.run( composite_queries.CompositeQueries.clean_scada_data( object(), object(), ["fengyang-pressure-1"], datetime(2026, 6, 1, tzinfo=timezone.utc), datetime(2026, 6, 2, tzinfo=timezone.utc), ) ) assert result == "success" update_mock.assert_awaited_once() def test_clean_scada_rejects_devices_missing_from_project_metadata(monkeypatch): monkeypatch.setattr( composite_queries.ScadaInfoRepository, "get_scadas", AsyncMock(return_value=[{"id": "other-device", "type": "pressure"}]), ) query_mock = AsyncMock() monkeypatch.setattr( composite_queries.ScadaRepository, "get_scada_field_by_id_time_range", query_mock, ) with pytest.raises(ValueError, match="缺少元数据"): asyncio.run( composite_queries.CompositeQueries.clean_scada_data( object(), object(), ["fengyang-pressure-1"], datetime(2026, 6, 1, tzinfo=timezone.utc), datetime(2026, 6, 2, tzinfo=timezone.utc), ) ) query_mock.assert_not_awaited() def test_clean_scada_rejects_zero_database_updates(monkeypatch): monkeypatch.setattr( composite_queries.ScadaInfoRepository, "get_scadas", AsyncMock( return_value=[{"id": "fengyang-pressure-1", "type": "pressure"}] ), ) monkeypatch.setattr( composite_queries.ScadaRepository, "get_scada_field_by_id_time_range", AsyncMock( return_value={ "fengyang-pressure-1": [ {"time": "2026-06-01T00:00:00+08:00", "value": 26.5} ] } ), ) update_mock = AsyncMock() monkeypatch.setattr( composite_queries.ScadaRepository, "update_scada_field", update_mock, ) monkeypatch.setattr( composite_queries, "clean_pressure_data_df_km", lambda _frame: pd.DataFrame( {"time": [], "fengyang-pressure-1": []} ), ) with pytest.raises(ValueError, match="未产生任何数据库更新"): asyncio.run( composite_queries.CompositeQueries.clean_scada_data( object(), object(), ["fengyang-pressure-1"], datetime(2026, 6, 1, tzinfo=timezone.utc), datetime(2026, 6, 2, tzinfo=timezone.utc), ) ) update_mock.assert_not_awaited() def test_clean_scada_propagates_write_failures(monkeypatch): monkeypatch.setattr( composite_queries.ScadaInfoRepository, "get_scadas", AsyncMock( return_value=[{"id": "fengyang-pressure-1", "type": "pressure"}] ), ) monkeypatch.setattr( composite_queries.ScadaRepository, "get_scada_field_by_id_time_range", AsyncMock( return_value={ "fengyang-pressure-1": [ {"time": "2026-06-01T00:00:00+08:00", "value": 26.5} ] } ), ) monkeypatch.setattr( composite_queries.ScadaRepository, "update_scada_field", AsyncMock(side_effect=RuntimeError("database write failed")), ) monkeypatch.setattr( composite_queries, "clean_pressure_data_df_km", lambda frame: frame, ) with pytest.raises(RuntimeError, match="database write failed"): asyncio.run( composite_queries.CompositeQueries.clean_scada_data( object(), object(), ["fengyang-pressure-1"], datetime(2026, 6, 1, tzinfo=timezone.utc), datetime(2026, 6, 2, tzinfo=timezone.utc), ) ) def test_clean_scada_endpoint_returns_http_400_for_validation_error(monkeypatch): monkeypatch.setattr( composite_endpoint.CompositeQueries, "clean_scada_data", AsyncMock(side_effect=ValueError("当前项目没有可清洗的 SCADA 设备")), ) with pytest.raises(HTTPException) as exc_info: asyncio.run( composite_endpoint.clean_scada_data( device_ids="all", start_time=datetime(2026, 6, 1, tzinfo=timezone.utc), end_time=datetime(2026, 6, 2, tzinfo=timezone.utc), timescale_conn=object(), postgres_conn=object(), ) ) assert exc_info.value.status_code == 400 assert exc_info.value.detail == "当前项目没有可清洗的 SCADA 设备"