-
Notifications
You must be signed in to change notification settings - Fork 313
test: adding 21 tests for client/card_resolver.py #592
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
91c4909
test: adding 17 tests for client/card_resolver.py
didier-durand ed7cff4
test: applying Gemini's suggestions
didier-durand 97c9fe5
test: applying additional Gemini's suggestions
didier-durand 9200fc4
test: applying additional Gemini's suggestions
didier-durand 8824a02
test: applying final set of Gemini's suggestions
didier-durand 8edff01
test: adding exclusion for PLR0913 in Ruff setup to allow pytest func…
didier-durand 6ab646e
test: reverting global Ruff exclusionn for PLR0913 and replacing with…
didier-durand b667fc9
test: removing erroneous bak file
didier-durand de32cb5
test: reformatted source code of test file
didier-durand 8a091e3
Merge branch 'main' into test-card-resolver
lkawka File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,379 @@ | ||
| 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', | ||
| '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'], | ||
| } | ||
| ], | ||
| } | ||
|
|
||
|
|
||
| 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 | ||
| assert resolver.agent_card_path == custom_path[1:] | ||
|
|
||
| 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 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 | ||
| 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( # noqa: PLR0913 | ||
| 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) | ||
| ), | ||
| 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.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, status_code | ||
| ): | ||
| """Test different HTTP status codes raise appropriate errors.""" | ||
| 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 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.