from uuid import uuid4 import httpx import pytest from pydantic import ValidationError from app.auth.project_dependencies import ProjectContext 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")