import os import tempfile from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import aiohttp import pytest from skyvern.forge.sdk.core.aiohttp_helper import aiohttp_request @pytest.mark.asyncio async def test_aiohttp_request_with_json_data_sends_correct_format() -> None: """Test that json_data parameter sends data as JSON with correct encoding""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api", json_data={"key": "value", "number": 42}, ) # Verify the request was called with correct parameters mock_session.request.assert_called_once() assert captured_args[0] == "POST" # method is first positional argument assert captured_request_kwargs["url"] == "https://example.com/api" # Verify json parameter was used (aiohttp will handle JSON encoding) assert "json" in captured_request_kwargs assert captured_request_kwargs["json"] == {"key": "value", "number": 42} # Verify data parameter was NOT used assert "data" not in captured_request_kwargs @pytest.mark.asyncio async def test_aiohttp_request_with_data_sends_as_form_data() -> None: """Test that data parameter sends data as form-encoded when Content-Type is not application/json""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api", data={"field1": "value1", "field2": "value2"}, ) # Verify data parameter was used (aiohttp will handle form encoding) assert "data" in captured_request_kwargs assert captured_request_kwargs["data"] == {"field1": "value1", "field2": "value2"} # Verify json parameter was NOT used assert "json" not in captured_request_kwargs @pytest.mark.asyncio async def test_aiohttp_request_with_data_and_json_content_type_uses_json() -> None: """Test that data parameter with application/json Content-Type uses JSON encoding""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api", headers={"Content-Type": "application/json"}, data={"name": "test", "value": 42}, ) # Verify json parameter was used (Content-Type is application/json) assert "json" in captured_request_kwargs assert captured_request_kwargs["json"] == {"name": "test", "value": 42} assert "data" not in captured_request_kwargs @pytest.mark.asyncio async def test_aiohttp_request_with_data_and_form_urlencoded_uses_data() -> None: """Test that data parameter with form-urlencoded Content-Type uses data encoding""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api", headers={"Content-Type": "application/x-www-form-urlencoded"}, data={"username": "user", "password": "pass"}, ) # Verify data parameter was used (form-urlencoded Content-Type) assert "data" in captured_request_kwargs assert captured_request_kwargs["data"] == {"username": "user", "password": "pass"} # Verify headers were passed correctly assert captured_request_kwargs["headers"]["Content-Type"] == "application/x-www-form-urlencoded" assert "json" not in captured_request_kwargs @pytest.mark.asyncio async def test_aiohttp_request_with_data_and_json_content_type_case_insensitive() -> None: """Test that Content-Type header check is case-insensitive for application/json""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): # Test lowercase content-type await aiohttp_request( method="POST", url="https://example.com/api", headers={"content-type": "application/json"}, data={"key": "value"}, ) # Verify json parameter was used (content-type is application/json, case-insensitive) assert "json" in captured_request_kwargs assert captured_request_kwargs["json"] == {"key": "value"} assert "data" not in captured_request_kwargs @pytest.mark.asyncio async def test_aiohttp_request_with_data_and_json_content_type_with_charset() -> None: """Test that Content-Type with charset still matches application/json""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api", headers={"Content-Type": "application/json; charset=utf-8"}, data={"key": "value"}, ) # Verify json parameter was used (Content-Type contains application/json) assert "json" in captured_request_kwargs assert captured_request_kwargs["json"] == {"key": "value"} assert "data" not in captured_request_kwargs @pytest.mark.asyncio async def test_aiohttp_request_headers_passed_correctly() -> None: """Test that custom headers are passed correctly to the request""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) custom_headers = { "Authorization": "Bearer token123", "X-Custom-Header": "custom-value", "Content-Type": "application/json", } with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api", headers=custom_headers, json_data={"key": "value"}, ) # Verify headers were passed correctly assert captured_request_kwargs["headers"] == custom_headers @pytest.mark.asyncio async def test_aiohttp_request_cookies_passed_correctly() -> None: """Test that cookies are passed correctly to the request""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) cookies = {"session_id": "abc123", "user_id": "456"} with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="GET", url="https://example.com/api", cookies=cookies, ) # Verify cookies were passed correctly assert captured_request_kwargs["cookies"] == cookies @pytest.mark.asyncio async def test_aiohttp_request_method_uppercased() -> None: """Test that HTTP method is uppercased correctly""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="post", # lowercase url="https://example.com/api", json_data={"key": "value"}, ) # Verify method was uppercased assert captured_args[0] == "POST" @pytest.mark.asyncio async def test_aiohttp_request_get_method_no_body_sent() -> None: """Test that GET requests do not send body data""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"method": "GET"}) mock_response.text = AsyncMock(return_value='{"method": "GET"}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="GET", url="https://example.com/api", data={"should": "not_be_sent"}, ) # Verify no body data was sent for GET request assert "json" not in captured_request_kwargs assert "data" not in captured_request_kwargs assert captured_args[0] == "GET" @pytest.mark.asyncio async def test_aiohttp_request_priority_json_data_over_data() -> None: """Test parameter priority: json_data takes precedence over data""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"result": "json_data_used"}) mock_response.text = AsyncMock(return_value='{"result": "json_data_used"}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api", json_data={"priority": "json"}, data={"should": "not_be_used"}, ) # Verify only json parameter was used assert "json" in captured_request_kwargs assert captured_request_kwargs["json"] == {"priority": "json"} assert "data" not in captured_request_kwargs @pytest.mark.asyncio async def test_aiohttp_request_response_json_decoding() -> None: """Test decoding JSON response (returns dict)""" mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json; charset=utf-8"} mock_response.json = AsyncMock(return_value={"users": [{"id": 1, "name": "Alice"}]}) mock_response.text = AsyncMock(return_value='{"users": [{"id": 1, "name": "Alice"}]}') mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(return_value=mock_response) mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): status, headers, body = await aiohttp_request( method="GET", url="https://example.com/api/users", ) assert status == 200 assert isinstance(body, dict) assert "users" in body assert len(body["users"]) == 1 assert body["users"][0]["id"] == 1 assert body["users"][0]["name"] == "Alice" @pytest.mark.asyncio async def test_aiohttp_request_response_text_decoding() -> None: """Test decoding non-JSON response (returns str)""" mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "text/html; charset=utf-8"} # Simulate JSON parsing failure, return text mock_response.json = AsyncMock( side_effect=aiohttp.ContentTypeError( request_info=MagicMock(), history=(), message="Attempt to decode JSON with unexpected mimetype: text/html", ) ) mock_response.text = AsyncMock(return_value="Hello World") mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(return_value=mock_response) mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): status, headers, body = await aiohttp_request( method="GET", url="https://example.com/page", ) assert status == 200 assert isinstance(body, str) assert body == "Hello World" @pytest.mark.asyncio async def test_aiohttp_request_response_headers_dict() -> None: """Test that response headers are correctly converted to dict""" mock_response = AsyncMock() mock_response.status = 201 mock_response.headers = { "Content-Type": "application/json", "X-Custom-Header": "custom-value", "Set-Cookie": "session=abc123", } mock_response.json = AsyncMock(return_value={"created": True}) mock_response.text = AsyncMock(return_value='{"created": true}') mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(return_value=mock_response) mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): status, headers, body = await aiohttp_request( method="POST", url="https://example.com/api", ) assert status == 201 assert isinstance(headers, dict) assert headers["Content-Type"] == "application/json" assert headers["X-Custom-Header"] == "custom-value" assert headers["Set-Cookie"] == "session=abc123" @pytest.mark.asyncio async def test_aiohttp_request_follow_redirects() -> None: """Test that follow_redirects parameter is passed correctly""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="GET", url="https://example.com/api", follow_redirects=False, ) # Verify allow_redirects parameter was set correctly assert captured_request_kwargs["allow_redirects"] is False @pytest.mark.asyncio async def test_aiohttp_request_proxy_passed_correctly() -> None: """Test that proxy parameter is passed correctly""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) proxy_url = "http://proxy.example.com:8080" with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="GET", url="https://example.com/api", proxy=proxy_url, ) # Verify proxy was passed correctly assert captured_request_kwargs["proxy"] == proxy_url @pytest.mark.asyncio async def test_aiohttp_request_with_files_uses_multipart() -> None: """Test that files parameter sends data as multipart/form-data""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} # Create a temporary file for testing with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as tmp_file: tmp_file.write("test file content") tmp_file_path = tmp_file.name try: mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api/upload", files={"file": tmp_file_path}, ) # Verify data parameter was used (should be FormData for multipart) assert "data" in captured_request_kwargs # FormData object should be passed assert isinstance(captured_request_kwargs["data"], aiohttp.FormData) # Verify Content-Type header was removed (aiohttp will set it for multipart) assert "Content-Type" not in captured_request_kwargs["headers"] assert "content-type" not in captured_request_kwargs["headers"] finally: # Clean up temporary file if os.path.exists(tmp_file_path): os.unlink(tmp_file_path) @pytest.mark.asyncio async def test_aiohttp_request_with_files_and_data_combines_in_multipart() -> None: """Test that files and data can be combined in multipart/form-data""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} # Create a temporary file for testing with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as tmp_file: tmp_file.write("test file content") tmp_file_path = tmp_file.name try: mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api/upload", data={"field1": "value1", "field2": "value2"}, files={"file": tmp_file_path}, ) # Verify data parameter was used (should be FormData for multipart) assert "data" in captured_request_kwargs assert isinstance(captured_request_kwargs["data"], aiohttp.FormData) # Verify Content-Type header was removed assert "Content-Type" not in captured_request_kwargs["headers"] finally: # Clean up temporary file if os.path.exists(tmp_file_path): os.unlink(tmp_file_path) @pytest.mark.asyncio async def test_aiohttp_request_with_files_raises_file_not_found() -> None: """Test that files parameter raises FileNotFoundError for non-existent files""" mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(return_value=mock_response) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): with pytest.raises(FileNotFoundError, match="File not found"): await aiohttp_request( method="POST", url="https://example.com/api/upload", files={"file": "/nonexistent/path/to/file.txt"}, ) @pytest.mark.asyncio async def test_aiohttp_request_with_multiple_files() -> None: """Test that multiple files can be uploaded in a single request""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} # Create temporary files for testing with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as tmp_file1: tmp_file1.write("file 1 content") tmp_file_path1 = tmp_file1.name with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".pdf") as tmp_file2: tmp_file2.write("file 2 content") tmp_file_path2 = tmp_file2.name try: mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api/upload", files={"document": tmp_file_path1, "attachment": tmp_file_path2}, ) # Verify data parameter was used (should be FormData for multipart) assert "data" in captured_request_kwargs assert isinstance(captured_request_kwargs["data"], aiohttp.FormData) finally: # Clean up temporary files for file_path in [tmp_file_path1, tmp_file_path2]: if os.path.exists(file_path): os.unlink(file_path) @pytest.mark.asyncio async def test_aiohttp_request_files_priority_over_data() -> None: """Test that files parameter takes priority and uses multipart even if data is provided""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} # Create a temporary file for testing with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as tmp_file: tmp_file.write("test file content") tmp_file_path = tmp_file.name try: mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api/upload", headers={"Content-Type": "application/json"}, data={"key": "value"}, files={"file": tmp_file_path}, ) # Verify data parameter was used (should be FormData for multipart) assert "data" in captured_request_kwargs assert isinstance(captured_request_kwargs["data"], aiohttp.FormData) # Verify json parameter was NOT used (files take priority) assert "json" not in captured_request_kwargs # Verify Content-Type header was removed (aiohttp will set it for multipart) assert "Content-Type" not in captured_request_kwargs["headers"] finally: # Clean up temporary file if os.path.exists(tmp_file_path): os.unlink(tmp_file_path) @pytest.mark.asyncio async def test_aiohttp_request_data_dict_with_non_json_content_type_uses_data() -> None: """Test that data (dict) with non-JSON Content-Type uses form encoding, not JSON""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api", headers={"Content-Type": "application/xml"}, data={"key": "value", "number": 42}, ) # Verify data parameter was used (not json), even though data is a dict # This ensures form encoding is used, which might cause server decoding issues # if server expects JSON but receives form-encoded data assert "data" in captured_request_kwargs assert captured_request_kwargs["data"] == {"key": "value", "number": 42} assert "json" not in captured_request_kwargs assert captured_request_kwargs["headers"]["Content-Type"] == "application/xml" @pytest.mark.asyncio async def test_aiohttp_request_data_string_with_json_content_type_uses_json() -> None: """Test that data (string) with application/json Content-Type uses json parameter Note: This might cause issues if the string is not valid JSON, as aiohttp's json parameter expects serializable objects, not strings. """ captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api", headers={"Content-Type": "application/json"}, data='{"raw": "json_string"}', # String, not dict ) # Verify json parameter was used (Content-Type is application/json) # This might cause issues because aiohttp's json parameter expects # serializable objects, not strings assert "json" in captured_request_kwargs assert captured_request_kwargs["json"] == '{"raw": "json_string"}' assert "data" not in captured_request_kwargs @pytest.mark.asyncio async def test_aiohttp_request_data_string_with_form_content_type_uses_data() -> None: """Test that data (string) with form-urlencoded Content-Type uses data parameter This scenario might cause server decoding issues if the server expects form-encoded key-value pairs but receives a raw string. """ captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api", headers={"Content-Type": "application/x-www-form-urlencoded"}, data='{"raw": "json_string"}', # String, not dict ) # Verify data parameter was used (form-urlencoded Content-Type) # This might cause server decoding issues if server expects form-encoded # key-value pairs but receives a raw JSON string assert "data" in captured_request_kwargs assert captured_request_kwargs["data"] == '{"raw": "json_string"}' assert "json" not in captured_request_kwargs assert captured_request_kwargs["headers"]["Content-Type"] == "application/x-www-form-urlencoded" @pytest.mark.asyncio async def test_aiohttp_request_data_dict_with_text_content_type_uses_data() -> None: """Test that data (dict) with text/plain Content-Type uses form encoding This scenario might cause server decoding issues if the server expects plain text but receives form-encoded data. """ captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api", headers={"Content-Type": "text/plain"}, data={"key": "value", "number": 42}, ) # Verify data parameter was used (not json), even though data is a dict # This might cause server decoding issues if server expects plain text # but receives form-encoded data assert "data" in captured_request_kwargs assert captured_request_kwargs["data"] == {"key": "value", "number": 42} assert "json" not in captured_request_kwargs assert captured_request_kwargs["headers"]["Content-Type"] == "text/plain" @pytest.mark.asyncio async def test_aiohttp_request_data_list_with_json_content_type_uses_json() -> None: """Test that data (list) with application/json Content-Type uses json parameter""" captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api", headers={"Content-Type": "application/json"}, data=[{"item": 1}, {"item": 2}], # List, not dict ) # Verify json parameter was used (Content-Type is application/json) assert "json" in captured_request_kwargs assert captured_request_kwargs["json"] == [{"item": 1}, {"item": 2}] assert "data" not in captured_request_kwargs @pytest.mark.asyncio async def test_aiohttp_request_data_list_with_form_content_type_uses_data() -> None: """Test that data (list) with form-urlencoded Content-Type uses data parameter This scenario might cause server decoding issues if the server expects form-encoded key-value pairs but receives a list. """ captured_args: list[Any] = [] captured_request_kwargs: dict[str, Any] = {} mock_response = AsyncMock() mock_response.status = 200 mock_response.headers = {"Content-Type": "application/json"} mock_response.json = AsyncMock(return_value={"success": True}) mock_response.text = AsyncMock(return_value='{"success": true}') mock_response.__aenter__ = AsyncMock(return_value=mock_response) mock_response.__aexit__ = AsyncMock(return_value=None) # type: ignore[no-untyped-def] def capture_request(*args: Any, **kwargs: Any) -> AsyncMock: captured_args.extend(args) captured_request_kwargs.update(kwargs) return mock_response mock_session = MagicMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(side_effect=capture_request) with patch("skyvern.forge.sdk.core.aiohttp_helper.aiohttp.ClientSession", return_value=mock_session): await aiohttp_request( method="POST", url="https://example.com/api", headers={"Content-Type": "application/x-www-form-urlencoded"}, data=[{"item": 1}, {"item": 2}], # List, not dict ) # Verify data parameter was used (form-urlencoded Content-Type) # This might cause server decoding issues if server expects form-encoded # key-value pairs but receives a list assert "data" in captured_request_kwargs assert captured_request_kwargs["data"] == [{"item": 1}, {"item": 2}] assert "json" not in captured_request_kwargs assert captured_request_kwargs["headers"]["Content-Type"] == "application/x-www-form-urlencoded"