Skip to content

Commit 91c4909

Browse files
committed
test: adding 17 tests for client/card_resolver.py
1 parent 03fa4c2 commit 91c4909

File tree

1 file changed

+362
-0
lines changed

1 file changed

+362
-0
lines changed

tests/client/test_card_resolver.py

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

0 commit comments

Comments
 (0)