"""Tests for the HTTP request tool with SSRF protection.""" from __future__ import annotations from unittest.mock import patch import httpx import respx from openjarvis.tools.http_request import HttpRequestTool class TestHttpRequestTool: def test_spec_name_and_category(self): tool = HttpRequestTool() assert tool.spec.name == "http_request" assert tool.spec.category == "network" def test_spec_required_capabilities(self): tool = HttpRequestTool() assert "network:fetch" in tool.spec.required_capabilities def test_spec_parameters_require_url(self): tool = HttpRequestTool() assert "url" in tool.spec.parameters["properties"] assert "url" in tool.spec.parameters["required"] def test_tool_id(self): tool = HttpRequestTool() assert tool.tool_id == "http_request" def test_to_openai_function(self): tool = HttpRequestTool() fn = tool.to_openai_function() assert fn["type"] == "function" assert fn["function"]["name"] == "http_request" assert "url" in fn["function"]["parameters"]["properties"] def test_no_url(self): tool = HttpRequestTool() result = tool.execute() assert result.success is False assert "No URL" in result.content def test_empty_url(self): tool = HttpRequestTool() result = tool.execute(url="") assert result.success is False assert "No URL" in result.content def test_ssrf_blocked_private_ip(self): """Request to private IP should be blocked by SSRF protection.""" tool = HttpRequestTool() with patch("openjarvis.tools.http_request.check_ssrf") as mock_ssrf: mock_ssrf.return_value = "URL resolves to private IP: 192.168.1.1" result = tool.execute(url="http://192.168.1.1/admin") assert result.success is False assert "SSRF protection" in result.content assert "private IP" in result.content def test_ssrf_blocked_metadata_endpoint(self): """Request to cloud metadata endpoint should be blocked.""" tool = HttpRequestTool() with patch("openjarvis.tools.http_request.check_ssrf") as mock_ssrf: mock_ssrf.return_value = ( "Blocked host: 169.254.169.254" " (cloud metadata endpoint)" ) result = tool.execute(url="http://169.254.169.254/latest/meta-data/") assert result.success is False assert "SSRF protection" in result.content assert "metadata" in result.content.lower() or "Blocked host" in result.content @respx.mock def test_successful_get(self): """Successful GET request returns response content and metadata.""" respx.get("https://api.example.com/data").mock( return_value=httpx.Response( 200, text='{"key": "value"}', headers={"content-type": "application/json"}, ) ) tool = HttpRequestTool() with patch("openjarvis.tools.http_request.check_ssrf", return_value=None): result = tool.execute(url="https://api.example.com/data") assert result.success is True assert '"key": "value"' in result.content assert result.metadata["status_code"] == 200 assert "application/json" in result.metadata["content_type"] assert "elapsed_ms" in result.metadata @respx.mock def test_post_with_body(self): """POST request with body sends content correctly.""" respx.post("https://api.example.com/submit").mock( return_value=httpx.Response( 201, text='{"id": 42}', headers={"content-type": "application/json"}, ) ) tool = HttpRequestTool() with patch("openjarvis.tools.http_request.check_ssrf", return_value=None): result = tool.execute( url="https://api.example.com/submit", method="POST", body='{"name": "test"}', headers={"Content-Type": "application/json"}, ) assert result.success is True assert '"id": 42' in result.content assert result.metadata["status_code"] == 201 @respx.mock def test_put_method(self): """PUT request works correctly.""" respx.put("https://api.example.com/resource/1").mock( return_value=httpx.Response(200, text="updated") ) tool = HttpRequestTool() with patch("openjarvis.tools.http_request.check_ssrf", return_value=None): result = tool.execute( url="https://api.example.com/resource/1", method="PUT", body="new data", ) assert result.success is True assert "updated" in result.content @respx.mock def test_delete_method(self): """DELETE request works correctly.""" respx.delete("https://api.example.com/resource/1").mock( return_value=httpx.Response(204, text="") ) tool = HttpRequestTool() with patch("openjarvis.tools.http_request.check_ssrf", return_value=None): result = tool.execute( url="https://api.example.com/resource/1", method="DELETE", ) assert result.success is True @respx.mock def test_head_method(self): """HEAD request works correctly.""" respx.head("https://api.example.com/check").mock( return_value=httpx.Response( 200, text="", headers={"x-custom": "header-value"}, ) ) tool = HttpRequestTool() with patch("openjarvis.tools.http_request.check_ssrf", return_value=None): result = tool.execute( url="https://api.example.com/check", method="HEAD", ) assert result.success is True assert result.metadata["status_code"] == 200 def test_timeout_handling(self): """Timeout should produce a clear error.""" tool = HttpRequestTool() with patch("openjarvis.tools.http_request.check_ssrf", return_value=None): with patch( "openjarvis.tools.http_request.httpx.request", side_effect=httpx.TimeoutException("timed out"), ): result = tool.execute(url="https://slow.example.com", timeout=5) assert result.success is False assert "timed out" in result.content.lower() def test_request_error(self): """Connection error should produce a clear error.""" tool = HttpRequestTool() with patch("openjarvis.tools.http_request.check_ssrf", return_value=None): with patch( "openjarvis.tools.http_request.httpx.request", side_effect=httpx.ConnectError("Connection refused"), ): result = tool.execute(url="https://down.example.com") assert result.success is False assert "Request error" in result.content def test_method_validation(self): """Invalid HTTP method should be rejected.""" tool = HttpRequestTool() result = tool.execute(url="https://example.com", method="TRACE") assert result.success is False assert "Unsupported HTTP method" in result.content assert "TRACE" in result.content def test_method_case_insensitive(self): """Method should be case-insensitive.""" tool = HttpRequestTool() with patch("openjarvis.tools.http_request.check_ssrf", return_value=None): with respx.mock: respx.get("https://api.example.com/data").mock( return_value=httpx.Response(200, text="ok") ) result = tool.execute(url="https://api.example.com/data", method="get") assert result.success is True @respx.mock def test_response_truncation(self): """Response larger than 1 MB should be truncated.""" large_body = "x" * 2_000_000 # 2 MB respx.get("https://api.example.com/large").mock( return_value=httpx.Response(200, text=large_body) ) tool = HttpRequestTool() with patch("openjarvis.tools.http_request.check_ssrf", return_value=None): result = tool.execute(url="https://api.example.com/large") assert result.success is True assert "[Response truncated at 1 MB]" in result.content assert result.metadata["truncated"] is True @respx.mock def test_response_not_truncated_when_small(self): """Response smaller than 1 MB should not be truncated.""" small_body = "hello world" respx.get("https://api.example.com/small").mock( return_value=httpx.Response(200, text=small_body) ) tool = HttpRequestTool() with patch("openjarvis.tools.http_request.check_ssrf", return_value=None): result = tool.execute(url="https://api.example.com/small") assert result.success is True assert result.content == "hello world" assert result.metadata["truncated"] is False @respx.mock def test_metadata_includes_headers(self): """Response metadata should include headers dict.""" respx.get("https://api.example.com/data").mock( return_value=httpx.Response( 200, text="ok", headers={ "content-type": "text/plain", "x-request-id": "abc123", }, ) ) tool = HttpRequestTool() with patch("openjarvis.tools.http_request.check_ssrf", return_value=None): result = tool.execute(url="https://api.example.com/data") assert isinstance(result.metadata["headers"], dict) assert result.metadata["headers"]["x-request-id"] == "abc123" __all__ = ["TestHttpRequestTool"]