Skip to content

Commit 8824a02

Browse files
committed
test: applying final set of Gemini's suggestions
1 parent 9200fc4 commit 8824a02

File tree

1 file changed

+355
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)