Files
next-tjwater-server/tests/unit/test_operational_data.py
T

159 lines
7.5 KiB
Python

from uuid import uuid4
import httpx
import pytest
from pydantic import ValidationError
from app.auth.project_dependencies import ProjectContext
from app.api.v1.endpoints.operational_tasks import _task_permissions
from app.auth.permissions import SCADA_VIEW, SIMULATION_VIEW
from app.domain.schemas.operational_data import TaskDataQuery
from app.infra.integrations.operational_data import TaskDataError, TaskDataSettings, read_task_data
@pytest.fixture
def anyio_backend():
return "asyncio"
def context():
return ProjectContext(project_id=uuid4(), user_id=uuid4(), project_code="test", project_role="member")
def settings():
return TaskDataSettings(_env_file=None, server_url="http://server.test", peripheral_url="http://sample.test")
@pytest.mark.anyio
async def test_actual_read_has_scoped_headers_and_synthetic_provenance():
ctx = context()
def handler(request):
assert request.method == "GET"
assert request.url.path == "/api/sample/v1/demo-data"
assert not request.url.params
assert request.headers["X-Project-Id"] == str(ctx.project_id)
return httpx.Response(200, json={
"project_id": str(ctx.project_id),
"source_id": "tjwater_v2:peripheral_sample.demo_datasets:water-demo-v1",
"source_revision": "sha256:test",
"points": [{"point_id": "DEMO-P001"}],
"observations": [
{"point_id": "DEMO-P001", "observed_at": "2026-09-23T00:30:00Z", "value": 0.22},
{"point_id": "DEMO-P002", "observed_at": "2026-09-23T00:30:00Z", "value": 1.0},
{"point_id": "DEMO-P001", "observed_at": "2026-09-24T00:00:00Z", "value": 2.0},
],
})
result = await read_task_data(context=ctx, credential="Bearer TEST_ONLY", settings=settings(),
query=TaskDataQuery(source="peripheral_observations", point_id="DEMO-P001",
start="2026-09-23T00:00:00Z", end="2026-09-23T01:00:00Z"),
transport=httpx.MockTransport(handler))
assert result.origin == "synthetic"
assert result.data["items"][0]["value"] == 0.22
assert len(result.data["items"]) == 1
assert result.data["point_id"] == "DEMO-P001"
assert result.data["window"] == {
"start": "2026-09-23T00:00:00+00:00", "end": "2026-09-23T01:00:00+00:00",
}
assert result.source_ref.startswith(
"peripheral_demo_data:DEMO-P001:2026-09-23T00:00:00+00:00:"
"2026-09-23T01:00:00+00:00:sha256:"
)
assert not result.truncated
@pytest.mark.anyio
async def test_historical_observations_keep_monitoring_endpoint():
def handler(request):
assert request.url.path == "/api/sample/v1/monitoring/observations"
assert request.url.params["point_id"] == "100026"
return httpx.Response(200, json={"items": [], "truncated": False, "origin": "historical_replay"})
result = await read_task_data(context=context(), credential="Bearer TEST_ONLY", settings=settings(),
query=TaskDataQuery(source="peripheral_observations", point_id="100026",
start="2026-09-23T00:00:00Z", end="2026-09-23T01:00:00Z"),
transport=httpx.MockTransport(handler))
assert result.origin == "system_data"
assert result.data["origin"] == "historical_replay"
assert result.source_ref.startswith("peripheral_historical_replay:peripheral_observations:sha256:")
@pytest.mark.anyio
@pytest.mark.parametrize("origin,expected", [("historical_replay", "system_data"), ("synthetic", "synthetic")])
async def test_monitoring_point_provenance_follows_source_not_transport(origin, expected):
result = await read_task_data(context=context(), credential="Bearer TEST_ONLY", settings=settings(),
query=TaskDataQuery(source="peripheral_points"),
transport=httpx.MockTransport(lambda request: httpx.Response(200, json={"origin": origin, "items": []})))
assert result.origin == expected
assert result.data["origin"] == origin
assert result.source_ref.startswith(
"peripheral_historical_replay:" if origin == "historical_replay" else "peripheral_points:"
)
@pytest.mark.anyio
async def test_monitoring_data_without_known_provenance_is_not_saved_as_fact():
with pytest.raises(TaskDataError, match="来源"):
await read_task_data(context=context(), credential="Bearer TEST_ONLY", settings=settings(),
query=TaskDataQuery(source="peripheral_points"),
transport=httpx.MockTransport(lambda request: httpx.Response(200, json={"items": []})))
@pytest.mark.anyio
async def test_paged_data_is_marked_incomplete_and_receipt_is_content_bound():
values = [{"items": [1], "total": 2}, {"items": [2], "total": 2}]
def handler(request):
return httpx.Response(200, json=values.pop(0))
kwargs = dict(context=context(), credential="Bearer TEST_ONLY", settings=settings(),
query=TaskDataQuery(source="scada_devices"), transport=httpx.MockTransport(handler))
first, second = await read_task_data(**kwargs), await read_task_data(**kwargs)
assert first.truncated and second.truncated
assert first.source_ref != second.source_ref
@pytest.mark.anyio
@pytest.mark.parametrize("shape", ["root", "items", "list"])
async def test_cross_project_or_failed_query_cannot_become_evidence(shape):
ctx = context()
foreign = {"project_id": str(uuid4()), "value": 0.22}
payload = {"root": foreign, "items": {"project_id": str(ctx.project_id), "items": [foreign]},
"list": [foreign]}[shape]
def handler(request):
return httpx.Response(200, json=payload)
with pytest.raises(TaskDataError, match="其他项目"):
await read_task_data(context=ctx, credential="Bearer TEST_ONLY", settings=settings(),
query=TaskDataQuery(source="workbench_runtime"), transport=httpx.MockTransport(handler))
def test_query_is_a_finite_read_scope_with_bounded_time_and_device_count():
with pytest.raises(ValidationError):
TaskDataQuery(source="http://arbitrary.test/delete")
with pytest.raises(ValidationError):
TaskDataQuery(source="scada_readings", device_ids=["p1"],
start="2026-09-01T00:00:00Z", end="2026-09-23T00:00:00Z")
with pytest.raises(ValidationError):
TaskDataQuery(source="peripheral_observations")
@pytest.mark.anyio
async def test_task_service_uses_approved_delegation_only_during_source_read():
ctx = ProjectContext(project_id=uuid4(), user_id=uuid4(), project_code="test",
project_role="member", system_role="service")
assert SCADA_VIEW not in _task_permissions(ctx)
assert SIMULATION_VIEW not in _task_permissions(ctx)
query = TaskDataQuery(source="scada_devices")
with pytest.raises(TaskDataError, match="委托身份尚未配置"):
await read_task_data(context=ctx, credential="Bearer AGENT", query=query,
delegated=True, settings=settings())
def handler(request):
assert request.headers["Authorization"] == "Bearer REVIEWED_SOURCE"
assert request.headers["X-Project-Id"] == str(ctx.project_id)
return httpx.Response(200, json={"items": [], "project_id": str(ctx.project_id)})
snapshot = await read_task_data(
context=ctx, credential="Bearer AGENT", query=query, delegated=True,
settings=TaskDataSettings(_env_file=None, server_url="http://server.test",
delegation_bearer="REVIEWED_SOURCE"),
transport=httpx.MockTransport(handler),
)
assert snapshot.source == "scada_devices"