import asyncio from datetime import datetime, timedelta, timezone from unittest.mock import AsyncMock import pytest from app.infra.db.timescaledb.repositories.scada import ScadaRepository from app.services.scada_raw import read_raw_page START = datetime(2026, 10, 8, tzinfo=timezone.utc) def reading(minutes, device="P1", value=1): return {"time": START + timedelta(minutes=minutes), "device_id": device, "monitored_value": value, "source_hash": "hash"} def test_pagination_keeps_same_timestamp_devices_and_freezes_clock(monkeypatch): fetch = AsyncMock(side_effect=[[reading(0), reading(0, "P2")], [reading(0, "P2")]]) monkeypatch.setattr(ScadaRepository, "get_raw_page", fetch) async def run(): args = dict(project_id="project", device_ids=["P1", "P2"], start=START, end=START + timedelta(hours=1), limit=1) first = await read_raw_page(None, **args, through=START + timedelta(minutes=15)) second = await read_raw_page(None, **args, cursor=first.next_cursor, through=START + timedelta(minutes=30)) assert first.items[0].device_id == "P1" assert second.items[0].device_id == "P2" assert second.scanned_through == START + timedelta(minutes=15) assert fetch.await_args.args[-1] == (START, "P1") asyncio.run(run()) def test_cursor_cannot_expand_query_or_move_into_future(monkeypatch): fetch = AsyncMock(return_value=[reading(0), reading(5)]) monkeypatch.setattr(ScadaRepository, "get_raw_page", fetch) async def run(): args = dict(project_id="project", device_ids=["P1"], start=START, end=START + timedelta(hours=1), limit=1) first = await read_raw_page(None, **args, through=START + timedelta(minutes=15)) for change in ({"project_id": "other"}, {"device_ids": ["P2"]}, {"through": START}): with pytest.raises(ValueError, match="游标"): await read_raw_page(None, **(args | change), cursor=first.next_cursor) asyncio.run(run()) assert fetch.await_count == 1 def test_raw_response_does_not_expose_corrected_values_and_marks_bad_numbers(monkeypatch): monkeypatch.setattr(ScadaRepository, "get_raw_page", AsyncMock(return_value=[ reading(0, value=float("nan")), reading(5, value=None), reading(10, value=0), ])) page = asyncio.run(read_raw_page(None, project_id="project", device_ids=["P1"], start=START, end=START + timedelta(hours=1))) assert [item.quality for item in page.items] == ["invalid", "missing", "unvalidated"] assert page.items[2].monitored_value == 0 assert "cleaned_value" not in page.model_dump_json() @pytest.mark.parametrize("cursor", ["not-base64", "e30=", "W10="]) def test_malformed_cursor_is_rejected_without_query(monkeypatch, cursor): fetch = AsyncMock() monkeypatch.setattr(ScadaRepository, "get_raw_page", fetch) with pytest.raises(ValueError): asyncio.run(read_raw_page(None, project_id="project", device_ids=["P1"], start=START, end=START + timedelta(hours=1), cursor=cursor)) fetch.assert_not_awaited()