From 97be1dc35a1d21087ae7517e373fb32bcbe7c822 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:41:56 +0000 Subject: [PATCH 1/4] Initial plan From cf786a28ce9b8a4ee5362cb2f78e9a182eb51016 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:48:21 +0000 Subject: [PATCH 2/4] Improve conversation client tests with URL and payload validation Co-authored-by: heyitsaamir <48929123+heyitsaamir@users.noreply.github.com> --- packages/api/tests/conftest.py | 107 +++++++++++ .../tests/unit/test_conversation_client.py | 168 +++++++++++++++--- 2 files changed, 252 insertions(+), 23 deletions(-) diff --git a/packages/api/tests/conftest.py b/packages/api/tests/conftest.py index 86ec38b4..9324d9b7 100644 --- a/packages/api/tests/conftest.py +++ b/packages/api/tests/conftest.py @@ -214,6 +214,113 @@ def mock_http_client(mock_transport): return client +@pytest.fixture +def request_capture(): + """Fixture to capture HTTP request details for testing.""" + + class RequestCapture: + """Helper class to capture request details.""" + + def __init__(self): + self.requests: list[httpx.Request] = [] + + def handler(self, request: httpx.Request) -> httpx.Response: + """Handler that captures requests and returns mock responses.""" + self.requests.append(request) + + # Default response + response_data: Any = { + "ok": True, + "url": str(request.url), + "method": request.method, + } + + # Handle specific endpoints with realistic responses + if "GetAadTokens" in str(request.url): + response_data = { + "https://graph.microsoft.com": { + "connectionName": "test_connection", + "token": "mock_graph_token_123", + "expiration": "2024-12-01T12:00:00Z", + }, + } + elif "/conversations/" in str(request.url) and str(request.url).endswith("/members"): + response_data = [ + { + "id": "mock_member_id", + "name": "Mock Member", + "objectId": "mock_object_id", + } + ] + elif "/conversations/" in str(request.url) and "/members/" in str(request.url) and request.method == "GET": + response_data = { + "id": "mock_member_id", + "name": "Mock Member", + "objectId": "mock_object_id", + } + elif "/conversations" in str(request.url) and request.method == "GET": + response_data = { + "conversations": [ + { + "id": "mock_conversation_id", + "conversationType": "personal", + "isGroup": True, + } + ], + "continuationToken": "mock_continuation_token", + } + elif "/conversations" in str(request.url) and request.method == "POST": + response_data = { + "id": "mock_conversation_id", + "type": "message", + "activityId": "mock_activity_id", + "serviceUrl": "https://mock.service.url", + } + elif "/activities" in str(request.url): + if request.method == "POST": + response_data = { + "id": "mock_activity_id", + "type": "message", + "text": "Mock activity response", + } + elif request.method == "PUT": + response_data = { + "id": "mock_activity_id", + "type": "message", + "text": "Updated mock activity", + } + elif request.method == "GET": + response_data = [ + { + "id": "mock_member_id", + "name": "Mock Member", + "objectId": "mock_object_id", + } + ] + + return httpx.Response( + status_code=200, + json=response_data, + headers={"content-type": "application/json"}, + ) + + @property + def last_request(self) -> httpx.Request | None: + """Get the last captured request.""" + return self.requests[-1] if self.requests else None + + def clear(self): + """Clear all captured requests.""" + self.requests.clear() + + capture = RequestCapture() + transport = httpx.MockTransport(capture.handler) + client = Client(ClientOptions(base_url="https://mock.api.com")) + client.http._transport = transport + client._request_capture = capture # type: ignore[attr-defined] # Attach for easy access + return client + + @pytest.fixture def mock_client_credentials(): """Create mock client credentials for testing.""" diff --git a/packages/api/tests/unit/test_conversation_client.py b/packages/api/tests/unit/test_conversation_client.py index b094c2d4..14dcc4ec 100644 --- a/packages/api/tests/unit/test_conversation_client.py +++ b/packages/api/tests/unit/test_conversation_client.py @@ -39,34 +39,46 @@ def test_conversation_client_initialization_with_options(self): assert client.service_url == service_url @pytest.mark.asyncio - async def test_get_conversations(self, mock_http_client): - """Test getting conversations.""" + async def test_get_conversations(self, request_capture): + """Test getting conversations with continuation token.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) params = GetConversationsParams(continuation_token="test_token") response = await client.get(params) + # Validate response assert response.conversations is not None assert isinstance(response.conversations, list) assert response.continuation_token is not None + # Validate request details + last_request = request_capture._request_capture.last_request + assert last_request.method == "GET" + assert str(last_request.url) == "https://test.service.url/v3/conversations?continuationToken=test_token" + @pytest.mark.asyncio - async def test_get_conversations_without_params(self, mock_http_client): + async def test_get_conversations_without_params(self, request_capture): """Test getting conversations without parameters.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) response = await client.get() + # Validate response assert response.conversations is not None assert isinstance(response.conversations, list) + # Validate request details + last_request = request_capture._request_capture.last_request + assert last_request.method == "GET" + assert str(last_request.url) == "https://test.service.url/v3/conversations" + @pytest.mark.asyncio - async def test_create_conversation(self, mock_http_client, mock_account, mock_activity): + async def test_create_conversation(self, request_capture, mock_account, mock_activity): """Test creating a conversation.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) params = CreateConversationParams( is_group=True, @@ -80,10 +92,31 @@ async def test_create_conversation(self, mock_http_client, mock_account, mock_ac response = await client.create(params) + # Validate response assert response.id is not None assert response.activity_id is not None assert response.service_url is not None + # Validate request details + last_request = request_capture._request_capture.last_request + assert last_request.method == "POST" + assert str(last_request.url) == "https://test.service.url/v3/conversations" + + # Validate request payload + import json + + payload = json.loads(last_request.content.decode("utf-8")) + assert payload["isGroup"] is True + assert payload["bot"]["id"] == mock_account.id + assert payload["bot"]["name"] == mock_account.name + assert len(payload["members"]) == 1 + assert payload["members"][0]["id"] == mock_account.id + assert payload["topicName"] == "Test Conversation" + assert payload["tenantId"] == "test_tenant_id" + assert payload["activity"]["type"] == "message" + assert payload["activity"]["text"] == "Mock activity text" + assert payload["channelData"]["custom"] == "data" + def test_activities_operations(self, mock_http_client): """Test activities operations object creation.""" service_url = "https://test.service.url" @@ -112,22 +145,37 @@ def test_members_operations(self, mock_http_client): class TestConversationActivityOperations: """Unit tests for ConversationClient activity operations.""" - async def test_activity_create(self, mock_http_client, mock_activity): + async def test_activity_create(self, request_capture, mock_activity): """Test creating an activity.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" activities = client.activities(conversation_id) result = await activities.create(mock_activity) + # Validate response assert result is not None + assert result.id is not None + + # Validate request details + last_request = request_capture._request_capture.last_request + assert last_request.method == "POST" + assert str(last_request.url) == f"https://test.service.url/v3/conversations/{conversation_id}/activities" + + # Validate request payload + import json - async def test_activity_update(self, mock_http_client, mock_activity): + payload = json.loads(last_request.content.decode("utf-8")) + assert payload["type"] == "message" + assert payload["text"] == "Mock activity text" + assert payload["from"]["id"] == "sender_id" + + async def test_activity_update(self, request_capture, mock_activity): """Test updating an activity.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" activity_id = "test_activity_id" @@ -135,12 +183,29 @@ async def test_activity_update(self, mock_http_client, mock_activity): result = await activities.update(activity_id, mock_activity) + # Validate response assert result is not None + assert result.id is not None + + # Validate request details + last_request = request_capture._request_capture.last_request + assert last_request.method == "PUT" + assert ( + str(last_request.url) + == f"https://test.service.url/v3/conversations/{conversation_id}/activities/{activity_id}" + ) + + # Validate request payload + import json - async def test_activity_reply(self, mock_http_client, mock_activity): + payload = json.loads(last_request.content.decode("utf-8")) + assert payload["type"] == "message" + assert payload["text"] == "Mock activity text" + + async def test_activity_reply(self, request_capture, mock_activity): """Test replying to an activity.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" activity_id = "test_activity_id" @@ -148,12 +213,30 @@ async def test_activity_reply(self, mock_http_client, mock_activity): result = await activities.reply(activity_id, mock_activity) + # Validate response assert result is not None + assert result.id is not None + + # Validate request details + last_request = request_capture._request_capture.last_request + assert last_request.method == "POST" + assert ( + str(last_request.url) + == f"https://test.service.url/v3/conversations/{conversation_id}/activities/{activity_id}" + ) + + # Validate request payload + import json - async def test_activity_delete(self, mock_http_client): + payload = json.loads(last_request.content.decode("utf-8")) + assert payload["type"] == "message" + assert payload["text"] == "Mock activity text" + assert payload["replyToId"] == activity_id + + async def test_activity_delete(self, request_capture): """Test deleting an activity.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" activity_id = "test_activity_id" @@ -162,10 +245,18 @@ async def test_activity_delete(self, mock_http_client): # Should not raise an exception await activities.delete(activity_id) - async def test_activity_get_members(self, mock_http_client): + # Validate request details + last_request = request_capture._request_capture.last_request + assert last_request.method == "DELETE" + assert ( + str(last_request.url) + == f"https://test.service.url/v3/conversations/{conversation_id}/activities/{activity_id}" + ) + + async def test_activity_get_members(self, request_capture): """Test getting activity members.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" activity_id = "test_activity_id" @@ -173,7 +264,17 @@ async def test_activity_get_members(self, mock_http_client): result = await activities.get_members(activity_id) + # Validate response assert result is not None + assert len(result) > 0 + + # Validate request details + last_request = request_capture._request_capture.last_request + assert last_request.method == "GET" + assert ( + str(last_request.url) + == f"https://test.service.url/v3/conversations/{conversation_id}/activities/{activity_id}/members" + ) @pytest.mark.unit @@ -181,16 +282,17 @@ async def test_activity_get_members(self, mock_http_client): class TestConversationMemberOperations: """Unit tests for ConversationClient member operations.""" - async def test_member_get_all(self, mock_http_client): + async def test_member_get_all(self, request_capture): """Test getting all members returns TeamsChannelAccount instances.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" members = client.members(conversation_id) result = await members.get_all() + # Validate response assert result is not None assert len(result) > 0 assert isinstance(result[0], TeamsChannelAccount) @@ -198,10 +300,15 @@ async def test_member_get_all(self, mock_http_client): assert result[0].name == "Mock Member" assert result[0].object_id == "mock_object_id" - async def test_member_get(self, mock_http_client): + # Validate request details + last_request = request_capture._request_capture.last_request + assert last_request.method == "GET" + assert str(last_request.url) == f"https://test.service.url/v3/conversations/{conversation_id}/members" + + async def test_member_get(self, request_capture): """Test getting a specific member returns TeamsChannelAccount instance.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" member_id = "test_member_id" @@ -209,16 +316,24 @@ async def test_member_get(self, mock_http_client): result = await members.get(member_id) + # Validate response assert result is not None assert isinstance(result, TeamsChannelAccount) assert result.id == "mock_member_id" assert result.name == "Mock Member" assert result.object_id == "mock_object_id" - async def test_member_delete(self, mock_http_client): + # Validate request details + last_request = request_capture._request_capture.last_request + assert last_request.method == "GET" + assert ( + str(last_request.url) == f"https://test.service.url/v3/conversations/{conversation_id}/members/{member_id}" + ) + + async def test_member_delete(self, request_capture): """Test deleting a member.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" member_id = "test_member_id" @@ -227,6 +342,13 @@ async def test_member_delete(self, mock_http_client): # Should not raise an exception await members.delete(member_id) + # Validate request details + last_request = request_capture._request_capture.last_request + assert last_request.method == "DELETE" + assert ( + str(last_request.url) == f"https://test.service.url/v3/conversations/{conversation_id}/members/{member_id}" + ) + @pytest.mark.unit class TestConversationClientHttpClientSharing: From 5cab74a6eee6e0ffef91d96f5985a6336a555328 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:53:13 +0000 Subject: [PATCH 3/4] Refactor request capture to use cleaner API and add json import Co-authored-by: heyitsaamir <48929123+heyitsaamir@users.noreply.github.com> --- packages/api/tests/conftest.py | 9 ++++-- .../tests/unit/test_conversation_client.py | 28 +++++++++---------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/api/tests/conftest.py b/packages/api/tests/conftest.py index 9324d9b7..a260f144 100644 --- a/packages/api/tests/conftest.py +++ b/packages/api/tests/conftest.py @@ -216,7 +216,12 @@ def mock_http_client(mock_transport): @pytest.fixture def request_capture(): - """Fixture to capture HTTP request details for testing.""" + """Fixture to capture HTTP request details for testing. + + Returns: + A Client instance with an attached `_capture` attribute containing the RequestCapture helper. + Access captured requests via `client._capture.last_request` or `client._capture.requests`. + """ class RequestCapture: """Helper class to capture request details.""" @@ -317,7 +322,7 @@ def clear(self): transport = httpx.MockTransport(capture.handler) client = Client(ClientOptions(base_url="https://mock.api.com")) client.http._transport = transport - client._request_capture = capture # type: ignore[attr-defined] # Attach for easy access + client._capture = capture # type: ignore[attr-defined] # Attach for test access return client diff --git a/packages/api/tests/unit/test_conversation_client.py b/packages/api/tests/unit/test_conversation_client.py index 14dcc4ec..51c9ead7 100644 --- a/packages/api/tests/unit/test_conversation_client.py +++ b/packages/api/tests/unit/test_conversation_client.py @@ -4,6 +4,8 @@ """ # pyright: basic +import json + import pytest from microsoft_teams.api.clients.conversation import ConversationClient from microsoft_teams.api.clients.conversation.params import ( @@ -53,7 +55,7 @@ async def test_get_conversations(self, request_capture): assert response.continuation_token is not None # Validate request details - last_request = request_capture._request_capture.last_request + last_request = request_capture._capture.last_request assert last_request.method == "GET" assert str(last_request.url) == "https://test.service.url/v3/conversations?continuationToken=test_token" @@ -70,7 +72,7 @@ async def test_get_conversations_without_params(self, request_capture): assert isinstance(response.conversations, list) # Validate request details - last_request = request_capture._request_capture.last_request + last_request = request_capture._capture.last_request assert last_request.method == "GET" assert str(last_request.url) == "https://test.service.url/v3/conversations" @@ -98,12 +100,11 @@ async def test_create_conversation(self, request_capture, mock_account, mock_act assert response.service_url is not None # Validate request details - last_request = request_capture._request_capture.last_request + last_request = request_capture._capture.last_request assert last_request.method == "POST" assert str(last_request.url) == "https://test.service.url/v3/conversations" # Validate request payload - import json payload = json.loads(last_request.content.decode("utf-8")) assert payload["isGroup"] is True @@ -160,12 +161,11 @@ async def test_activity_create(self, request_capture, mock_activity): assert result.id is not None # Validate request details - last_request = request_capture._request_capture.last_request + last_request = request_capture._capture.last_request assert last_request.method == "POST" assert str(last_request.url) == f"https://test.service.url/v3/conversations/{conversation_id}/activities" # Validate request payload - import json payload = json.loads(last_request.content.decode("utf-8")) assert payload["type"] == "message" @@ -188,7 +188,7 @@ async def test_activity_update(self, request_capture, mock_activity): assert result.id is not None # Validate request details - last_request = request_capture._request_capture.last_request + last_request = request_capture._capture.last_request assert last_request.method == "PUT" assert ( str(last_request.url) @@ -196,7 +196,6 @@ async def test_activity_update(self, request_capture, mock_activity): ) # Validate request payload - import json payload = json.loads(last_request.content.decode("utf-8")) assert payload["type"] == "message" @@ -218,7 +217,7 @@ async def test_activity_reply(self, request_capture, mock_activity): assert result.id is not None # Validate request details - last_request = request_capture._request_capture.last_request + last_request = request_capture._capture.last_request assert last_request.method == "POST" assert ( str(last_request.url) @@ -226,7 +225,6 @@ async def test_activity_reply(self, request_capture, mock_activity): ) # Validate request payload - import json payload = json.loads(last_request.content.decode("utf-8")) assert payload["type"] == "message" @@ -246,7 +244,7 @@ async def test_activity_delete(self, request_capture): await activities.delete(activity_id) # Validate request details - last_request = request_capture._request_capture.last_request + last_request = request_capture._capture.last_request assert last_request.method == "DELETE" assert ( str(last_request.url) @@ -269,7 +267,7 @@ async def test_activity_get_members(self, request_capture): assert len(result) > 0 # Validate request details - last_request = request_capture._request_capture.last_request + last_request = request_capture._capture.last_request assert last_request.method == "GET" assert ( str(last_request.url) @@ -301,7 +299,7 @@ async def test_member_get_all(self, request_capture): assert result[0].object_id == "mock_object_id" # Validate request details - last_request = request_capture._request_capture.last_request + last_request = request_capture._capture.last_request assert last_request.method == "GET" assert str(last_request.url) == f"https://test.service.url/v3/conversations/{conversation_id}/members" @@ -324,7 +322,7 @@ async def test_member_get(self, request_capture): assert result.object_id == "mock_object_id" # Validate request details - last_request = request_capture._request_capture.last_request + last_request = request_capture._capture.last_request assert last_request.method == "GET" assert ( str(last_request.url) == f"https://test.service.url/v3/conversations/{conversation_id}/members/{member_id}" @@ -343,7 +341,7 @@ async def test_member_delete(self, request_capture): await members.delete(member_id) # Validate request details - last_request = request_capture._request_capture.last_request + last_request = request_capture._capture.last_request assert last_request.method == "DELETE" assert ( str(last_request.url) == f"https://test.service.url/v3/conversations/{conversation_id}/members/{member_id}" From b05eb7919a2bbbb0a86e5e2dbe451881bd860b68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:04:08 +0000 Subject: [PATCH 4/4] Simplify payload validation and add token authorization test Co-authored-by: heyitsaamir <48929123+heyitsaamir@users.noreply.github.com> --- .../tests/unit/test_conversation_client.py | 68 +++++++++++++------ 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/packages/api/tests/unit/test_conversation_client.py b/packages/api/tests/unit/test_conversation_client.py index 51c9ead7..ecdcc072 100644 --- a/packages/api/tests/unit/test_conversation_client.py +++ b/packages/api/tests/unit/test_conversation_client.py @@ -76,6 +76,54 @@ async def test_get_conversations_without_params(self, request_capture): assert last_request.method == "GET" assert str(last_request.url) == "https://test.service.url/v3/conversations" + @pytest.mark.asyncio + async def test_get_conversations_with_token(self): + """Test that authorization token is sent in requests.""" + service_url = "https://test.service.url" + + # Create client with token + options = ClientOptions(base_url="https://mock.api.com", token="test_bearer_token") + + # Create request capture with the configured client + from typing import Any + + import httpx + + class RequestCapture: + def __init__(self): + self.requests: list[httpx.Request] = [] + + def handler(self, request: httpx.Request) -> httpx.Response: + self.requests.append(request) + response_data: Any = { + "conversations": [{"id": "test", "conversationType": "personal", "isGroup": False}], + "continuationToken": "token", + } + return httpx.Response(status_code=200, json=response_data, headers={"content-type": "application/json"}) + + @property + def last_request(self) -> httpx.Request | None: + return self.requests[-1] if self.requests else None + + capture = RequestCapture() + transport = httpx.MockTransport(capture.handler) + from microsoft_teams.common.http import Client + + client_with_token = Client(options) + client_with_token.http._transport = transport + + # Create conversation client with the token-enabled HTTP client + conv_client = ConversationClient(service_url, client_with_token) + + # Make request + await conv_client.get() + + # Validate token was sent in Authorization header + last_request = capture.last_request + assert last_request is not None + assert "Authorization" in last_request.headers + assert last_request.headers["Authorization"] == "Bearer test_bearer_token" + @pytest.mark.asyncio async def test_create_conversation(self, request_capture, mock_account, mock_activity): """Test creating a conversation.""" @@ -105,18 +153,8 @@ async def test_create_conversation(self, request_capture, mock_account, mock_act assert str(last_request.url) == "https://test.service.url/v3/conversations" # Validate request payload - payload = json.loads(last_request.content.decode("utf-8")) assert payload["isGroup"] is True - assert payload["bot"]["id"] == mock_account.id - assert payload["bot"]["name"] == mock_account.name - assert len(payload["members"]) == 1 - assert payload["members"][0]["id"] == mock_account.id - assert payload["topicName"] == "Test Conversation" - assert payload["tenantId"] == "test_tenant_id" - assert payload["activity"]["type"] == "message" - assert payload["activity"]["text"] == "Mock activity text" - assert payload["channelData"]["custom"] == "data" def test_activities_operations(self, mock_http_client): """Test activities operations object creation.""" @@ -166,11 +204,8 @@ async def test_activity_create(self, request_capture, mock_activity): assert str(last_request.url) == f"https://test.service.url/v3/conversations/{conversation_id}/activities" # Validate request payload - payload = json.loads(last_request.content.decode("utf-8")) assert payload["type"] == "message" - assert payload["text"] == "Mock activity text" - assert payload["from"]["id"] == "sender_id" async def test_activity_update(self, request_capture, mock_activity): """Test updating an activity.""" @@ -196,10 +231,8 @@ async def test_activity_update(self, request_capture, mock_activity): ) # Validate request payload - payload = json.loads(last_request.content.decode("utf-8")) assert payload["type"] == "message" - assert payload["text"] == "Mock activity text" async def test_activity_reply(self, request_capture, mock_activity): """Test replying to an activity.""" @@ -224,11 +257,8 @@ async def test_activity_reply(self, request_capture, mock_activity): == f"https://test.service.url/v3/conversations/{conversation_id}/activities/{activity_id}" ) - # Validate request payload - + # Validate request payload - check that replyToId was added payload = json.loads(last_request.content.decode("utf-8")) - assert payload["type"] == "message" - assert payload["text"] == "Mock activity text" assert payload["replyToId"] == activity_id async def test_activity_delete(self, request_capture):