Reorganize algorithm packages by business responsibility, move orchestration into services, and keep database access behind pooled repositories. Harden analysis API validation, remove unsafe legacy simulation endpoints, and add regression and architecture boundary coverage. BREAKING CHANGE: legacy algorithm module paths and obsolete simulation endpoints are removed.
139 lines
4.0 KiB
Python
139 lines
4.0 KiB
Python
import asyncio
|
|
from datetime import datetime, timezone
|
|
from unittest.mock import AsyncMock
|
|
from uuid import uuid4
|
|
|
|
from app.api.v1.endpoints import project_data
|
|
from app.services import timeseries_analysis as composite_queries
|
|
|
|
|
|
PROJECT_SCADA = {
|
|
"device_id": "fengyang-pressure-1",
|
|
"device_type": "pressure",
|
|
"node_id": "J1",
|
|
"link_id": None,
|
|
"api_query_id": "query-1",
|
|
"transmission_mode": "realtime",
|
|
"transmission_frequency": None,
|
|
"reliability": 1.0,
|
|
"x": 117.1,
|
|
"y": 32.9,
|
|
}
|
|
START_TIME = datetime(2026, 6, 1, tzinfo=timezone.utc)
|
|
END_TIME = datetime(2026, 6, 2, tzinfo=timezone.utc)
|
|
|
|
|
|
def _patch_project_scadas(monkeypatch):
|
|
monkeypatch.setattr(
|
|
composite_queries.ScadaInfoRepository,
|
|
"get_scadas",
|
|
AsyncMock(return_value=[PROJECT_SCADA.copy()]),
|
|
)
|
|
|
|
|
|
def test_realtime_scada_simulation_uses_current_project_metadata(monkeypatch):
|
|
_patch_project_scadas(monkeypatch)
|
|
query_mock = AsyncMock(
|
|
return_value={"J1": [{"time": START_TIME, "value": 26.5}]}
|
|
)
|
|
monkeypatch.setattr(
|
|
composite_queries.RealtimeRepository,
|
|
"get_node_fields_by_ids_time_range",
|
|
query_mock,
|
|
)
|
|
monkeypatch.setattr(
|
|
composite_queries.RealtimeRepository,
|
|
"get_link_fields_by_ids_time_range",
|
|
AsyncMock(return_value={}),
|
|
)
|
|
|
|
result = asyncio.run(
|
|
composite_queries.TimeseriesAnalysisService.get_scada_associated_realtime_simulation_data(
|
|
object(),
|
|
object(),
|
|
[PROJECT_SCADA["device_id"]],
|
|
START_TIME,
|
|
END_TIME,
|
|
)
|
|
)
|
|
|
|
assert result[PROJECT_SCADA["device_id"]][0]["scada_id"] == PROJECT_SCADA["device_id"]
|
|
assert query_mock.await_count == 1
|
|
assert query_mock.await_args.args[1:] == (
|
|
START_TIME,
|
|
END_TIME,
|
|
["J1"],
|
|
"pressure",
|
|
)
|
|
|
|
|
|
def test_analysis_scada_simulation_uses_current_project_metadata(monkeypatch):
|
|
_patch_project_scadas(monkeypatch)
|
|
async def query_series(_conn, _run_id, element_type, element_ids, *_args):
|
|
if element_type == "node":
|
|
return {"J1": [{"time": START_TIME, "value": 26.5}]}
|
|
return {}
|
|
|
|
query_mock = AsyncMock(side_effect=query_series)
|
|
monkeypatch.setattr(
|
|
composite_queries.AnalysisResultsRepository,
|
|
"get_series_by_ids",
|
|
query_mock,
|
|
)
|
|
|
|
result = asyncio.run(
|
|
composite_queries.TimeseriesAnalysisService.get_scada_associated_analysis_simulation_data(
|
|
object(),
|
|
object(),
|
|
[PROJECT_SCADA["device_id"]],
|
|
START_TIME,
|
|
END_TIME,
|
|
uuid4(),
|
|
)
|
|
)
|
|
|
|
assert result[PROJECT_SCADA["device_id"]][0]["scada_id"] == PROJECT_SCADA["device_id"]
|
|
assert query_mock.await_count == 2
|
|
node_call = next(
|
|
call for call in query_mock.await_args_list if call.args[2] == "node"
|
|
)
|
|
assert node_call.args[3:] == (["J1"], START_TIME, END_TIME, "pressure")
|
|
|
|
|
|
def test_element_scada_query_uses_current_project_metadata(monkeypatch):
|
|
_patch_project_scadas(monkeypatch)
|
|
query_mock = AsyncMock(
|
|
return_value={
|
|
PROJECT_SCADA["device_id"]: [{"time": START_TIME, "value": 26.5}]
|
|
}
|
|
)
|
|
monkeypatch.setattr(
|
|
composite_queries.ScadaRepository,
|
|
"get_scada_field_by_id_time_range",
|
|
query_mock,
|
|
)
|
|
|
|
result = asyncio.run(
|
|
composite_queries.TimeseriesAnalysisService.get_element_associated_scada_data(
|
|
object(),
|
|
object(),
|
|
"J1",
|
|
START_TIME,
|
|
END_TIME,
|
|
)
|
|
)
|
|
|
|
assert result == {"J1": [{"time": START_TIME, "value": 26.5}]}
|
|
|
|
|
|
def test_scada_info_endpoint_uses_current_project_connection(monkeypatch):
|
|
monkeypatch.setattr(
|
|
project_data.ScadaInfoRepository,
|
|
"get_scadas",
|
|
AsyncMock(return_value=[PROJECT_SCADA.copy()]),
|
|
)
|
|
|
|
result = asyncio.run(project_data.get_scada_info_with_connection(object()))
|
|
|
|
assert result == {"success": True, "data": [PROJECT_SCADA], "count": 1}
|