from pathlib import Path from datetime import datetime, timezone from fastapi.testclient import TestClient from tests.conftest import build_test_app, install_stub, load_module_from_path def _load_simulation_module(monkeypatch): install_stub(monkeypatch, "app.services", package=True) def parse_aware_time(value, field_name="datetime"): dt = datetime.fromisoformat(str(value).replace("Z", "+00:00")) if dt.tzinfo is None: raise ValueError(f"{field_name} is missing timezone information.") return dt def parse_utc_time(value, field_name="datetime"): return parse_aware_time(value, field_name=field_name).astimezone( timezone.utc ) install_stub( monkeypatch, "app.services.time_api", { "parse_aware_time": parse_aware_time, "parse_utc_time": parse_utc_time, }, ) install_stub( monkeypatch, "app.services.simulation", { "run_simulation": lambda **kwargs: None, "query_corresponding_element_id_and_query_id": lambda name: None, "query_corresponding_pattern_id_and_query_id": lambda name: None, "query_non_realtime_region": lambda name: [], "get_source_outflow_region_id": lambda name, region_result: {}, "query_realtime_region_pipe_flow_and_demand_id": lambda name, region_result: {}, "query_pipe_flow_region_patterns": lambda name: {}, "query_non_realtime_region_patterns": lambda name, region_result: {}, "get_realtime_region_patterns": lambda name, source_outflow_region_id, realtime_region_pipe_flow_and_demand_id: ({}, {}), }, ) install_stub(monkeypatch, "app.services.globals", {}) install_stub( monkeypatch, "app.services.tjnetwork", { "run_project": lambda network: "report", "run_project_return_dict": lambda network: {"output": {}, "report": "ok"}, "run_inp": lambda network: "inp-report", "dump_output": lambda output: f"dump::{output}", }, ) install_stub(monkeypatch, "app.algorithms", package=True) install_stub(monkeypatch, "app.algorithms.simulation", package=True) install_stub( monkeypatch, "app.algorithms.simulation.scenarios", { "burst_analysis": lambda *args, **kwargs: "burst", "valve_close_analysis": lambda *args, **kwargs: "valve", "flushing_analysis": lambda *args, **kwargs: "flush", "contaminant_simulation": lambda *args, **kwargs: "contaminant", "age_analysis": lambda *args, **kwargs: "age", "pressure_regulation": lambda *args, **kwargs: "pressure", }, ) install_stub( monkeypatch, "app.algorithms.sensor", { "pressure_sensor_placement_sensitivity": lambda *args, **kwargs: [], "pressure_sensor_placement_kmeans": lambda *args, **kwargs: [], }, ) install_stub( monkeypatch, "app.services.network_import", {"network_update": lambda *args, **kwargs: "updated"}, ) install_stub( monkeypatch, "app.services.simulation_ops", { "project_management": lambda *args, **kwargs: "managed", "scheduling_simulation": lambda *args, **kwargs: "scheduled", "daily_scheduling_simulation": lambda *args, **kwargs: "daily", }, ) install_stub( monkeypatch, "app.services.valve_isolation", {"analyze_valve_isolation": lambda *args, **kwargs: {}}, ) return load_module_from_path( "tests_simulation_endpoints_module", "app/api/v1/endpoints/simulation.py", ) def test_run_project_endpoint_returns_plain_text(monkeypatch): module = _load_simulation_module(monkeypatch) monkeypatch.setattr(module, "run_project", lambda network: f"report::{network}") client = TestClient(build_test_app(module.router, "/api/v1")) response = client.get("/api/v1/runproject/", params={"network": "demo"}) assert response.status_code == 200 assert response.text == "report::demo" def test_scheduling_analysis_maps_request_body(monkeypatch): module = _load_simulation_module(monkeypatch) captured = {} def fake_schedule(network, start_time, pump_control, tank_id, water_plant_output_id, time_delta): captured["args"] = ( network, start_time, pump_control, tank_id, water_plant_output_id, time_delta, ) return "scheduled" monkeypatch.setattr(module, "scheduling_simulation", fake_schedule) client = TestClient(build_test_app(module.router, "/api/v1")) response = client.post( "/api/v1/scheduling_analysis/", json={ "network": "demo", "start_time": "2025-01-01T08:00:00+08:00", "pump_control": {"P1": [1, 0, 1]}, "tank_id": "T1", "water_plant_output_id": "R1", }, ) assert response.status_code == 200 assert response.json() == "scheduled" assert captured["args"] == ( "demo", "2025-01-01T08:00:00+08:00", {"P1": [1, 0, 1]}, "T1", "R1", 300, ) def test_project_management_maps_named_arguments(monkeypatch): module = _load_simulation_module(monkeypatch) captured = {} def fake_project_management(**kwargs): captured.update(kwargs) return "managed" monkeypatch.setattr(module, "project_management", fake_project_management) client = TestClient(build_test_app(module.router, "/api/v1")) response = client.post( "/api/v1/project_management/", json={ "network": "demo", "start_time": "2025-01-01T08:00:00+08:00", "pump_control": {"P1": [1]}, "tank_init_level": {"T1": 10.0}, "region_demand": {"R1": 20.0}, }, ) assert response.status_code == 200 assert response.json() == "managed" assert captured == { "prj_name": "demo", "start_datetime": "2025-01-01T08:00:00+08:00", "pump_control": {"P1": [1]}, "tank_initial_level_control": {"T1": 10.0}, "region_demand_control": {"R1": 20.0}, } def test_network_update_surfaces_service_error(monkeypatch, tmp_path): module = _load_simulation_module(monkeypatch) monkeypatch.chdir(tmp_path) def boom(_path): raise RuntimeError("write failed") monkeypatch.setattr(module, "network_update", boom) client = TestClient(build_test_app(module.router, "/api/v1")) response = client.post( "/api/v1/network_update/", files={"file": ("update.txt", b"payload")}, ) assert response.status_code == 500 assert "数据库操作失败: write failed" in response.json()["detail"] assert list(Path(tmp_path).glob("network_update_*")) def test_run_simulation_manually_by_date_uses_utc_aware_timestamps(monkeypatch): module = _load_simulation_module(monkeypatch) captured_calls = [] monkeypatch.setattr( module.simulation, "run_simulation", lambda **kwargs: captured_calls.append(kwargs), ) module.run_simulation_manually_by_date( "demo", datetime(2025, 1, 1, 19, 4, 5, tzinfo=timezone.utc), 30, ) assert [call["modify_pattern_start_time"] for call in captured_calls] == [ "2025-01-01T19:04:05+00:00", "2025-01-01T19:19:05+00:00", ] def test_runsimulationmanuallybydate_endpoint_accepts_timezone_aware_start_time(monkeypatch): module = _load_simulation_module(monkeypatch) captured = {} def fake_run(network_name, start_time, duration): captured["network_name"] = network_name captured["start_time"] = start_time captured["duration"] = duration monkeypatch.setattr(module, "run_simulation_manually_by_date", fake_run) client = TestClient(build_test_app(module.router, "/api/v1")) response = client.post( "/api/v1/runsimulationmanuallybydate/", json={ "name": "demo", "start_time": "2025-01-02T03:04:05+08:00", "duration": 30, }, ) assert response.status_code == 200 assert response.json() == {"status": "success"} assert captured["network_name"] == "demo" assert captured["duration"] == 30 assert captured["start_time"].isoformat() == "2025-01-01T19:04:05+00:00" def test_runsimulationmanuallybydate_endpoint_rejects_naive_start_time(monkeypatch): module = _load_simulation_module(monkeypatch) client = TestClient(build_test_app(module.router, "/api/v1")) response = client.post( "/api/v1/runsimulationmanuallybydate/", json={ "name": "demo", "start_time": "2025-01-02T03:04:05", "duration": 30, }, ) assert response.status_code == 422 def test_valve_close_endpoint_passes_scheme_name(monkeypatch): module = _load_simulation_module(monkeypatch) captured = {} def fake_valve_close_analysis(**kwargs): captured.update(kwargs) return "ok" monkeypatch.setattr(module, "valve_close_analysis", fake_valve_close_analysis) client = TestClient(build_test_app(module.router, "/api/v1")) response = client.get( "/api/v1/valve_close_analysis/", params={ "network": "demo", "start_time": "2025-01-02T03:04:05+08:00", "valves": ["V1", "V2"], "duration": 900, "scheme_name": "valve_case_01", }, ) assert response.status_code == 200 assert response.text == "ok" assert captured == { "name": "demo", "modify_pattern_start_time": "2025-01-02T03:04:05+08:00", "modify_total_duration": 900, "modify_valve_opening": {"V1": 0.0, "V2": 0.0}, "scheme_name": "valve_case_01", } def test_flushing_endpoint_passes_required_scheme_name(monkeypatch): module = _load_simulation_module(monkeypatch) captured = {} def fake_flushing_analysis(**kwargs): captured.update(kwargs) return "ok" monkeypatch.setattr(module, "flushing_analysis", fake_flushing_analysis) client = TestClient(build_test_app(module.router, "/api/v1")) response = client.get( "/api/v1/flushing_analysis/", params={ "network": "demo", "start_time": "2025-01-02T03:04:05+08:00", "valves": ["V1"], "valves_k": [0.5], "drainage_node_ID": "N1", "flush_flow": 100.0, "duration": 900, "scheme_name": "flush_case_01", }, ) assert response.status_code == 200 assert response.text == "ok" assert captured == { "name": "demo", "modify_pattern_start_time": "2025-01-02T03:04:05+08:00", "modify_total_duration": 900, "modify_valve_opening": {"V1": 0.5}, "drainage_node_ID": "N1", "flushing_flow": 100.0, "scheme_name": "flush_case_01", } def test_contaminant_endpoint_requires_scheme_name(monkeypatch): module = _load_simulation_module(monkeypatch) client = TestClient(build_test_app(module.router, "/api/v1")) response = client.get( "/api/v1/contaminant_simulation/", params={ "network": "demo", "start_time": "2025-01-02T03:04:05+08:00", "source": "N1", "concentration": 10.0, "duration": 900, }, ) assert response.status_code == 422