Skip to content

Commit 3deecc4

Browse files
test: adding 21 tests for client/card_resolver.py (#592)
# Description Adding 21 tests for client/card_resolver.py They all pass: ``` ========================= test session starts ============================== collecting ... collected 21 items tests/client/test_card_resolver.py::TestA2ACardResolverInit::test_init_with_defaults PASSED [ 4%] tests/client/test_card_resolver.py::TestA2ACardResolverInit::test_init_with_custom_path PASSED [ 9%] tests/client/test_card_resolver.py::TestA2ACardResolverInit::test_init_strips_leading_slash_from_agent_card_path PASSED [ 14%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_success_default_path PASSED [ 19%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_success_custom_path PASSED [ 23%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_strips_leading_slash_from_relative_path PASSED [ 28%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_with_http_kwargs PASSED [ 33%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_root_path PASSED [ 38%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_http_status_error PASSED [ 42%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_json_decode_error PASSED [ 47%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_request_error PASSED [ 52%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_validation_error PASSED [ 57%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_logs_success PASSED [ 61%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_none_relative_path PASSED [ 66%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_empty_string_relative_path PASSED [ 71%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_different_status_codes[400] PASSED [ 76%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_different_status_codes[401] PASSED [ 80%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_different_status_codes[403] PASSED [ 85%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_different_status_codes[500] PASSED [ 90%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_different_status_codes[502] PASSED [ 95%] tests/client/test_card_resolver.py::TestGetAgentCard::test_get_agent_card_returns_agent_card_instance PASSED [100%] ======================== 21 passed, 2 warnings in 0.11s ======================== ``` - [X] Follow the [`CONTRIBUTING` Guide](https://github.com/a2aproject/a2a-python/blob/main/CONTRIBUTING.md). - [X] Make your Pull Request title in the <https://www.conventionalcommits.org/> specification. - [X] Ensure the tests and linter pass (Run `bash scripts/format.sh` from the repository root to format) - [N/A ] Appropriate docs were updated (if necessary) Fixes #<issue_number_goes_here> 🦕 N/A --------- Co-authored-by: Lukasz Kawka <luk.kawka@gmail.com>
1 parent e12ca42 commit 3deecc4

File tree

1 file changed

+379
-0
lines changed

1 file changed

+379
-0
lines changed

tests/client/test_card_resolver.py

Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
import json
2+
import logging
3+
4+
from unittest.mock import AsyncMock, Mock, patch
5+
6+
import httpx
7+
import pytest
8+
9+
from a2a.client import A2ACardResolver, A2AClientHTTPError, A2AClientJSONError
10+
from a2a.types import AgentCard
11+
from a2a.utils import AGENT_CARD_WELL_KNOWN_PATH
12+
13+
14+
@pytest.fixture
15+
def mock_httpx_client():
16+
"""Fixture providing a mocked async httpx client."""
17+
return AsyncMock(spec=httpx.AsyncClient)
18+
19+
20+
@pytest.fixture
21+
def base_url():
22+
"""Fixture providing a test base URL."""
23+
return 'https://example.com'
24+
25+
26+
@pytest.fixture
27+
def resolver(mock_httpx_client, base_url):
28+
"""Fixture providing an A2ACardResolver instance."""
29+
return A2ACardResolver(
30+
httpx_client=mock_httpx_client,
31+
base_url=base_url,
32+
)
33+
34+
35+
@pytest.fixture
36+
def mock_response():
37+
"""Fixture providing a mock httpx Response."""
38+
response = Mock(spec=httpx.Response)
39+
response.raise_for_status = Mock()
40+
return response
41+
42+
43+
@pytest.fixture
44+
def valid_agent_card_data():
45+
"""Fixture providing valid agent card data."""
46+
return {
47+
'name': 'TestAgent',
48+
'description': 'A test agent',
49+
'version': '1.0.0',
50+
'url': 'https://example.com/a2a',
51+
'capabilities': {},
52+
'default_input_modes': ['text/plain'],
53+
'default_output_modes': ['text/plain'],
54+
'skills': [
55+
{
56+
'id': 'test-skill',
57+
'name': 'Test Skill',
58+
'description': 'A skill for testing',
59+
'tags': ['test'],
60+
}
61+
],
62+
}
63+
64+
65+
class TestA2ACardResolverInit:
66+
"""Tests for A2ACardResolver initialization."""
67+
68+
def test_init_with_defaults(self, mock_httpx_client, base_url):
69+
"""Test initialization with default agent_card_path."""
70+
resolver = A2ACardResolver(
71+
httpx_client=mock_httpx_client,
72+
base_url=base_url,
73+
)
74+
assert resolver.base_url == base_url
75+
assert resolver.agent_card_path == AGENT_CARD_WELL_KNOWN_PATH[1:]
76+
assert resolver.httpx_client == mock_httpx_client
77+
78+
def test_init_with_custom_path(self, mock_httpx_client, base_url):
79+
"""Test initialization with custom agent_card_path."""
80+
custom_path = '/custom/agent/card'
81+
resolver = A2ACardResolver(
82+
httpx_client=mock_httpx_client,
83+
base_url=base_url,
84+
agent_card_path=custom_path,
85+
)
86+
assert resolver.base_url == base_url
87+
assert resolver.agent_card_path == custom_path[1:]
88+
89+
def test_init_strips_leading_slash_from_agent_card_path(
90+
self, mock_httpx_client, base_url
91+
):
92+
"""Test that leading slash is stripped from agent_card_path."""
93+
agent_card_path = '/well-known/agent'
94+
resolver = A2ACardResolver(
95+
httpx_client=mock_httpx_client,
96+
base_url=base_url,
97+
agent_card_path=agent_card_path,
98+
)
99+
assert resolver.agent_card_path == agent_card_path[1:]
100+
101+
102+
class TestGetAgentCard:
103+
"""Tests for get_agent_card methods."""
104+
105+
@pytest.mark.asyncio
106+
async def test_get_agent_card_success_default_path(
107+
self,
108+
base_url,
109+
resolver,
110+
mock_httpx_client,
111+
mock_response,
112+
valid_agent_card_data,
113+
):
114+
"""Test successful agent card fetch using default path."""
115+
mock_response.json.return_value = valid_agent_card_data
116+
mock_httpx_client.get.return_value = mock_response
117+
118+
with patch.object(
119+
AgentCard, 'model_validate', return_value=Mock(spec=AgentCard)
120+
) as mock_validate:
121+
result = await resolver.get_agent_card()
122+
mock_httpx_client.get.assert_called_once_with(
123+
f'{base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}',
124+
)
125+
mock_response.raise_for_status.assert_called_once()
126+
mock_response.json.assert_called_once()
127+
mock_validate.assert_called_once_with(valid_agent_card_data)
128+
assert result is not None
129+
130+
@pytest.mark.asyncio
131+
async def test_get_agent_card_success_custom_path(
132+
self,
133+
base_url,
134+
resolver,
135+
mock_httpx_client,
136+
mock_response,
137+
valid_agent_card_data,
138+
):
139+
"""Test successful agent card fetch using custom relative path."""
140+
custom_path = 'custom/path/card'
141+
mock_response.json.return_value = valid_agent_card_data
142+
mock_httpx_client.get.return_value = mock_response
143+
with patch.object(
144+
AgentCard, 'model_validate', return_value=Mock(spec=AgentCard)
145+
):
146+
await resolver.get_agent_card(relative_card_path=custom_path)
147+
148+
mock_httpx_client.get.assert_called_once_with(
149+
f'{base_url}/{custom_path}',
150+
)
151+
152+
@pytest.mark.asyncio
153+
async def test_get_agent_card_strips_leading_slash_from_relative_path(
154+
self,
155+
base_url,
156+
resolver,
157+
mock_httpx_client,
158+
mock_response,
159+
valid_agent_card_data,
160+
):
161+
"""Test successful agent card fetch using custom path with leading slash."""
162+
custom_path = '/custom/path/card'
163+
mock_response.json.return_value = valid_agent_card_data
164+
mock_httpx_client.get.return_value = mock_response
165+
with patch.object(
166+
AgentCard, 'model_validate', return_value=Mock(spec=AgentCard)
167+
):
168+
await resolver.get_agent_card(relative_card_path=custom_path)
169+
170+
mock_httpx_client.get.assert_called_once_with(
171+
f'{base_url}/{custom_path[1:]}',
172+
)
173+
174+
@pytest.mark.asyncio
175+
async def test_get_agent_card_with_http_kwargs(
176+
self,
177+
base_url,
178+
resolver,
179+
mock_httpx_client,
180+
mock_response,
181+
valid_agent_card_data,
182+
):
183+
"""Test that http_kwargs are passed to httpx.get."""
184+
mock_response.json.return_value = valid_agent_card_data
185+
mock_httpx_client.get.return_value = mock_response
186+
http_kwargs = {
187+
'timeout': 30,
188+
'headers': {'Authorization': 'Bearer token'},
189+
}
190+
with patch.object(
191+
AgentCard, 'model_validate', return_value=Mock(spec=AgentCard)
192+
):
193+
await resolver.get_agent_card(http_kwargs=http_kwargs)
194+
mock_httpx_client.get.assert_called_once_with(
195+
f'{base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}',
196+
timeout=30,
197+
headers={'Authorization': 'Bearer token'},
198+
)
199+
200+
@pytest.mark.asyncio
201+
async def test_get_agent_card_root_path(
202+
self,
203+
base_url,
204+
resolver,
205+
mock_httpx_client,
206+
mock_response,
207+
valid_agent_card_data,
208+
):
209+
"""Test fetching agent card from root path."""
210+
mock_response.json.return_value = valid_agent_card_data
211+
mock_httpx_client.get.return_value = mock_response
212+
with patch.object(
213+
AgentCard, 'model_validate', return_value=Mock(spec=AgentCard)
214+
):
215+
await resolver.get_agent_card(relative_card_path='/')
216+
mock_httpx_client.get.assert_called_once_with(f'{base_url}/')
217+
218+
@pytest.mark.asyncio
219+
async def test_get_agent_card_http_status_error(
220+
self, resolver, mock_httpx_client
221+
):
222+
"""Test A2AClientHTTPError raised on HTTP status error."""
223+
status_code = 404
224+
mock_response = Mock(spec=httpx.Response)
225+
mock_response.status_code = status_code
226+
mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
227+
'Not Found', request=Mock(), response=mock_response
228+
)
229+
mock_httpx_client.get.return_value = mock_response
230+
231+
with pytest.raises(A2AClientHTTPError) as exc_info:
232+
await resolver.get_agent_card()
233+
234+
assert exc_info.value.status_code == status_code
235+
assert 'Failed to fetch agent card' in str(exc_info.value)
236+
237+
@pytest.mark.asyncio
238+
async def test_get_agent_card_json_decode_error(
239+
self, resolver, mock_httpx_client, mock_response
240+
):
241+
"""Test A2AClientJSONError raised on JSON decode error."""
242+
mock_response.json.side_effect = json.JSONDecodeError(
243+
'Invalid JSON', '', 0
244+
)
245+
mock_httpx_client.get.return_value = mock_response
246+
with pytest.raises(A2AClientJSONError) as exc_info:
247+
await resolver.get_agent_card()
248+
assert 'Failed to parse JSON' in str(exc_info.value)
249+
250+
@pytest.mark.asyncio
251+
async def test_get_agent_card_request_error(
252+
self, resolver, mock_httpx_client
253+
):
254+
"""Test A2AClientHTTPError raised on network request error."""
255+
mock_httpx_client.get.side_effect = httpx.RequestError(
256+
'Connection timeout', request=Mock()
257+
)
258+
with pytest.raises(A2AClientHTTPError) as exc_info:
259+
await resolver.get_agent_card()
260+
assert exc_info.value.status_code == 503
261+
assert 'Network communication error' in str(exc_info.value)
262+
263+
@pytest.mark.asyncio
264+
async def test_get_agent_card_validation_error(
265+
self,
266+
base_url,
267+
resolver,
268+
mock_httpx_client,
269+
mock_response,
270+
valid_agent_card_data,
271+
):
272+
"""Test A2AClientJSONError is raised on agent card validation error."""
273+
return_json = {'invalid': 'data'}
274+
mock_response.json.return_value = return_json
275+
mock_httpx_client.get.return_value = mock_response
276+
with pytest.raises(A2AClientJSONError) as exc_info:
277+
await resolver.get_agent_card()
278+
assert (
279+
f'Failed to validate agent card structure from {base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}'
280+
in exc_info.value.message
281+
)
282+
mock_httpx_client.get.assert_called_once_with(
283+
f'{base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}',
284+
)
285+
286+
@pytest.mark.asyncio
287+
async def test_get_agent_card_logs_success( # noqa: PLR0913
288+
self,
289+
base_url,
290+
resolver,
291+
mock_httpx_client,
292+
mock_response,
293+
valid_agent_card_data,
294+
caplog,
295+
):
296+
mock_response.json.return_value = valid_agent_card_data
297+
mock_httpx_client.get.return_value = mock_response
298+
with (
299+
patch.object(
300+
AgentCard, 'model_validate', return_value=Mock(spec=AgentCard)
301+
),
302+
caplog.at_level(logging.INFO),
303+
):
304+
await resolver.get_agent_card()
305+
assert (
306+
f'Successfully fetched agent card data from {base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}'
307+
in caplog.text
308+
)
309+
310+
@pytest.mark.asyncio
311+
async def test_get_agent_card_none_relative_path(
312+
self,
313+
base_url,
314+
resolver,
315+
mock_httpx_client,
316+
mock_response,
317+
valid_agent_card_data,
318+
):
319+
"""Test that None relative_card_path uses default path."""
320+
mock_response.json.return_value = valid_agent_card_data
321+
mock_httpx_client.get.return_value = mock_response
322+
323+
with patch.object(
324+
AgentCard, 'model_validate', return_value=Mock(spec=AgentCard)
325+
):
326+
await resolver.get_agent_card(relative_card_path=None)
327+
mock_httpx_client.get.assert_called_once_with(
328+
f'{base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}',
329+
)
330+
331+
@pytest.mark.asyncio
332+
async def test_get_agent_card_empty_string_relative_path(
333+
self,
334+
base_url,
335+
resolver,
336+
mock_httpx_client,
337+
mock_response,
338+
valid_agent_card_data,
339+
):
340+
"""Test that empty string relative_card_path uses default path."""
341+
mock_response.json.return_value = valid_agent_card_data
342+
mock_httpx_client.get.return_value = mock_response
343+
344+
with patch.object(
345+
AgentCard, 'model_validate', return_value=Mock(spec=AgentCard)
346+
):
347+
await resolver.get_agent_card(relative_card_path='')
348+
349+
mock_httpx_client.get.assert_called_once_with(
350+
f'{base_url}/{AGENT_CARD_WELL_KNOWN_PATH[1:]}',
351+
)
352+
353+
@pytest.mark.parametrize('status_code', [400, 401, 403, 500, 502])
354+
@pytest.mark.asyncio
355+
async def test_get_agent_card_different_status_codes(
356+
self, resolver, mock_httpx_client, status_code
357+
):
358+
"""Test different HTTP status codes raise appropriate errors."""
359+
mock_response = Mock(spec=httpx.Response)
360+
mock_response.status_code = status_code
361+
mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
362+
f'Status {status_code}', request=Mock(), response=mock_response
363+
)
364+
mock_httpx_client.get.return_value = mock_response
365+
with pytest.raises(A2AClientHTTPError) as exc_info:
366+
await resolver.get_agent_card()
367+
assert exc_info.value.status_code == status_code
368+
369+
@pytest.mark.asyncio
370+
async def test_get_agent_card_returns_agent_card_instance(
371+
self, resolver, mock_httpx_client, mock_response, valid_agent_card_data
372+
):
373+
"""Test that get_agent_card returns an AgentCard instance."""
374+
mock_agent_card = Mock(spec=AgentCard)
375+
with patch.object(
376+
AgentCard, 'model_validate', return_value=mock_agent_card
377+
):
378+
result = await resolver.get_agent_card()
379+
assert result == mock_agent_card

0 commit comments

Comments
 (0)