fix(caido): normalize string null and sentinels for scopeId and parentId

This commit is contained in:
vardhans07 2026-08-23 15:58:53 +05:30
parent 1c499c5b2d
commit 36f2df6540
2 changed files with 90 additions and 0 deletions

View file

@ -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,

66
tests/test_caido_api.py Normal file
View file

@ -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},
)