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.
129 lines
3.7 KiB
Python
129 lines
3.7 KiB
Python
from datetime import datetime, timezone
|
|
from io import BytesIO
|
|
from types import SimpleNamespace
|
|
from uuid import uuid4
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.api.v1.endpoints import sensor_placement as endpoint
|
|
from tests.conftest import build_test_app
|
|
|
|
|
|
RUN_ID = uuid4()
|
|
|
|
|
|
def _run(**overrides):
|
|
value = {
|
|
"run_id": RUN_ID,
|
|
"name": "北区测压点",
|
|
"sensor_count": 1,
|
|
"min_diameter": 300,
|
|
"created_by": "alice",
|
|
"created_at": datetime(2026, 8, 24, tzinfo=timezone.utc),
|
|
"status": "completed",
|
|
"sensor_locations": ["J1"],
|
|
"sensor_points": [
|
|
{
|
|
"node_id": "J1",
|
|
"max_pipe_diameter": 400.0,
|
|
"project_x": 1.0,
|
|
"project_y": 2.0,
|
|
"map_x": 3.0,
|
|
"map_y": 4.0,
|
|
"longitude": 121.0,
|
|
"latitude": 31.0,
|
|
"elevation": 5.0,
|
|
}
|
|
],
|
|
}
|
|
value.update(overrides)
|
|
return value
|
|
|
|
|
|
def _client(project_role="member", username="alice", role="user"):
|
|
app = build_test_app(endpoint.router, "/api/v1")
|
|
app.dependency_overrides[endpoint.get_project_context] = lambda: SimpleNamespace(
|
|
project_code="tjwater", project_role=project_role
|
|
)
|
|
app.dependency_overrides[endpoint.get_current_metadata_user] = lambda: SimpleNamespace(
|
|
username=username, role=role, is_superuser=False
|
|
)
|
|
return TestClient(app)
|
|
|
|
|
|
def test_optimize_returns_analysis_run(monkeypatch):
|
|
captured = {}
|
|
monkeypatch.setattr(
|
|
endpoint,
|
|
"optimize_sensor_placement_by_kmeans",
|
|
lambda **kwargs: captured.update(kwargs) or {"run_id": RUN_ID},
|
|
)
|
|
monkeypatch.setattr(endpoint, "get_sensor_placement_run", lambda *_: _run())
|
|
|
|
response = _client().post(
|
|
"/api/v1/sensor-placement-runs",
|
|
json={
|
|
"network": "tjwater",
|
|
"run_name": "北区测压点",
|
|
"sensor_type": "pressure",
|
|
"method": "kmeans",
|
|
"sensor_count": 1,
|
|
"min_diameter": 300,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["run_id"] == str(RUN_ID)
|
|
assert captured["created_by"] == "alice"
|
|
|
|
|
|
def test_optimize_rejects_project_mismatch(monkeypatch):
|
|
response = _client().post(
|
|
"/api/v1/sensor-placement-runs",
|
|
json={
|
|
"network": "other",
|
|
"run_name": "越权运行",
|
|
"sensor_type": "pressure",
|
|
"method": "kmeans",
|
|
"sensor_count": 1,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_viewer_cannot_update_run(monkeypatch):
|
|
monkeypatch.setattr(endpoint, "get_sensor_placement_run", lambda *_: _run())
|
|
|
|
response = _client(project_role="viewer").put(
|
|
f"/api/v1/sensor-placement-runs/{RUN_ID}",
|
|
params={"network": "tjwater"},
|
|
json={
|
|
"expected_sensor_locations": ["J1"],
|
|
"sensor_locations": ["J2"],
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_export_returns_xlsx(monkeypatch):
|
|
monkeypatch.setattr(endpoint, "get_sensor_placement_run", lambda *_: _run())
|
|
monkeypatch.setattr(
|
|
endpoint,
|
|
"build_sensor_placement_workbook",
|
|
lambda **kwargs: BytesIO(b"xlsx"),
|
|
)
|
|
|
|
response = _client().post(
|
|
f"/api/v1/sensor-placement-runs/{RUN_ID}/exports/excel",
|
|
params={"network": "tjwater"},
|
|
json={"sensor_locations": ["J1"], "adjustment_status": {}},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.content == b"xlsx"
|
|
assert response.headers["content-type"].startswith(
|
|
"application/vnd.openxmlformats-officedocument"
|
|
)
|