Reorganize WNDB by responsibility and remove legacy scheme endpoints.\n\nRoute analysis and time-series access through project pools, preserve transactional realtime replacement, and refresh GIS materialized views after writes.\n\nAdd database architecture documentation, live pooling coverage, API contract updates, and executable container verification.\n\nBREAKING CHANGE: legacy scheme APIs and flat app.native.wndb module imports are removed.
129 lines
3.6 KiB
Python
129 lines
3.6 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,
|
|
"pressure_sensor_placement_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["username"] == "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"
|
|
)
|