Files
Dorod-Sky/tests/unit/test_aiohttp_helper.py

1111 lines
46 KiB
Python
Raw Permalink Normal View History

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="<html><body>Hello World</body></html>")
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 == "<html><body>Hello World</body></html>"
@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"