From 36f2df6540fba2a1511d4a45f0976e24ca050d54 Mon Sep 17 00:00:00 2001 From: vardhans07 Date: Sun, 23 Aug 2026 15:58:53 +0530 Subject: [PATCH] fix(caido): normalize string null and sentinels for scopeId and parentId --- strix/tools/proxy/caido_api.py | 24 +++++++++++++ tests/test_caido_api.py | 66 ++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 tests/test_caido_api.py diff --git a/strix/tools/proxy/caido_api.py b/strix/tools/proxy/caido_api.py index af81b5008..32b446fb4 100644 --- a/strix/tools/proxy/caido_api.py +++ b/strix/tools/proxy/caido_api.py @@ -127,6 +127,25 @@ async def close_client() -> None: await client.aclose() +def _normalize_optional_id(value: str | None, *, name: str) -> str | None: + if value is None: + return None + + normalized = value.strip() + if not normalized or normalized.lower() in {"null", "none", "undefined"}: + return None + + try: + numeric_id = int(normalized, 10) + except ValueError as exc: + raise ValueError(f"{name} must be an integer-shaped Caido ID") from exc + + if not -(2**31) <= numeric_id < 2**31: + raise ValueError(f"{name} must fit in a signed 32-bit integer") + + return str(numeric_id) + + async def list_requests_with_client( client: CaidoClient, *, @@ -137,6 +156,8 @@ async def list_requests_with_client( sort_order: SortOrder = "desc", scope_id: str | None = None, ) -> Any: + scope_id = _normalize_optional_id(scope_id, name="scope_id") + builder = client.request.list().first(first) if httpql_filter: builder = builder.filter(httpql_filter) @@ -651,6 +672,9 @@ async def list_sitemap_with_client( pagination, so we fetch all edges for the requested level and slice client-side. """ + scope_id = _normalize_optional_id(scope_id, name="scope_id") + parent_id = _normalize_optional_id(parent_id, name="parent_id") + if parent_id: raw = await client.graphql.query( _SITEMAP_DESCENDANTS_QUERY, diff --git a/tests/test_caido_api.py b/tests/test_caido_api.py new file mode 100644 index 000000000..cfaaa8582 --- /dev/null +++ b/tests/test_caido_api.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from typing import Any +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from strix.tools.proxy.caido_api import ( + _SITEMAP_ROOTS_QUERY, + _normalize_optional_id, + list_requests_with_client, + list_sitemap_with_client, +) + + +@pytest.mark.parametrize("value", ["", " ", "null", "None", "UNDEFINED", "none", "undefined"]) +def test_normalize_optional_id_treats_llm_null_sentinels_as_none(value: str) -> None: + assert _normalize_optional_id(value, name="scope_id") is None + + +def test_normalize_optional_id_accepts_valid_integers() -> None: + assert _normalize_optional_id("123", name="scope_id") == "123" + assert _normalize_optional_id(" 456 ", name="scope_id") == "456" + assert _normalize_optional_id("-1", name="scope_id") == "-1" + + +def test_normalize_optional_id_rejects_non_numeric_value() -> None: + with pytest.raises(ValueError, match="integer-shaped Caido ID"): + _normalize_optional_id("all", name="scope_id") + + +def test_normalize_optional_id_rejects_overflow() -> None: + with pytest.raises(ValueError, match="signed 32-bit integer"): + _normalize_optional_id(str(2**31), name="scope_id") + + with pytest.raises(ValueError, match="signed 32-bit integer"): + _normalize_optional_id(str(-(2**31) - 1), name="scope_id") + + +@pytest.mark.asyncio +async def test_list_requests_with_client_omits_sentinel_scope() -> None: + mock_client = MagicMock() + mock_builder = MagicMock() + mock_builder.first.return_value = mock_builder + mock_builder.descending.return_value = mock_builder + mock_builder.ascending.return_value = mock_builder + mock_builder.execute = AsyncMock(return_value={"data": []}) + mock_client.request.list.return_value = mock_builder + + await list_requests_with_client(mock_client, scope_id="null") + + mock_builder.scope.assert_not_called() + + +@pytest.mark.asyncio +async def test_list_sitemap_with_client_sentinel_parent_queries_roots() -> None: + mock_client = MagicMock() + mock_client.graphql.query = AsyncMock(return_value={"sitemapRootEntries": {"edges": [], "count": {"value": 0}}}) + + res = await list_sitemap_with_client(mock_client, scope_id="null", parent_id="none") + + assert res["success"] is True + mock_client.graphql.query.assert_called_once_with( + _SITEMAP_ROOTS_QUERY, + variables={"scopeId": None}, + )