feat(scada): add project monitoring replay
Generic Container CI/CD / test-build-publish (push) Successful in 3m39s
Server CI/CD v2 / build-test-publish-and-deploy (push) Successful in 3m39s

This commit is contained in:
2026-11-04 14:27:21 +08:00
parent f25423dec4
commit 26fa5d1909
53 changed files with 5153 additions and 20 deletions
+113
View File
@@ -0,0 +1,113 @@
import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock
from uuid import uuid4
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.api.v1.endpoints import scada_replay
from app.auth.project_dependencies import ProjectContext, get_project_context
@pytest.fixture
def replay_api(monkeypatch):
context = ProjectContext(uuid4(), "demo", uuid4(), "member")
project = SimpleNamespace(settings={"scada_monitoring_interval_seconds": 900, "other": True})
metadata = SimpleNamespace(get_project_by_id=AsyncMock(return_value=project))
repo = SimpleNamespace(available=AsyncMock(return_value=True), current=AsyncMock(return_value=None))
create = AsyncMock(side_effect=RuntimeError("creation mock must be configured"))
monkeypatch.setattr(scada_replay.ScadaReplayService, "create", create)
app = FastAPI()
app.include_router(scada_replay.router)
app.dependency_overrides[get_project_context] = lambda: context
app.dependency_overrides[scada_replay.repository] = lambda: repo
app.dependency_overrides[scada_replay.get_metadata_repository] = lambda: metadata
with TestClient(app) as client:
yield client, project, metadata, context, create
def test_capabilities_reads_frequency_from_current_project_settings(replay_api):
client, project, metadata, context, _ = replay_api
response = client.get("/scada-replays/capabilities")
assert response.status_code == 200
assert response.json()["monitoring_interval_seconds"] == 900
assert response.json()["burst_monitoring_ready"] is False
assert "尚未配置" in response.json()["burst_monitoring_reason"]
metadata.get_project_by_id.assert_awaited_once_with(context.project_id)
project.settings = {}
assert client.get("/scada-replays/capabilities").json()["monitoring_interval_seconds"] is None
def test_capabilities_reports_ready_only_with_five_pressures_and_outlet_flow(replay_api):
client, project, *_ = replay_api
project.settings = {
"scada_monitoring_interval_seconds": 900,
"scada_monitoring": {
"pressure_device_ids": ["P1", "P2", "P3", "P4", "P5"],
"outlet_flow_device_ids": ["F1"],
},
}
response = client.get("/scada-replays/capabilities")
assert response.status_code == 200
assert response.json()["burst_monitoring_ready"] is True
assert response.json()["burst_monitoring_reason"] is None
def test_capabilities_reports_only_missing_storage_not_worker():
repo = SimpleNamespace(available=AsyncMock(return_value=False))
context = ProjectContext(uuid4(), "demo", uuid4(), "member")
result = asyncio.run(scada_replay.replay_capabilities(
repo=repo,
ctx=context,
settings=scada_replay.ProjectSettings(scada_monitoring_interval_seconds=900),
))
assert result.available is False
assert result.reason == "需先部署 SCADA 回放存储结构"
def test_step_command_processes_pending_period_in_same_request(monkeypatch):
calls = []
class Service:
def __init__(self, repo, context, timescale):
calls.append((repo, context, timescale))
async def command(self, replay_id, command):
calls.append((replay_id, command.action))
return SimpleNamespace(step_pending=True)
async def process_pending_step(self, replay_id):
calls.append((replay_id, "processed"))
return "completed-period"
monkeypatch.setattr(scada_replay, "ScadaReplayService", Service)
replay_id = uuid4()
context = ProjectContext(uuid4(), "demo", uuid4(), "member")
command = scada_replay.ScadaReplayCommand(
request_id=uuid4(), expected_revision=1, action="step"
)
result = asyncio.run(scada_replay.replay_command(
replay_id, command, repo="repo", ctx=context, timescale="timescale"
))
assert result == "completed-period"
assert calls[-2:] == [(replay_id, "step"), (replay_id, "processed")]
def test_client_cannot_override_project_frequency_on_creation(replay_api):
client, _, _, _, create = replay_api
response = client.post("/scada-replays", json={
"request_id": str(uuid4()), "start_time": "2026-10-08T00:00:00Z",
"end_time": "2026-10-08T01:00:00Z", "device_ids": ["P1"],
"rules": [{"rule_id": "p", "device_ids": ["P1"], "kind": "bounds", "upper": 10, "unit": "m"}],
"monitoring_interval_seconds": 300,
})
assert response.status_code == 422
create.assert_not_awaited()