diff --git a/packages/api/tests/conftest.py b/packages/api/tests/conftest.py index 90efb2ff..2fa97f01 100644 --- a/packages/api/tests/conftest.py +++ b/packages/api/tests/conftest.py @@ -240,6 +240,128 @@ def mock_http_client(mock_transport): return client +@pytest.fixture +def request_capture(): + """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.""" + + 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", + "aadObjectId": "mock_aad_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", + "aadObjectId": "mock_aad_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": + # Parse request body to check if activity is included + try: + import json + + request_body = json.loads(request.content.decode("utf-8")) if request.content else {} + has_activity = "activity" in request_body and request_body["activity"] is not None + except Exception: + has_activity = True # Default to including activity_id if we can't parse + + response_data = { + "id": "mock_conversation_id", + "type": "message", + "serviceUrl": "https://mock.service.url", + } + if has_activity: + response_data["activityId"] = "mock_activity_id" + 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", + "aadObjectId": "mock_aad_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._capture = capture # type: ignore[attr-defined] # Attach for test 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 bc468e20..cc309015 100644 --- a/packages/api/tests/unit/test_conversation_client.py +++ b/packages/api/tests/unit/test_conversation_client.py @@ -4,6 +4,7 @@ """ # pyright: basic +import json from unittest.mock import AsyncMock, patch import httpx @@ -48,13 +49,12 @@ def test_conversation_client_initialization_with_options(self): assert client.service_url == service_url @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 with an activity.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) params = CreateConversationParams( - is_group=True, members=[mock_account], tenant_id="test_tenant_id", activity=mock_activity, @@ -63,28 +63,43 @@ 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._capture.last_request + assert last_request.method == "POST" + assert str(last_request.url) == "https://test.service.url/v3/conversations" + + # Validate request payload + payload = json.loads(last_request.content) + assert payload["tenantId"] == "test_tenant_id" + @pytest.mark.asyncio - async def test_create_conversation_without_activity(self, mock_http_client, mock_account): + async def test_create_conversation_without_activity(self, request_capture, mock_account): """Test creating a conversation without an activity.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) params = CreateConversationParams( - is_group=True, members=[mock_account], tenant_id="test_tenant_id", ) response = await client.create(params) + # Validate response assert response.id is not None assert response.activity_id is None assert response.service_url is not None + # Validate request details + last_request = request_capture._capture.last_request + assert last_request.method == "POST" + assert str(last_request.url) == "https://test.service.url/v3/conversations" + def test_conversation_resource_with_all_fields(self): """Test that ConversationResource correctly handles all fields present.""" resource = ConversationResource.model_validate( @@ -161,22 +176,33 @@ 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._capture.last_request + assert last_request.method == "POST" + assert str(last_request.url) == f"https://test.service.url/v3/conversations/{conversation_id}/activities" - async def test_activity_update(self, mock_http_client, mock_activity): + # Validate request payload + payload = json.loads(last_request.content) + assert payload["type"] == "message" + + 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" @@ -184,12 +210,26 @@ 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._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 + payload = json.loads(last_request.content) + assert payload["type"] == "message" - async def test_activity_reply(self, mock_http_client, mock_activity): + 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" @@ -197,12 +237,26 @@ 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._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 - check that replyToId was added + payload = json.loads(last_request.content) + assert payload["replyToId"] == activity_id - async def test_activity_delete(self, mock_http_client): + 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" @@ -211,10 +265,33 @@ 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._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}" + ) + + conversation_id = "test_conversation_id" + activity_id = "test_activity_id" + activities = client.activities(conversation_id) + + # Should not raise an exception + await activities.delete(activity_id) + + # Validate request details + 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}/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" @@ -222,7 +299,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._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 @@ -230,16 +317,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) @@ -247,10 +335,15 @@ async def test_member_get_all(self, mock_http_client): assert result[0].name == "Mock Member" assert result[0].aad_object_id == "mock_aad_object_id" - async def test_member_get(self, mock_http_client): + # Validate request details + 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" + + 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" @@ -258,12 +351,20 @@ 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.aad_object_id == "mock_aad_object_id" + # Validate request details + 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}" + ) + async def test_member_get_paged(self, mock_http_client): """Test getting a page of members returns PagedMembersResult."""