From 91c49091e87ee972aaa7bb3a19fcaaea7b6e1b10 Mon Sep 17 00:00:00 2001 From: Didier Durand Date: Sun, 14 Dec 2025 08:45:29 +0100 Subject: [PATCH 1/9] test: adding 17 tests for client/card_resolver.py --- tests/client/test_card_resolver.py | 362 +++++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 tests/client/test_card_resolver.py diff --git a/tests/client/test_card_resolver.py b/tests/client/test_card_resolver.py new file mode 100644 index 00000000..a2f82f13 --- /dev/null +++ b/tests/client/test_card_resolver.py @@ -0,0 +1,362 @@ +import json +import logging + +from unittest.mock import AsyncMock, Mock, patch + +import httpx +import pytest + +from a2a.client import A2ACardResolver, A2AClientHTTPError, A2AClientJSONError +from a2a.types import AgentCard +from a2a.utils import AGENT_CARD_WELL_KNOWN_PATH + + +@pytest.fixture +def mock_httpx_client(): + """Fixture providing a mocked async httpx client.""" + return AsyncMock(spec=httpx.AsyncClient) + + +@pytest.fixture +def base_url(): + """Fixture providing a test base URL.""" + return 'https://example.com' + + +@pytest.fixture +def resolver(mock_httpx_client, base_url): + """Fixture providing an A2ACardResolver instance.""" + return A2ACardResolver( + httpx_client=mock_httpx_client, + base_url=base_url, + ) + + +@pytest.fixture +def mock_response(): + """Fixture providing a mock httpx Response.""" + response = Mock(spec=httpx.Response) + response.raise_for_status = Mock() + return response + + +@pytest.fixture +def valid_agent_card_data(): + """Fixture providing valid agent card data.""" + return { + 'name': 'TestAgent', + 'description': 'A test agent', + 'version': '1.0.0', + # Add other required fields based on your AgentCard model + } + + +class TestA2ACardResolverInit: + """Tests for A2ACardResolver initialization.""" + + def test_init_with_defaults(self, mock_httpx_client, base_url): + """Test initialization with default agent_card_path.""" + resolver = A2ACardResolver( + httpx_client=mock_httpx_client, + base_url=base_url, + ) + assert resolver.base_url == base_url + assert resolver.agent_card_path == AGENT_CARD_WELL_KNOWN_PATH[1:] + assert resolver.httpx_client == mock_httpx_client + + def test_init_with_custom_path(self, mock_httpx_client, base_url): + """Test initialization with custom agent_card_path.""" + custom_path = '/custom/agent/card' + resolver = A2ACardResolver( + httpx_client=mock_httpx_client, + base_url=base_url, + agent_card_path=custom_path, + ) + assert resolver.base_url == base_url + + def test_init_strips_leading_slash_from_agent_card_path( + self, mock_httpx_client, base_url + ): + """Test that leading slash is stripped from agent_card_path.""" + agent_card_path = '/well-known/agent' + resolver = A2ACardResolver( + httpx_client=mock_httpx_client, + base_url=base_url, + agent_card_path=agent_card_path, + ) + assert resolver.agent_card_path == agent_card_path[1:] + + +class TestGetAgentCard: + """Tests for get_agent_card methods.""" + + @pytest.mark.asyncio + async def test_get_agent_card_success_default_path( + self, + base_url, + resolver, + mock_httpx_client, + mock_response, + valid_agent_card_data, + ): + """Test successful agent card fetch using default path.""" + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response + + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ) as mock_validate: + result = await resolver.get_agent_card() + mock_httpx_client.get.assert_called_once_with( + f'{base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}', + ) + mock_response.raise_for_status.assert_called_once() + mock_response.json.assert_called_once() + mock_validate.assert_called_once_with(valid_agent_card_data) + assert result is not None + + @pytest.mark.asyncio + async def test_get_agent_card_success_custom_path( + self, + base_url, + resolver, + mock_httpx_client, + mock_response, + valid_agent_card_data, + ): + """Test successful agent card fetch using custom relative path.""" + custom_path = 'custom/path/card' + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ): + await resolver.get_agent_card(relative_card_path=custom_path) + + mock_httpx_client.get.assert_called_once_with( + f'{base_url}/{custom_path}', + ) + + @pytest.mark.asyncio + async def test_get_agent_card_strips_leading_slash_from_relative_path( + self, + base_url, + resolver, + mock_httpx_client, + mock_response, + valid_agent_card_data, + ): + """Test successful agent card fetch using custom path with leading slash.""" + custom_path = '/custom/path/card' + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ): + await resolver.get_agent_card(relative_card_path=custom_path) + + mock_httpx_client.get.assert_called_once_with( + f'{base_url}/{custom_path[1:]}', + ) + + @pytest.mark.asyncio + async def test_get_agent_card_with_http_kwargs( + self, + base_url, + resolver, + mock_httpx_client, + mock_response, + valid_agent_card_data, + ): + """Test that http_kwargs are passed to httpx.get.""" + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response + http_kwargs = { + 'timeout': 30, + 'headers': {'Authorization': 'Bearer token'}, + } + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ): + await resolver.get_agent_card(http_kwargs=http_kwargs) + mock_httpx_client.get.assert_called_once_with( + f'{base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}', + timeout=30, + headers={'Authorization': 'Bearer token'}, + ) + + @pytest.mark.asyncio + async def test_get_agent_card_root_path( + self, + base_url, + resolver, + mock_httpx_client, + mock_response, + valid_agent_card_data, + ): + """Test fetching agent card from root path.""" + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ): + await resolver.get_agent_card(relative_card_path='/') + mock_httpx_client.get.assert_called_once_with(f'{base_url}/') + + @pytest.mark.asyncio + async def test_get_agent_card_http_status_error( + self, resolver, mock_httpx_client + ): + """Test A2AClientHTTPError raised on HTTP status error.""" + status_code = 404 + mock_response = Mock(spec=httpx.Response) + mock_response.status_code = status_code + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + 'Not Found', request=Mock(), response=mock_response + ) + mock_httpx_client.get.return_value = mock_response + + with pytest.raises(A2AClientHTTPError) as exc_info: + await resolver.get_agent_card() + + assert exc_info.value.status_code == status_code + assert 'Failed to fetch agent card' in str(exc_info.value) + + @pytest.mark.asyncio + async def test_get_agent_card_json_decode_error( + self, resolver, mock_httpx_client, mock_response + ): + """Test A2AClientJSONError raised on JSON decode error.""" + mock_response.json.side_effect = json.JSONDecodeError( + 'Invalid JSON', '', 0 + ) + mock_httpx_client.get.return_value = mock_response + with pytest.raises(A2AClientJSONError) as exc_info: + await resolver.get_agent_card() + assert 'Failed to parse JSON' in str(exc_info.value) + + @pytest.mark.asyncio + async def test_get_agent_card_request_error( + self, resolver, mock_httpx_client + ): + """Test A2AClientHTTPError raised on network request error.""" + mock_httpx_client.get.side_effect = httpx.RequestError( + 'Connection timeout', request=Mock() + ) + with pytest.raises(A2AClientHTTPError) as exc_info: + await resolver.get_agent_card() + assert exc_info.value.status_code == 503 + assert 'Network communication error' in str(exc_info.value) + + @pytest.mark.asyncio + async def test_get_agent_card_validation_error( + self, + base_url, + resolver, + mock_httpx_client, + mock_response, + valid_agent_card_data, + ): + """Test that empty string relative_card_path uses default path.""" + return_json = {'invalid': 'data'} + mock_response.json.return_value = return_json + mock_httpx_client.get.return_value = mock_response + with pytest.raises(A2AClientJSONError) as exc_info: + await resolver.get_agent_card() + assert ( + f'Failed to validate agent card structure from {base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}' + in exc_info.value.message + ) + mock_httpx_client.get.assert_called_once_with( + f'{base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}', + ) + + @pytest.mark.asyncio + async def test_get_agent_card_logs_success( + self, + base_url, + resolver, + caplog, + ): + with ( + patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ), + caplog.at_level(logging.INFO), + ): + await resolver.get_agent_card() + assert ( + f'Successfully fetched agent card data from {base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}' + in caplog.text + ) + + @pytest.mark.asyncio + async def test_get_agent_card_none_relative_path( + self, + base_url, + resolver, + mock_httpx_client, + mock_response, + valid_agent_card_data, + ): + """Test that None relative_card_path uses default path.""" + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response + + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ): + await resolver.get_agent_card(relative_card_path=None) + mock_httpx_client.get.assert_called_once_with( + f'{base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}', + ) + + @pytest.mark.asyncio + async def test_get_agent_card_empty_string_relative_path( + self, + base_url, + resolver, + mock_httpx_client, + mock_response, + valid_agent_card_data, + ): + """Test that empty string relative_card_path uses default path.""" + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response + + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ): + await resolver.get_agent_card(relative_card_path='') + + mock_httpx_client.get.assert_called_once_with( + f'{base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}', + ) + + @pytest.mark.asyncio + async def test_get_agent_card_different_status_codes( + self, resolver, mock_httpx_client + ): + """Test different HTTP status codes raise appropriate errors.""" + for status_code in [400, 401, 403, 500, 502]: + mock_response = Mock(spec=httpx.Response) + mock_response.status_code = status_code + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + f'Status {status_code}', request=Mock(), response=mock_response + ) + mock_httpx_client.get.return_value = mock_response + with pytest.raises(A2AClientHTTPError) as exc_info: + await resolver.get_agent_card() + assert exc_info.value.status_code == status_code + + @pytest.mark.asyncio + async def test_get_agent_card_returns_agent_card_instance( + self, resolver, mock_httpx_client, mock_response, valid_agent_card_data + ): + """Test that get_agent_card returns an AgentCard instance.""" + mock_agent_card = Mock(spec=AgentCard) + with patch.object( + AgentCard, 'model_validate', return_value=mock_agent_card + ): + result = await resolver.get_agent_card() + assert result == mock_agent_card From ed7cff4544496ba4af79f6d9fd6872c2cedae977 Mon Sep 17 00:00:00 2001 From: Didier Durand Date: Sun, 14 Dec 2025 08:57:14 +0100 Subject: [PATCH 2/9] test: applying Gemini's suggestions --- tests/client/test_card_resolver.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/client/test_card_resolver.py b/tests/client/test_card_resolver.py index a2f82f13..794931e1 100644 --- a/tests/client/test_card_resolver.py +++ b/tests/client/test_card_resolver.py @@ -47,7 +47,18 @@ def valid_agent_card_data(): 'name': 'TestAgent', 'description': 'A test agent', 'version': '1.0.0', - # Add other required fields based on your AgentCard model + 'url': 'https://example.com/a2a', + 'capabilities': {}, + 'default_input_modes': ['text/plain'], + 'default_output_modes': ['text/plain'], + 'skills': [ + { + 'id': 'test-skill', + 'name': 'Test Skill', + 'description': 'A skill for testing', + 'tags': ['test'], + } + ], } @@ -73,6 +84,7 @@ def test_init_with_custom_path(self, mock_httpx_client, base_url): agent_card_path=custom_path, ) assert resolver.base_url == base_url + assert resolver.agent_card_path == custom_path[1:] def test_init_strips_leading_slash_from_agent_card_path( self, mock_httpx_client, base_url From 97c9fe5addf5635f0a6cb723e9ab71dd1930c7c8 Mon Sep 17 00:00:00 2001 From: Didier Durand Date: Sun, 14 Dec 2025 09:06:58 +0100 Subject: [PATCH 3/9] test: applying additional Gemini's suggestions --- tests/client/test_card_resolver.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/client/test_card_resolver.py b/tests/client/test_card_resolver.py index 794931e1..f96478b8 100644 --- a/tests/client/test_card_resolver.py +++ b/tests/client/test_card_resolver.py @@ -269,7 +269,7 @@ async def test_get_agent_card_validation_error( mock_response, valid_agent_card_data, ): - """Test that empty string relative_card_path uses default path.""" + """Test A2AClientJSONError is raised on agent card validation error.""" return_json = {'invalid': 'data'} mock_response.json.return_value = return_json mock_httpx_client.get.return_value = mock_response @@ -345,21 +345,21 @@ async def test_get_agent_card_empty_string_relative_path( f'{base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}', ) + @pytest.mark.parametrize('status_code', [400, 401, 403, 500, 502]) @pytest.mark.asyncio async def test_get_agent_card_different_status_codes( - self, resolver, mock_httpx_client + self, resolver, mock_httpx_client, status_code ): """Test different HTTP status codes raise appropriate errors.""" - for status_code in [400, 401, 403, 500, 502]: - mock_response = Mock(spec=httpx.Response) - mock_response.status_code = status_code - mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( - f'Status {status_code}', request=Mock(), response=mock_response - ) - mock_httpx_client.get.return_value = mock_response - with pytest.raises(A2AClientHTTPError) as exc_info: - await resolver.get_agent_card() - assert exc_info.value.status_code == status_code + mock_response = Mock(spec=httpx.Response) + mock_response.status_code = status_code + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + f'Status {status_code}', request=Mock(), response=mock_response + ) + mock_httpx_client.get.return_value = mock_response + with pytest.raises(A2AClientHTTPError) as exc_info: + await resolver.get_agent_card() + assert exc_info.value.status_code == status_code @pytest.mark.asyncio async def test_get_agent_card_returns_agent_card_instance( From 9200fc445714d3114557a42716d8065f47d7cdef Mon Sep 17 00:00:00 2001 From: Didier Durand Date: Sun, 14 Dec 2025 09:14:14 +0100 Subject: [PATCH 4/9] test: applying additional Gemini's suggestions --- tests/client/test_card_resolver.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/client/test_card_resolver.py b/tests/client/test_card_resolver.py index f96478b8..38f996c6 100644 --- a/tests/client/test_card_resolver.py +++ b/tests/client/test_card_resolver.py @@ -288,8 +288,13 @@ async def test_get_agent_card_logs_success( self, base_url, resolver, + mock_httpx_client, + mock_response, + valid_agent_card_data, caplog, ): + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response with ( patch.object( AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) From 8824a0256e8ef92e6dea9e3daf87a8a90a7296dd Mon Sep 17 00:00:00 2001 From: Didier Durand Date: Sun, 14 Dec 2025 09:14:35 +0100 Subject: [PATCH 5/9] test: applying final set of Gemini's suggestions --- tests/client/test_card_resolver_bak.py | 355 +++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 tests/client/test_card_resolver_bak.py diff --git a/tests/client/test_card_resolver_bak.py b/tests/client/test_card_resolver_bak.py new file mode 100644 index 00000000..61cd9b0f --- /dev/null +++ b/tests/client/test_card_resolver_bak.py @@ -0,0 +1,355 @@ +import json +import logging + +from unittest.mock import AsyncMock, Mock, patch + +import httpx +import pytest + +from pydantic import ValidationError + +from a2a.client.card_resolver import A2ACardResolver +from a2a.client.errors import A2AClientHTTPError, A2AClientJSONError +from a2a.types import AgentCard +from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH + + +@pytest.fixture +def mock_httpx_client(): + """Fixture providing a mocked async httpx client.""" + return AsyncMock(spec=httpx.AsyncClient) + + +@pytest.fixture +def base_url(): + """Fixture providing a test base URL.""" + return 'https://example.com' + + +@pytest.fixture +def resolver(mock_httpx_client, base_url): + """Fixture providing an A2ACardResolver instance.""" + return A2ACardResolver( + httpx_client=mock_httpx_client, + base_url=base_url, + ) + + +@pytest.fixture +def valid_agent_card_data(): + """Fixture providing valid agent card data.""" + return { + 'name': 'TestAgent', + 'description': 'A test agent', + 'version': '1.0.0', + # Add other required fields based on your AgentCard model + } + + +@pytest.fixture +def mock_response(): + """Fixture providing a mock httpx Response.""" + response = Mock(spec=httpx.Response) + response.raise_for_status = Mock() + return response + + +class TestA2ACardResolverInit: + """Tests for A2ACardResolver initialization.""" + + def test_init_with_defaults(self, mock_httpx_client, base_url): + """Test initialization with default agent_card_path.""" + resolver = A2ACardResolver( + httpx_client=mock_httpx_client, + base_url=base_url, + ) + + assert resolver.base_url == base_url + assert resolver.agent_card_path == AGENT_CARD_WELL_KNOWN_PATH[1:] + assert resolver.httpx_client == mock_httpx_client + + def test_init_with_custom_path(self, mock_httpx_client, base_url): + """Test initialization with custom agent_card_path.""" + custom_path = '/custom/agent/card' + resolver = A2ACardResolver( + httpx_client=mock_httpx_client, + base_url=base_url, + agent_card_path=custom_path, + ) + + assert resolver.agent_card_path == custom_path + + def test_init_strips_trailing_slash_from_base_url(self, mock_httpx_client): + """Test that trailing slash is stripped from base_url.""" + resolver = A2ACardResolver( + httpx_client=mock_httpx_client, + base_url='https://example.com/', + ) + + assert resolver.base_url == 'https://example.com' + + def test_init_strips_leading_slash_from_agent_card_path( + self, mock_httpx_client, base_url + ): + """Test that leading slash is stripped from agent_card_path.""" + resolver = A2ACardResolver( + httpx_client=mock_httpx_client, + base_url=base_url, + agent_card_path='/well-known/agent', + ) + + assert resolver.agent_card_path == 'well-known/agent' + + +class TestGetAgentCard: + """Tests for get_agent_card method.""" + + @pytest.mark.asyncio + async def test_get_agent_card_success_default_path( + self, resolver, mock_httpx_client, mock_response, valid_agent_card_data + ): + """Test successful agent card fetch using default path.""" + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response + + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ) as mock_validate: + result = await resolver.get_agent_card() + + mock_httpx_client.get.assert_called_once_with( + f'https://example.com/{AGENT_CARD_WELL_KNOWN_PATH[1:]}', + ) + mock_response.raise_for_status.assert_called_once() + mock_response.json.assert_called_once() + mock_validate.assert_called_once_with(valid_agent_card_data) + assert result is not None + + @pytest.mark.asyncio + async def test_get_agent_card_success_custom_path( + self, resolver, mock_httpx_client, mock_response, valid_agent_card_data + ): + """Test successful agent card fetch using custom relative path.""" + custom_path = 'custom/path/card' + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response + + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ): + await resolver.get_agent_card(relative_card_path=custom_path) + + mock_httpx_client.get.assert_called_once_with( + f'https://example.com/{custom_path}', + ) + + @pytest.mark.asyncio + async def test_get_agent_card_strips_leading_slash_from_relative_path( + self, resolver, mock_httpx_client, mock_response, valid_agent_card_data + ): + """Test that leading slash is stripped from relative_card_path.""" + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response + + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ): + await resolver.get_agent_card(relative_card_path='/custom/path') + + mock_httpx_client.get.assert_called_once_with( + 'https://example.com/custom/path', + ) + + @pytest.mark.asyncio + async def test_get_agent_card_with_http_kwargs( + self, resolver, mock_httpx_client, mock_response, valid_agent_card_data + ): + """Test that http_kwargs are passed to httpx.get.""" + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response + http_kwargs = { + 'timeout': 30, + 'headers': {'Authorization': 'Bearer token'}, + } + + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ): + await resolver.get_agent_card(http_kwargs=http_kwargs) + + mock_httpx_client.get.assert_called_once_with( + f'https://example.com/{AGENT_CARD_WELL_KNOWN_PATH[1:]}', + timeout=30, + headers={'Authorization': 'Bearer token'}, + ) + + @pytest.mark.asyncio + async def test_get_agent_card_root_path( + self, resolver, mock_httpx_client, mock_response, valid_agent_card_data + ): + """Test fetching agent card from root path.""" + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response + + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ): + await resolver.get_agent_card(relative_card_path='/') + + mock_httpx_client.get.assert_called_once_with( + 'https://example.com/', + ) + + @pytest.mark.asyncio + async def test_get_agent_card_http_status_error( + self, resolver, mock_httpx_client + ): + """Test A2AClientHTTPError raised on HTTP status error.""" + mock_response = Mock(spec=httpx.Response) + mock_response.status_code = 404 + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + 'Not Found', request=Mock(), response=mock_response + ) + mock_httpx_client.get.return_value = mock_response + + with pytest.raises(A2AClientHTTPError) as exc_info: + await resolver.get_agent_card() + + assert exc_info.value.status_code == 404 + assert 'Failed to fetch agent card' in str(exc_info.value) + + @pytest.mark.asyncio + async def test_get_agent_card_json_decode_error( + self, resolver, mock_httpx_client, mock_response + ): + """Test A2AClientJSONError raised on JSON decode error.""" + mock_response.json.side_effect = json.JSONDecodeError( + 'Invalid JSON', '', 0 + ) + mock_httpx_client.get.return_value = mock_response + + with pytest.raises(A2AClientJSONError) as exc_info: + await resolver.get_agent_card() + + assert 'Failed to parse JSON' in str(exc_info.value) + + @pytest.mark.asyncio + async def test_get_agent_card_request_error( + self, resolver, mock_httpx_client + ): + """Test A2AClientHTTPError raised on network request error.""" + mock_httpx_client.get.side_effect = httpx.RequestError( + 'Connection timeout', request=Mock() + ) + + with pytest.raises(A2AClientHTTPError) as exc_info: + await resolver.get_agent_card() + + assert exc_info.value.status_code == 503 + assert 'Network communication error' in str(exc_info.value) + + @pytest.mark.asyncio + async def test_get_agent_card_validation_error( + self, resolver, mock_httpx_client, mock_response + ): + """Test A2AClientJSONError raised on Pydantic validation error.""" + invalid_data = {"invalid": "data"} + mock_response.json.return_value = invalid_data + mock_httpx_client.get.return_value = mock_response + + # Create a mock validation error + mock_validation_error = Mock(spec=ValidationError) + mock_validation_error.json.return_value = '{"error": "validation failed"}' + + with patch.object( + AgentCard, 'model_validate', side_effect=mock_validation_error + ): + with pytest.raises(A2AClientJSONError) as exc_info: + await resolver.get_agent_card() + + assert "Failed to validate agent card structure" in str(exc_info.value) + + + @pytest.mark.asyncio + async def test_get_agent_card_logs_success( + self, + resolver, + mock_httpx_client, + mock_response, + valid_agent_card_data, + caplog, + ): + + + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ), caplog.at_level(logging.INFO): + await resolver.get_agent_card() + + assert 'Successfully fetched agent card data' in caplog.text + + @pytest.mark.asyncio + async def test_get_agent_card_none_relative_path( + self, resolver, mock_httpx_client, mock_response, valid_agent_card_data + ): + """Test that None relative_card_path uses default path.""" + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response + + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ): + await resolver.get_agent_card(relative_card_path=None) + + mock_httpx_client.get.assert_called_once_with( + f'https://example.com/{AGENT_CARD_WELL_KNOWN_PATH[1:]}', + ) + + @pytest.mark.asyncio + async def test_get_agent_card_empty_string_relative_path( + self, resolver, mock_httpx_client, mock_response, valid_agent_card_data + ): + """Test that empty string relative_card_path uses default path.""" + mock_response.json.return_value = valid_agent_card_data + mock_httpx_client.get.return_value = mock_response + + with patch.object( + AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) + ): + await resolver.get_agent_card(relative_card_path='') + + mock_httpx_client.get.assert_called_once_with( + f'https://example.com/{AGENT_CARD_WELL_KNOWN_PATH[1:]}', + ) + + @pytest.mark.asyncio + async def test_get_agent_card_different_status_codes( + self, resolver, mock_httpx_client + ): + """Test different HTTP status codes raise appropriate errors.""" + for status_code in [400, 401, 403, 500, 502]: + mock_response = Mock(spec=httpx.Response) + mock_response.status_code = status_code + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + f'Status {status_code}', request=Mock(), response=mock_response + ) + mock_httpx_client.get.return_value = mock_response + + with pytest.raises(A2AClientHTTPError) as exc_info: + await resolver.get_agent_card() + + assert exc_info.value.status_code == status_code + + @pytest.mark.asyncio + async def test_get_agent_card_returns_agent_card_instance( + self, resolver, mock_httpx_client, mock_response, valid_agent_card_data + ): + """Test that get_agent_card returns an AgentCard instance.""" + mock_agent_card = Mock(spec=AgentCard) + + with patch.object( + AgentCard, 'model_validate', return_value=mock_agent_card + ): + result = await resolver.get_agent_card() + + assert result == mock_agent_card From 8edff0136012dbf3d958664c5828e25f23a7b4cd Mon Sep 17 00:00:00 2001 From: Didier Durand Date: Sun, 14 Dec 2025 09:32:10 +0100 Subject: [PATCH 6/9] test: adding exclusion for PLR0913 in Ruff setup to allow pytest functions with number of fixtures > 5 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 561a5a45..79a1c575 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -175,6 +175,7 @@ target-version = "py310" # Minimum Python version [tool.ruff.lint] ignore = [ + "PLR0913", # Too many arguments in function definition "COM812", # Trailing comma missing. "FBT001", # Boolean positional arg in function definition "FBT002", # Boolean default value in function definition From 6ab646ec2922f255c240a023727081627cddda63 Mon Sep 17 00:00:00 2001 From: Didier Durand Date: Sun, 14 Dec 2025 09:40:18 +0100 Subject: [PATCH 7/9] test: reverting global Ruff exclusionn for PLR0913 and replacing with a local one for this single test --- pyproject.toml | 1 - tests/client/test_card_resolver.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 79a1c575..561a5a45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -175,7 +175,6 @@ target-version = "py310" # Minimum Python version [tool.ruff.lint] ignore = [ - "PLR0913", # Too many arguments in function definition "COM812", # Trailing comma missing. "FBT001", # Boolean positional arg in function definition "FBT002", # Boolean default value in function definition diff --git a/tests/client/test_card_resolver.py b/tests/client/test_card_resolver.py index 38f996c6..954dd363 100644 --- a/tests/client/test_card_resolver.py +++ b/tests/client/test_card_resolver.py @@ -284,7 +284,7 @@ async def test_get_agent_card_validation_error( ) @pytest.mark.asyncio - async def test_get_agent_card_logs_success( + async def test_get_agent_card_logs_success( # noqa: PLR0913 self, base_url, resolver, From b667fc9fa1426aaf06ab3951d7955919444cd40b Mon Sep 17 00:00:00 2001 From: Didier Durand Date: Sun, 14 Dec 2025 09:42:56 +0100 Subject: [PATCH 8/9] test: removing erroneous bak file --- tests/client/test_card_resolver_bak.py | 355 ------------------------- 1 file changed, 355 deletions(-) delete mode 100644 tests/client/test_card_resolver_bak.py diff --git a/tests/client/test_card_resolver_bak.py b/tests/client/test_card_resolver_bak.py deleted file mode 100644 index 61cd9b0f..00000000 --- a/tests/client/test_card_resolver_bak.py +++ /dev/null @@ -1,355 +0,0 @@ -import json -import logging - -from unittest.mock import AsyncMock, Mock, patch - -import httpx -import pytest - -from pydantic import ValidationError - -from a2a.client.card_resolver import A2ACardResolver -from a2a.client.errors import A2AClientHTTPError, A2AClientJSONError -from a2a.types import AgentCard -from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH - - -@pytest.fixture -def mock_httpx_client(): - """Fixture providing a mocked async httpx client.""" - return AsyncMock(spec=httpx.AsyncClient) - - -@pytest.fixture -def base_url(): - """Fixture providing a test base URL.""" - return 'https://example.com' - - -@pytest.fixture -def resolver(mock_httpx_client, base_url): - """Fixture providing an A2ACardResolver instance.""" - return A2ACardResolver( - httpx_client=mock_httpx_client, - base_url=base_url, - ) - - -@pytest.fixture -def valid_agent_card_data(): - """Fixture providing valid agent card data.""" - return { - 'name': 'TestAgent', - 'description': 'A test agent', - 'version': '1.0.0', - # Add other required fields based on your AgentCard model - } - - -@pytest.fixture -def mock_response(): - """Fixture providing a mock httpx Response.""" - response = Mock(spec=httpx.Response) - response.raise_for_status = Mock() - return response - - -class TestA2ACardResolverInit: - """Tests for A2ACardResolver initialization.""" - - def test_init_with_defaults(self, mock_httpx_client, base_url): - """Test initialization with default agent_card_path.""" - resolver = A2ACardResolver( - httpx_client=mock_httpx_client, - base_url=base_url, - ) - - assert resolver.base_url == base_url - assert resolver.agent_card_path == AGENT_CARD_WELL_KNOWN_PATH[1:] - assert resolver.httpx_client == mock_httpx_client - - def test_init_with_custom_path(self, mock_httpx_client, base_url): - """Test initialization with custom agent_card_path.""" - custom_path = '/custom/agent/card' - resolver = A2ACardResolver( - httpx_client=mock_httpx_client, - base_url=base_url, - agent_card_path=custom_path, - ) - - assert resolver.agent_card_path == custom_path - - def test_init_strips_trailing_slash_from_base_url(self, mock_httpx_client): - """Test that trailing slash is stripped from base_url.""" - resolver = A2ACardResolver( - httpx_client=mock_httpx_client, - base_url='https://example.com/', - ) - - assert resolver.base_url == 'https://example.com' - - def test_init_strips_leading_slash_from_agent_card_path( - self, mock_httpx_client, base_url - ): - """Test that leading slash is stripped from agent_card_path.""" - resolver = A2ACardResolver( - httpx_client=mock_httpx_client, - base_url=base_url, - agent_card_path='/well-known/agent', - ) - - assert resolver.agent_card_path == 'well-known/agent' - - -class TestGetAgentCard: - """Tests for get_agent_card method.""" - - @pytest.mark.asyncio - async def test_get_agent_card_success_default_path( - self, resolver, mock_httpx_client, mock_response, valid_agent_card_data - ): - """Test successful agent card fetch using default path.""" - mock_response.json.return_value = valid_agent_card_data - mock_httpx_client.get.return_value = mock_response - - with patch.object( - AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) - ) as mock_validate: - result = await resolver.get_agent_card() - - mock_httpx_client.get.assert_called_once_with( - f'https://example.com/{AGENT_CARD_WELL_KNOWN_PATH[1:]}', - ) - mock_response.raise_for_status.assert_called_once() - mock_response.json.assert_called_once() - mock_validate.assert_called_once_with(valid_agent_card_data) - assert result is not None - - @pytest.mark.asyncio - async def test_get_agent_card_success_custom_path( - self, resolver, mock_httpx_client, mock_response, valid_agent_card_data - ): - """Test successful agent card fetch using custom relative path.""" - custom_path = 'custom/path/card' - mock_response.json.return_value = valid_agent_card_data - mock_httpx_client.get.return_value = mock_response - - with patch.object( - AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) - ): - await resolver.get_agent_card(relative_card_path=custom_path) - - mock_httpx_client.get.assert_called_once_with( - f'https://example.com/{custom_path}', - ) - - @pytest.mark.asyncio - async def test_get_agent_card_strips_leading_slash_from_relative_path( - self, resolver, mock_httpx_client, mock_response, valid_agent_card_data - ): - """Test that leading slash is stripped from relative_card_path.""" - mock_response.json.return_value = valid_agent_card_data - mock_httpx_client.get.return_value = mock_response - - with patch.object( - AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) - ): - await resolver.get_agent_card(relative_card_path='/custom/path') - - mock_httpx_client.get.assert_called_once_with( - 'https://example.com/custom/path', - ) - - @pytest.mark.asyncio - async def test_get_agent_card_with_http_kwargs( - self, resolver, mock_httpx_client, mock_response, valid_agent_card_data - ): - """Test that http_kwargs are passed to httpx.get.""" - mock_response.json.return_value = valid_agent_card_data - mock_httpx_client.get.return_value = mock_response - http_kwargs = { - 'timeout': 30, - 'headers': {'Authorization': 'Bearer token'}, - } - - with patch.object( - AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) - ): - await resolver.get_agent_card(http_kwargs=http_kwargs) - - mock_httpx_client.get.assert_called_once_with( - f'https://example.com/{AGENT_CARD_WELL_KNOWN_PATH[1:]}', - timeout=30, - headers={'Authorization': 'Bearer token'}, - ) - - @pytest.mark.asyncio - async def test_get_agent_card_root_path( - self, resolver, mock_httpx_client, mock_response, valid_agent_card_data - ): - """Test fetching agent card from root path.""" - mock_response.json.return_value = valid_agent_card_data - mock_httpx_client.get.return_value = mock_response - - with patch.object( - AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) - ): - await resolver.get_agent_card(relative_card_path='/') - - mock_httpx_client.get.assert_called_once_with( - 'https://example.com/', - ) - - @pytest.mark.asyncio - async def test_get_agent_card_http_status_error( - self, resolver, mock_httpx_client - ): - """Test A2AClientHTTPError raised on HTTP status error.""" - mock_response = Mock(spec=httpx.Response) - mock_response.status_code = 404 - mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( - 'Not Found', request=Mock(), response=mock_response - ) - mock_httpx_client.get.return_value = mock_response - - with pytest.raises(A2AClientHTTPError) as exc_info: - await resolver.get_agent_card() - - assert exc_info.value.status_code == 404 - assert 'Failed to fetch agent card' in str(exc_info.value) - - @pytest.mark.asyncio - async def test_get_agent_card_json_decode_error( - self, resolver, mock_httpx_client, mock_response - ): - """Test A2AClientJSONError raised on JSON decode error.""" - mock_response.json.side_effect = json.JSONDecodeError( - 'Invalid JSON', '', 0 - ) - mock_httpx_client.get.return_value = mock_response - - with pytest.raises(A2AClientJSONError) as exc_info: - await resolver.get_agent_card() - - assert 'Failed to parse JSON' in str(exc_info.value) - - @pytest.mark.asyncio - async def test_get_agent_card_request_error( - self, resolver, mock_httpx_client - ): - """Test A2AClientHTTPError raised on network request error.""" - mock_httpx_client.get.side_effect = httpx.RequestError( - 'Connection timeout', request=Mock() - ) - - with pytest.raises(A2AClientHTTPError) as exc_info: - await resolver.get_agent_card() - - assert exc_info.value.status_code == 503 - assert 'Network communication error' in str(exc_info.value) - - @pytest.mark.asyncio - async def test_get_agent_card_validation_error( - self, resolver, mock_httpx_client, mock_response - ): - """Test A2AClientJSONError raised on Pydantic validation error.""" - invalid_data = {"invalid": "data"} - mock_response.json.return_value = invalid_data - mock_httpx_client.get.return_value = mock_response - - # Create a mock validation error - mock_validation_error = Mock(spec=ValidationError) - mock_validation_error.json.return_value = '{"error": "validation failed"}' - - with patch.object( - AgentCard, 'model_validate', side_effect=mock_validation_error - ): - with pytest.raises(A2AClientJSONError) as exc_info: - await resolver.get_agent_card() - - assert "Failed to validate agent card structure" in str(exc_info.value) - - - @pytest.mark.asyncio - async def test_get_agent_card_logs_success( - self, - resolver, - mock_httpx_client, - mock_response, - valid_agent_card_data, - caplog, - ): - - - with patch.object( - AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) - ), caplog.at_level(logging.INFO): - await resolver.get_agent_card() - - assert 'Successfully fetched agent card data' in caplog.text - - @pytest.mark.asyncio - async def test_get_agent_card_none_relative_path( - self, resolver, mock_httpx_client, mock_response, valid_agent_card_data - ): - """Test that None relative_card_path uses default path.""" - mock_response.json.return_value = valid_agent_card_data - mock_httpx_client.get.return_value = mock_response - - with patch.object( - AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) - ): - await resolver.get_agent_card(relative_card_path=None) - - mock_httpx_client.get.assert_called_once_with( - f'https://example.com/{AGENT_CARD_WELL_KNOWN_PATH[1:]}', - ) - - @pytest.mark.asyncio - async def test_get_agent_card_empty_string_relative_path( - self, resolver, mock_httpx_client, mock_response, valid_agent_card_data - ): - """Test that empty string relative_card_path uses default path.""" - mock_response.json.return_value = valid_agent_card_data - mock_httpx_client.get.return_value = mock_response - - with patch.object( - AgentCard, 'model_validate', return_value=Mock(spec=AgentCard) - ): - await resolver.get_agent_card(relative_card_path='') - - mock_httpx_client.get.assert_called_once_with( - f'https://example.com/{AGENT_CARD_WELL_KNOWN_PATH[1:]}', - ) - - @pytest.mark.asyncio - async def test_get_agent_card_different_status_codes( - self, resolver, mock_httpx_client - ): - """Test different HTTP status codes raise appropriate errors.""" - for status_code in [400, 401, 403, 500, 502]: - mock_response = Mock(spec=httpx.Response) - mock_response.status_code = status_code - mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( - f'Status {status_code}', request=Mock(), response=mock_response - ) - mock_httpx_client.get.return_value = mock_response - - with pytest.raises(A2AClientHTTPError) as exc_info: - await resolver.get_agent_card() - - assert exc_info.value.status_code == status_code - - @pytest.mark.asyncio - async def test_get_agent_card_returns_agent_card_instance( - self, resolver, mock_httpx_client, mock_response, valid_agent_card_data - ): - """Test that get_agent_card returns an AgentCard instance.""" - mock_agent_card = Mock(spec=AgentCard) - - with patch.object( - AgentCard, 'model_validate', return_value=mock_agent_card - ): - result = await resolver.get_agent_card() - - assert result == mock_agent_card From de32cb59c77f230f3c7c1e4e3c6b9366236abb53 Mon Sep 17 00:00:00 2001 From: Didier Durand Date: Sun, 14 Dec 2025 09:46:44 +0100 Subject: [PATCH 9/9] test: reformatted source code of test file --- tests/client/test_card_resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/client/test_card_resolver.py b/tests/client/test_card_resolver.py index 954dd363..f87d9450 100644 --- a/tests/client/test_card_resolver.py +++ b/tests/client/test_card_resolver.py @@ -284,7 +284,7 @@ async def test_get_agent_card_validation_error( ) @pytest.mark.asyncio - async def test_get_agent_card_logs_success( # noqa: PLR0913 + async def test_get_agent_card_logs_success( # noqa: PLR0913 self, base_url, resolver,