From 66c76619964c13353c7e8bd3d53a6c2d21dc9868 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 13 Jul 2026 05:59:16 +0000 Subject: [PATCH] test(security): cover validate_url usage in arxiv tool and PyPI polling Add behavior tests that assert URL validation runs before network requests and that unsafe URLs never reach requests.get. Also fix indentation in _wait_for_pypi after the requests migration. Co-authored-by: Rip&Tear --- .../tests/tools/arxiv_paper_tool_test.py | 48 +++++++++++++++++++ lib/devtools/src/crewai_devtools/cli.py | 8 ++-- lib/devtools/tests/test_wait_for_pypi.py | 37 ++++++++++++++ 3 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 lib/devtools/tests/test_wait_for_pypi.py diff --git a/lib/crewai-tools/tests/tools/arxiv_paper_tool_test.py b/lib/crewai-tools/tests/tools/arxiv_paper_tool_test.py index 4cd660895..d7ffcadb9 100644 --- a/lib/crewai-tools/tests/tools/arxiv_paper_tool_test.py +++ b/lib/crewai-tools/tests/tools/arxiv_paper_tool_test.py @@ -136,3 +136,51 @@ def test_run_with_max_results(mock_fetch, tool): result = tool._run(search_query="test", max_results=100) assert result.count("Title:") == 100 + + +@patch("crewai_tools.tools.arxiv_paper_tool.arxiv_paper_tool.requests.get") +@patch( + "crewai_tools.tools.arxiv_paper_tool.arxiv_paper_tool.validate_url", + return_value="https://validated.example/api/query?search_query=transformer&start=0&max_results=1", +) +def test_fetch_arxiv_data_validates_url_before_request(mock_validate_url, mock_get, tool): + mock_response = MagicMock() + mock_response.text = mock_arxiv_response() + mock_get.return_value = mock_response + + tool.fetch_arxiv_data("transformer", 1) + + mock_validate_url.assert_called_once() + validated_url = mock_validate_url.call_args.args[0] + assert validated_url.startswith(ArxivPaperTool.BASE_API_URL) + assert "search_query=transformer" in validated_url + assert "max_results=1" in validated_url + mock_get.assert_called_once_with( + "https://validated.example/api/query?search_query=transformer&start=0&max_results=1", + timeout=ArxivPaperTool.REQUEST_TIMEOUT, + ) + + +@patch("crewai_tools.tools.arxiv_paper_tool.arxiv_paper_tool.requests.get") +@patch( + "crewai_tools.tools.arxiv_paper_tool.arxiv_paper_tool.validate_url", + side_effect=ValueError("URL resolves to private/reserved IP"), +) +def test_fetch_arxiv_data_rejects_unsafe_url(mock_validate_url, mock_get, tool): + with pytest.raises(ValueError, match="private/reserved IP"): + tool.fetch_arxiv_data("transformer", 1) + + mock_validate_url.assert_called_once() + mock_get.assert_not_called() + + +@patch("crewai_tools.tools.arxiv_paper_tool.arxiv_paper_tool.requests.get") +@patch( + "crewai_tools.tools.arxiv_paper_tool.arxiv_paper_tool.validate_url", + side_effect=ValueError("URL resolves to private/reserved IP"), +) +def test_run_returns_error_when_url_validation_fails(mock_validate_url, mock_get, tool): + result = tool._run("transformer", 1) + + assert "Failed to fetch or download Arxiv papers" in result + mock_get.assert_not_called() diff --git a/lib/devtools/src/crewai_devtools/cli.py b/lib/devtools/src/crewai_devtools/cli.py index 46786f095..a32326333 100644 --- a/lib/devtools/src/crewai_devtools/cli.py +++ b/lib/devtools/src/crewai_devtools/cli.py @@ -1557,10 +1557,10 @@ def _wait_for_pypi(package: str, version: str) -> None: try: response = requests.get(url, timeout=30) if response.status_code == 200: - console.print( - f"[green]✓[/green] {package}=={version} is available on PyPI" - ) - return + console.print( + f"[green]✓[/green] {package}=={version} is available on PyPI" + ) + return except Exception: # noqa: S110 pass time.sleep(_PYPI_POLL_INTERVAL) diff --git a/lib/devtools/tests/test_wait_for_pypi.py b/lib/devtools/tests/test_wait_for_pypi.py new file mode 100644 index 000000000..c4d13d998 --- /dev/null +++ b/lib/devtools/tests/test_wait_for_pypi.py @@ -0,0 +1,37 @@ +"""Tests for PyPI polling URL validation in the release CLI.""" + +from unittest.mock import MagicMock, patch + +from crewai_devtools.cli import _wait_for_pypi +import pytest + + +@patch("crewai_devtools.cli.requests.get") +@patch( + "crewai_devtools.cli.validate_url", + return_value="https://pypi.org/pypi/crewai/1.0.0/json", +) +def test_wait_for_pypi_validates_url_before_request(mock_validate_url, mock_get): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + _wait_for_pypi("crewai", "1.0.0") + + mock_validate_url.assert_called_once_with("https://pypi.org/pypi/crewai/1.0.0/json") + mock_get.assert_called_once_with( + "https://pypi.org/pypi/crewai/1.0.0/json", timeout=30 + ) + + +@patch("crewai_devtools.cli.requests.get") +@patch( + "crewai_devtools.cli.validate_url", + side_effect=ValueError("URL resolves to private/reserved IP"), +) +def test_wait_for_pypi_rejects_unsafe_url(mock_validate_url, mock_get): + with pytest.raises(ValueError, match="private/reserved IP"): + _wait_for_pypi("crewai", "1.0.0") + + mock_validate_url.assert_called_once() + mock_get.assert_not_called()