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()