Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions strix/tools/proxy/caido_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
*,
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
66 changes: 66 additions & 0 deletions tests/test_caido_api.py
Original file line number Diff line number Diff line change
@@ -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:
Comment thread
vardhans07 marked this conversation as resolved.
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},
)