Skip to content

Commit 74c6a36

Browse files
Copilotheyitsaamir
andauthored
Add URL and payload validation to conversation client tests (#265)
Conversation client tests only verified non-null responses without validating HTTP requests. Tests now capture and assert on request methods, URLs, query parameters, and JSON payloads. ## Changes **Added `request_capture` fixture** - Captures HTTP requests during tests via mock transport - Exposes `last_request` property for validation - Returns standard `Client` instance compatible with existing APIs **Enhanced test assertions** - HTTP method validation (GET, POST, PUT, DELETE) - Exact URL path validation including route parameters - Query parameter validation (e.g., `continuationToken`) - JSON payload validation for create/update operations (simplified to check 1 key field per test) - Authorization header validation for token-based authentication **Added token authorization test** - New `test_get_conversations_with_token` validates Bearer tokens are properly sent in the Authorization header ## Example Before: ```python async def test_create_conversation(self, mock_http_client, mock_account): response = await client.create(params) assert response.id is not None ``` After: ```python async def test_create_conversation(self, request_capture, mock_account): response = await client.create(params) # Validate request request = request_capture._capture.last_request assert request.method == "POST" assert str(request.url) == "https://test.service.url/v3/conversations" payload = json.loads(request.content) assert payload["isGroup"] is True ``` Covers 10 methods across ConversationClient, ActivityOperations, and MemberOperations, plus token authorization. <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/microsoft/teams.py/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: heyitsaamir <48929123+heyitsaamir@users.noreply.github.com>
1 parent 77cacdb commit 74c6a36

2 files changed

Lines changed: 243 additions & 20 deletions

File tree

packages/api/tests/conftest.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,128 @@ def mock_http_client(mock_transport):
240240
return client
241241

242242

243+
@pytest.fixture
244+
def request_capture():
245+
"""Fixture to capture HTTP request details for testing.
246+
247+
Returns:
248+
A Client instance with an attached `_capture` attribute containing the RequestCapture helper.
249+
Access captured requests via `client._capture.last_request` or `client._capture.requests`.
250+
"""
251+
252+
class RequestCapture:
253+
"""Helper class to capture request details."""
254+
255+
def __init__(self):
256+
self.requests: list[httpx.Request] = []
257+
258+
def handler(self, request: httpx.Request) -> httpx.Response:
259+
"""Handler that captures requests and returns mock responses."""
260+
self.requests.append(request)
261+
262+
# Default response
263+
response_data: Any = {
264+
"ok": True,
265+
"url": str(request.url),
266+
"method": request.method,
267+
}
268+
269+
# Handle specific endpoints with realistic responses
270+
if "GetAadTokens" in str(request.url):
271+
response_data = {
272+
"https://graph.microsoft.com": {
273+
"connectionName": "test_connection",
274+
"token": "mock_graph_token_123",
275+
"expiration": "2024-12-01T12:00:00Z",
276+
},
277+
}
278+
elif "/conversations/" in str(request.url) and str(request.url).endswith("/members"):
279+
response_data = [
280+
{
281+
"id": "mock_member_id",
282+
"name": "Mock Member",
283+
"aadObjectId": "mock_aad_object_id",
284+
}
285+
]
286+
elif "/conversations/" in str(request.url) and "/members/" in str(request.url) and request.method == "GET":
287+
response_data = {
288+
"id": "mock_member_id",
289+
"name": "Mock Member",
290+
"aadObjectId": "mock_aad_object_id",
291+
}
292+
elif "/conversations" in str(request.url) and request.method == "GET":
293+
response_data = {
294+
"conversations": [
295+
{
296+
"id": "mock_conversation_id",
297+
"conversationType": "personal",
298+
"isGroup": True,
299+
}
300+
],
301+
"continuationToken": "mock_continuation_token",
302+
}
303+
elif "/conversations" in str(request.url) and request.method == "POST":
304+
# Parse request body to check if activity is included
305+
try:
306+
import json
307+
308+
request_body = json.loads(request.content.decode("utf-8")) if request.content else {}
309+
has_activity = "activity" in request_body and request_body["activity"] is not None
310+
except Exception:
311+
has_activity = True # Default to including activity_id if we can't parse
312+
313+
response_data = {
314+
"id": "mock_conversation_id",
315+
"type": "message",
316+
"serviceUrl": "https://mock.service.url",
317+
}
318+
if has_activity:
319+
response_data["activityId"] = "mock_activity_id"
320+
elif "/activities" in str(request.url):
321+
if request.method == "POST":
322+
response_data = {
323+
"id": "mock_activity_id",
324+
"type": "message",
325+
"text": "Mock activity response",
326+
}
327+
elif request.method == "PUT":
328+
response_data = {
329+
"id": "mock_activity_id",
330+
"type": "message",
331+
"text": "Updated mock activity",
332+
}
333+
elif request.method == "GET":
334+
response_data = [
335+
{
336+
"id": "mock_member_id",
337+
"name": "Mock Member",
338+
"aadObjectId": "mock_aad_object_id",
339+
}
340+
]
341+
342+
return httpx.Response(
343+
status_code=200,
344+
json=response_data,
345+
headers={"content-type": "application/json"},
346+
)
347+
348+
@property
349+
def last_request(self) -> httpx.Request | None:
350+
"""Get the last captured request."""
351+
return self.requests[-1] if self.requests else None
352+
353+
def clear(self):
354+
"""Clear all captured requests."""
355+
self.requests.clear()
356+
357+
capture = RequestCapture()
358+
transport = httpx.MockTransport(capture.handler)
359+
client = Client(ClientOptions(base_url="https://mock.api.com"))
360+
client.http._transport = transport
361+
client._capture = capture # type: ignore[attr-defined] # Attach for test access
362+
return client
363+
364+
243365
@pytest.fixture
244366
def mock_client_credentials():
245367
"""Create mock client credentials for testing."""

0 commit comments

Comments
 (0)