From 9c4249cae475bfe4c770930cc0c382e3a1b1ada1 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Thu, 15 Jan 2026 19:46:00 +0100 Subject: [PATCH 1/2] DEVEXP-794: Conversation - Messages (Endpoint Unit Tests) --- tests/conftest.py | 42 ++- .../v1/endpoints/messages/__init__.py | 0 .../messages/test_delete_message_endpoint.py | 83 +++++ .../messages/test_get_message_endpoint.py | 270 ++++++++++++++++ .../messages/test_update_message_endpoint.py | 301 ++++++++++++++++++ 5 files changed, 683 insertions(+), 13 deletions(-) create mode 100644 tests/unit/domains/conversation/v1/endpoints/messages/__init__.py create mode 100644 tests/unit/domains/conversation/v1/endpoints/messages/test_delete_message_endpoint.py create mode 100644 tests/unit/domains/conversation/v1/endpoints/messages/test_get_message_endpoint.py create mode 100644 tests/unit/domains/conversation/v1/endpoints/messages/test_update_message_endpoint.py diff --git a/tests/conftest.py b/tests/conftest.py index 424073bb..a8a8f164 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,12 +6,11 @@ ListDeliveryReportsRequest, ListDeliveryReportsResponse, ) -from sinch.domains.sms.models.v1.response import RecipientDeliveryReport import pytest from sinch import SinchClient -from sinch.core.models.base_model import SinchBaseModel, SinchRequestBaseModel +from sinch.core.models.base_model import SinchRequestBaseModel from sinch.core.models.http_response import HTTPResponse from sinch.domains.authentication.models.v1.authentication import OAuthToken from sinch.domains.numbers.models.v1.response import ActiveNumber @@ -266,8 +265,10 @@ class MockSinchClient: return MockSinchClient() -@pytest.fixture -def mock_sinch_client_sms(): +def _create_mock_sinch_client(**config_kwargs): + """ + Helper function to create a mock Sinch client with the given configuration. + """ from sinch.core.clients.sinch_client_configuration import Configuration from sinch.core.ports.http_transport import HTTPTransport from sinch.core.token_manager import TokenManager @@ -277,16 +278,16 @@ def mock_sinch_client_sms(): mock_token_manager = MagicMock(spec=TokenManager) - config = Configuration( - transport=mock_transport, - token_manager=mock_token_manager, - project_id="test_project_id", - key_id="test_key_id", - key_secret="test_key_secret", - service_plan_id="test_service_plan_id", - sms_region="eu" - ) + default_config = { + "transport": mock_transport, + "token_manager": mock_token_manager, + "project_id": "test_project_id", + "key_id": "test_key_id", + "key_secret": "test_key_secret", + } + default_config.update(config_kwargs) + config = Configuration(**default_config) config._authentication_method = "project_auth" class MockSinchClient: @@ -295,6 +296,21 @@ class MockSinchClient: return MockSinchClient() +@pytest.fixture +def mock_sinch_client_sms(): + return _create_mock_sinch_client( + service_plan_id="test_service_plan_id", + sms_region="eu" + ) + + +@pytest.fixture +def mock_sinch_client_conversation(): + return _create_mock_sinch_client( + conversation_region="us" + ) + + @pytest.fixture def mock_pagination_active_number_responses(): return [ diff --git a/tests/unit/domains/conversation/v1/endpoints/messages/__init__.py b/tests/unit/domains/conversation/v1/endpoints/messages/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/domains/conversation/v1/endpoints/messages/test_delete_message_endpoint.py b/tests/unit/domains/conversation/v1/endpoints/messages/test_delete_message_endpoint.py new file mode 100644 index 00000000..777cd8cc --- /dev/null +++ b/tests/unit/domains/conversation/v1/endpoints/messages/test_delete_message_endpoint.py @@ -0,0 +1,83 @@ +import pytest +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.conversation.api.v1.internal import DeleteMessageEndpoint +from sinch.domains.conversation.models.v1.messages.internal.request import MessageIdRequest +from sinch.domains.conversation.api.v1.exceptions import ConversationException + + +@pytest.fixture +def request_data(): + return MessageIdRequest(message_id="01FC66621XXXXX119Z8PMV1QPQ") + + +@pytest.fixture +def mock_response(): + return HTTPResponse( + status_code=204, + body=None, + headers={"Content-Type": "application/json"}, + ) + + +@pytest.fixture +def mock_error_response(): + return HTTPResponse( + status_code=404, + body={ + "error": { + "message": "Message not found", + "status": "NotFound" + } + }, + headers={"Content-Type": "application/json"}, + ) + + +@pytest.fixture +def endpoint(request_data): + return DeleteMessageEndpoint("test_project_id", request_data) + + +def test_build_url_expects_correct_url(endpoint, mock_sinch_client_conversation): + """ + Test that the URL is built correctly. + """ + assert ( + endpoint.build_url(mock_sinch_client_conversation) + == "https://us.conversation.api.sinch.com//v1/projects/test_project_id/messages/01FC66621XXXXX119Z8PMV1QPQ" + ) + + +def test_messages_source_query_param_expects_parsed_params(): + """ + Test that the messages_source query parameter is parsed correctly. + """ + request_data = MessageIdRequest( + message_id="01FC66621XXXXX119Z8PMV1QPQ", + messages_source="CONVERSATION_SOURCE" + ) + endpoint = DeleteMessageEndpoint("test_project_id", request_data) + + query_params = endpoint.build_query_params() + assert query_params["messages_source"] == "CONVERSATION_SOURCE" + + +def test_handle_response_expects_success(endpoint, mock_response): + """ + Test that a successful delete response (204 No Content) is handled correctly. + """ + result = endpoint.handle_response(mock_response) + assert result is None + + +def test_handle_response_expects_conversation_exception_on_error( + endpoint, mock_error_response +): + """ + Test that ConversationException is raised when server returns an error. + """ + with pytest.raises(ConversationException) as exc_info: + endpoint.handle_response(mock_error_response) + + assert exc_info.value.is_from_server is True + assert exc_info.value.http_response.status_code == 404 diff --git a/tests/unit/domains/conversation/v1/endpoints/messages/test_get_message_endpoint.py b/tests/unit/domains/conversation/v1/endpoints/messages/test_get_message_endpoint.py new file mode 100644 index 00000000..a9abdc25 --- /dev/null +++ b/tests/unit/domains/conversation/v1/endpoints/messages/test_get_message_endpoint.py @@ -0,0 +1,270 @@ +import pytest +from datetime import datetime, timezone +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.conversation.api.v1.internal import GetMessageEndpoint +from sinch.domains.conversation.models.v1.messages.internal.request import MessageIdRequest +from sinch.domains.conversation.models.v1.messages.response.message_response import ( + AppMessageResponse, + ContactMessageResponse, +) + + +@pytest.fixture +def request_data(): + return MessageIdRequest(message_id="CAPY123456789ABCDEFGHIJKLMNOP") + + +@pytest.fixture +def mock_contact_message_response(): + """Mock response for ContactMessageResponse (Union type test).""" + return HTTPResponse( + status_code=200, + body={ + "id": "CAPY123456789ABCDEFGHIJKLMNOP", + "conversation_id": "CONV987654321ZYXWVUTSRQPONMLK", + "contact_id": "CONTACT456789ABCDEFGHIJKLMNOPQR", + "direction": "UNDEFINED_DIRECTION", + "channel_identity": { + "app_id": "APP123456789ABCDEFGHIJK", + "channel": "WHATSAPP", + "identity": "+46701234567" + }, + "metadata": "test_metadata", + "accept_time": "2026-01-14T20:32:31.147Z", + "injected": True, + "sender_id": "SENDER123456789ABCDEFGHIJK", + "processing_mode": "CONVERSATION", + "contact_message": { + "channel_specific_message": { + "message_type": "nfm_reply", + "message": { + "type": "nfm_reply", + "nfm_reply": { + "name": "flow", + "response_json": "{\"key\": \"value\"}", + "body": "Message body text" + } + } + }, + "reply_to": { + "message_id": "REPLY_TO_MSG123456789ABCDEF" + } + } + }, + headers={"Content-Type": "application/json"}, + ) + + +@pytest.fixture +def mock_app_message_response(): + """Mock response for AppMessageResponse (Union type test).""" + return HTTPResponse( + status_code=200, + body={ + "id": "APP123456789ABCDEFGHIJKLMNOP", + "conversation_id": "CONV987654321ZYXWVUTSRQPONMLK", + "contact_id": "CONTACT456789ABCDEFGHIJKLMNOPQR", + "direction": "UNDEFINED_DIRECTION", + "channel_identity": { + "app_id": "APP123456789ABCDEFGHIJK", + "channel": "WHATSAPP", + "identity": "+46701234567" + }, + "metadata": "test_metadata", + "accept_time": "2026-01-14T20:32:31.147Z", + "injected": True, + "sender_id": "SENDER123456789ABCDEFGHIJK", + "processing_mode": "CONVERSATION", + "app_message": { + "card_message": { + "choices": [ + { + "call_message": { + "phone_number": "+15551231234", + "title": "Message text" + }, + "postback_data": None + } + ], + "description": "Card description text", + "height": "UNSPECIFIED_HEIGHT", + "title": "Card title", + "media_message": { + "thumbnail_url": "https://example.com/thumbnail.jpg", + "url": "https://example.com/media.jpg", + "filename_override": "custom_filename.jpg" + }, + "message_properties": { + "whatsapp_header": "WhatsApp header text" + } + }, + "explicit_channel_message": { + "property1": "string", + "property2": "string" + }, + "explicit_channel_omni_message": { + "property1": { + "text_message": { + "text": "string" + } + }, + "property2": { + "text_message": { + "text": "string" + } + } + }, + "channel_specific_message": { + "property1": { + "message_type": "FLOWS", + "message": { + "header": { + "type": "text", + "text": "string" + }, + "body": { + "text": "string" + }, + "footer": { + "text": "string" + }, + "flow_id": "string", + "flow_token": "string", + "flow_mode": "draft", + "flow_cta": "string", + "flow_action": "navigate", + "flow_action_payload": { + "screen": "string", + "data": {} + } + } + } + }, + "agent": { + "display_name": "Agent Name", + "type": "UNKNOWN_AGENT_TYPE", + "picture_url": "https://example.com/agent.jpg" + } + } + }, + headers={"Content-Type": "application/json"}, + ) + + +@pytest.fixture +def endpoint(request_data): + return GetMessageEndpoint("test_project_id", request_data) + + +def test_build_url_expects_correct_url(endpoint, mock_sinch_client_conversation): + """" + Test that the URL is built correctly. + """ + assert ( + endpoint.build_url(mock_sinch_client_conversation) + == "https://us.conversation.api.sinch.com//v1/projects/test_project_id/messages/CAPY123456789ABCDEFGHIJKLMNOP" + ) + + +def test_messages_source_query_param_expects_parsed_params(): + """ + Test that the messages_source query parameter is parsed correctly. + """ + request_data = MessageIdRequest( + message_id="CAPY123456789ABCDEFGHIJKLMNOP", + messages_source="CONVERSATION_SOURCE" + ) + endpoint = GetMessageEndpoint("test_project_id", request_data) + + query_params = endpoint.build_query_params() + assert query_params["messages_source"] == "CONVERSATION_SOURCE" + + +def test_handle_response_expects_contact_message_response(endpoint, mock_contact_message_response): + """ + Test that contact message response is handled correctly and mapped to the appropriate fields. + """ + parsed_response = endpoint.handle_response(mock_contact_message_response) + + # ConversationMessageResponse is a Union of AppMessageResponse and ContactMessageResponse + # In this test case, we expect a ContactMessageResponse + assert isinstance(parsed_response, ContactMessageResponse) + assert not isinstance(parsed_response, AppMessageResponse) + + assert parsed_response.id == "CAPY123456789ABCDEFGHIJKLMNOP" + assert parsed_response.conversation_id == "CONV987654321ZYXWVUTSRQPONMLK" + assert parsed_response.contact_id == "CONTACT456789ABCDEFGHIJKLMNOPQR" + assert parsed_response.direction == "UNDEFINED_DIRECTION" + assert parsed_response.metadata == "test_metadata" + assert parsed_response.contact_message is not None + assert parsed_response.contact_message.channel_specific_message is not None + assert parsed_response.contact_message.channel_specific_message.message_type == "nfm_reply" + assert parsed_response.contact_message.channel_specific_message.message.type == "nfm_reply" + assert parsed_response.contact_message.channel_specific_message.message.nfm_reply.name == "flow" + assert parsed_response.contact_message.channel_specific_message.message.nfm_reply.response_json == "{\"key\": \"value\"}" + assert parsed_response.contact_message.channel_specific_message.message.nfm_reply.body == "Message body text" + assert parsed_response.contact_message.reply_to is not None + assert parsed_response.contact_message.reply_to.message_id == "REPLY_TO_MSG123456789ABCDEF" + assert parsed_response.channel_identity is not None + assert parsed_response.channel_identity.app_id == "APP123456789ABCDEFGHIJK" + assert parsed_response.channel_identity.channel == "WHATSAPP" + assert parsed_response.channel_identity.identity == "+46701234567" + assert parsed_response.injected is True + assert parsed_response.sender_id == "SENDER123456789ABCDEFGHIJK" + assert parsed_response.processing_mode == "CONVERSATION" + + assert parsed_response.accept_time == datetime( + 2026, 1, 14, 20, 32, 31, 147000, tzinfo=timezone.utc + ) + + +def test_handle_response_expects_app_message_response(mock_app_message_response): + """ + Test that the app message response is handled correctly and mapped to the appropriate fields. + """ + request_data = MessageIdRequest(message_id="APP123456789ABCDEFGHIJKLMNOP") + endpoint = GetMessageEndpoint("test_project_id", request_data) + + parsed_response = endpoint.handle_response(mock_app_message_response) + + # ConversationMessageResponse is a Union of AppMessageResponse and ContactMessageResponse + # In this test case, we expect an AppMessageResponse + assert isinstance(parsed_response, AppMessageResponse) + assert not isinstance(parsed_response, ContactMessageResponse) + + assert parsed_response.id == "APP123456789ABCDEFGHIJKLMNOP" + assert parsed_response.conversation_id == "CONV987654321ZYXWVUTSRQPONMLK" + assert parsed_response.contact_id == "CONTACT456789ABCDEFGHIJKLMNOPQR" + assert parsed_response.direction == "UNDEFINED_DIRECTION" + assert parsed_response.metadata == "test_metadata" + assert parsed_response.app_message is not None + assert parsed_response.app_message.card_message is not None + assert parsed_response.app_message.card_message.title == "Card title" + assert parsed_response.app_message.card_message.description == "Card description text" + assert parsed_response.app_message.card_message.height == "UNSPECIFIED_HEIGHT" + assert parsed_response.app_message.card_message.choices is not None + assert len(parsed_response.app_message.card_message.choices) == 1 + assert parsed_response.app_message.card_message.choices[0].call_message is not None + assert parsed_response.app_message.card_message.choices[0].call_message.phone_number == "+15551231234" + assert parsed_response.app_message.card_message.choices[0].call_message.title == "Message text" + assert parsed_response.app_message.card_message.media_message is not None + assert parsed_response.app_message.card_message.media_message.url == "https://example.com/media.jpg" + assert parsed_response.app_message.card_message.media_message.thumbnail_url == "https://example.com/thumbnail.jpg" + assert parsed_response.app_message.card_message.media_message.filename_override == "custom_filename.jpg" + assert parsed_response.app_message.card_message.message_properties is not None + assert parsed_response.app_message.card_message.message_properties.whatsapp_header == "WhatsApp header text" + assert parsed_response.app_message.agent is not None + assert parsed_response.app_message.agent.display_name == "Agent Name" + assert parsed_response.app_message.agent.type == "UNKNOWN_AGENT_TYPE" + assert parsed_response.app_message.agent.picture_url == "https://example.com/agent.jpg" + assert parsed_response.channel_identity is not None + assert parsed_response.channel_identity.app_id == "APP123456789ABCDEFGHIJK" + assert parsed_response.channel_identity.channel == "WHATSAPP" + assert parsed_response.channel_identity.identity == "+46701234567" + assert parsed_response.injected is True + assert parsed_response.sender_id == "SENDER123456789ABCDEFGHIJK" + assert parsed_response.processing_mode == "CONVERSATION" + + assert parsed_response.accept_time == datetime( + 2026, 1, 14, 20, 32, 31, 147000, tzinfo=timezone.utc + ) diff --git a/tests/unit/domains/conversation/v1/endpoints/messages/test_update_message_endpoint.py b/tests/unit/domains/conversation/v1/endpoints/messages/test_update_message_endpoint.py new file mode 100644 index 00000000..7912bb6b --- /dev/null +++ b/tests/unit/domains/conversation/v1/endpoints/messages/test_update_message_endpoint.py @@ -0,0 +1,301 @@ +import json +import pytest +from datetime import datetime, timezone +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.conversation.api.v1.internal import UpdateMessageMetadataEndpoint +from sinch.domains.conversation.models.v1.messages.internal.request import UpdateMessageMetadataRequest +from sinch.domains.conversation.models.v1.messages.response.message_response import ( + AppMessageResponse, + ContactMessageResponse, +) + + +@pytest.fixture +def request_data(): + return UpdateMessageMetadataRequest( + message_id="UPDATE123456789ABCDEFGHIJKLMNOP", + metadata="updated_metadata_value", + ) + + +@pytest.fixture +def mock_contact_message_response(): + """Mock response for ContactMessageResponse (Union type test).""" + return HTTPResponse( + status_code=200, + body={ + "id": "UPDATE123456789ABCDEFGHIJKLMNOP", + "conversation_id": "UPDATE_CONV987654321ZYXWVUTSRQP", + "contact_id": "UPDATE_CONTACT456789ABCDEFGHIJK", + "direction": "TO_CONTACT", + "channel_identity": { + "app_id": "APP123456789ABCDEFGHIJK", + "channel": "WHATSAPP", + "identity": "+46701234567" + }, + "metadata": "updated_metadata_value", + "accept_time": "2026-01-15T17:19:12.000Z", + "injected": True, + "sender_id": "SENDER123456789ABCDEFGHIJK", + "processing_mode": "CONVERSATION", + "contact_message": { + "channel_specific_message": { + "message_type": "nfm_reply", + "message": { + "type": "nfm_reply", + "nfm_reply": { + "name": "flow", + "response_json": "{\"key\": \"value\"}", + "body": "Updated message content" + } + } + }, + "reply_to": { + "message_id": "REPLY_TO_MSG123456789ABCDEF" + } + } + }, + headers={"Content-Type": "application/json"}, + ) + + +@pytest.fixture +def mock_app_message_response(): + """Mock response for AppMessageResponse (Union type test).""" + return HTTPResponse( + status_code=200, + body={ + "id": "UPDATE_APP123456789ABCDEFGHIJK", + "conversation_id": "UPDATE_CONV987654321ZYXWVUTSRQP", + "contact_id": "UPDATE_CONTACT456789ABCDEFGHIJK", + "direction": "TO_CONTACT", + "channel_identity": { + "app_id": "APP123456789ABCDEFGHIJK", + "channel": "WHATSAPP", + "identity": "+46701234567" + }, + "metadata": "updated_metadata_value", + "accept_time": "2026-01-15T17:19:12.000Z", + "injected": True, + "sender_id": "SENDER123456789ABCDEFGHIJK", + "processing_mode": "CONVERSATION", + "app_message": { + "card_message": { + "choices": [ + { + "call_message": { + "phone_number": "+15551231234", + "title": "Message text" + }, + "postback_data": None + } + ], + "description": "Card description text", + "height": "UNSPECIFIED_HEIGHT", + "title": "Card title", + "media_message": { + "thumbnail_url": "https://update.example.com/thumb.jpg", + "url": "https://update.example.com/image.jpg", + "filename_override": "updated_image.jpg" + }, + "message_properties": { + "whatsapp_header": "WhatsApp header text" + } + }, + "explicit_channel_message": { + "property1": "string", + "property2": "string" + }, + "explicit_channel_omni_message": { + "property1": { + "text_message": { + "text": "string" + } + }, + "property2": { + "text_message": { + "text": "string" + } + } + }, + "channel_specific_message": { + "property1": { + "message_type": "FLOWS", + "message": { + "header": { + "type": "text", + "text": "string" + }, + "body": { + "text": "string" + }, + "footer": { + "text": "string" + }, + "flow_id": "string", + "flow_token": "string", + "flow_mode": "draft", + "flow_cta": "string", + "flow_action": "navigate", + "flow_action_payload": { + "screen": "string", + "data": {} + } + } + } + }, + "agent": { + "display_name": "Updated Agent", + "type": "HUMAN_AGENT", + "picture_url": "https://update.example.com/agent_photo.jpg" + } + } + }, + headers={"Content-Type": "application/json"}, + ) + + +@pytest.fixture +def endpoint(request_data): + return UpdateMessageMetadataEndpoint("test_project_id", request_data) + + +def test_build_url_expects_correct_url(endpoint, mock_sinch_client_conversation): + """Test that the URL is built correctly.""" + assert ( + endpoint.build_url(mock_sinch_client_conversation) + == "https://us.conversation.api.sinch.com//v1/projects/test_project_id/messages/UPDATE123456789ABCDEFGHIJKLMNOP" + ) + + +def test_messages_source_query_param_expects_parsed_params(): + """ + Test that the URL is built correctly with messages_source query parameter. + metadata is from body application/json, so it should not be in query params. + """ + request_data = UpdateMessageMetadataRequest( + message_id="UPDATE123456789ABCDEFGHIJKLMNOP", + metadata="updated_metadata_value", + messages_source="CONVERSATION_SOURCE" + ) + endpoint = UpdateMessageMetadataEndpoint("test_project_id", request_data) + + query_params = endpoint.build_query_params() + assert "metadata" not in query_params + assert query_params["messages_source"] == "CONVERSATION_SOURCE" + + +def test_request_body_expects_excludes_message_id(request_data): + """ + Test that message_id is excluded from request body. + """ + endpoint = UpdateMessageMetadataEndpoint("test_project_id", request_data) + body = json.loads(endpoint.request_body()) + + assert "message_id" not in body + assert "metadata" in body + assert body["metadata"] == "updated_metadata_value" + + +def test_request_body_expects_excludes_query_params(request_data): + """ + Test that messages_source is excluded from request body (it's a query param). + """ + request_data.messages_source = "CONVERSATION_SOURCE" + + endpoint = UpdateMessageMetadataEndpoint("test_project_id", request_data) + body = json.loads(endpoint.request_body()) + + assert "message_id" not in body + assert "messages_source" not in body + assert "metadata" in body + assert body["metadata"] == "updated_metadata_value" + + +def test_handle_response_expects_contact_message_mapping(endpoint, mock_contact_message_response): + """ + Test that the response handles ContactMessageResponse correctly (Union type test). + """ + parsed_response = endpoint.handle_response(mock_contact_message_response) + + assert isinstance(parsed_response, ContactMessageResponse) + assert not isinstance(parsed_response, AppMessageResponse) + + assert parsed_response.id == "UPDATE123456789ABCDEFGHIJKLMNOP" + assert parsed_response.conversation_id == "UPDATE_CONV987654321ZYXWVUTSRQP" + assert parsed_response.contact_id == "UPDATE_CONTACT456789ABCDEFGHIJK" + assert parsed_response.direction == "TO_CONTACT" + assert parsed_response.metadata == "updated_metadata_value" + assert parsed_response.contact_message is not None + assert parsed_response.contact_message.channel_specific_message is not None + assert parsed_response.contact_message.channel_specific_message.message_type == "nfm_reply" + assert parsed_response.contact_message.channel_specific_message.message.type == "nfm_reply" + assert parsed_response.contact_message.channel_specific_message.message.nfm_reply.name == "flow" + assert parsed_response.contact_message.channel_specific_message.message.nfm_reply.response_json == "{\"key\": \"value\"}" + assert parsed_response.contact_message.channel_specific_message.message.nfm_reply.body == "Updated message content" + assert parsed_response.contact_message.reply_to is not None + assert parsed_response.contact_message.reply_to.message_id == "REPLY_TO_MSG123456789ABCDEF" + assert parsed_response.channel_identity is not None + assert parsed_response.channel_identity.app_id == "APP123456789ABCDEFGHIJK" + assert parsed_response.channel_identity.channel == "WHATSAPP" + assert parsed_response.channel_identity.identity == "+46701234567" + assert parsed_response.injected is True + assert parsed_response.sender_id == "SENDER123456789ABCDEFGHIJK" + assert parsed_response.processing_mode == "CONVERSATION" + + assert parsed_response.accept_time == datetime( + 2026, 1, 15, 17, 19, 12, 0, tzinfo=timezone.utc + ) + + +def test_handle_response_expects_app_message_mapping(mock_app_message_response): + """ + Test that the response handles AppMessageResponse correctly (Union type test). + """ + request_data = UpdateMessageMetadataRequest( + message_id="UPDATE_APP123456789ABCDEFGHIJK", + metadata="updated_metadata_value", + ) + endpoint = UpdateMessageMetadataEndpoint("test_project_id", request_data) + + parsed_response = endpoint.handle_response(mock_app_message_response) + + assert isinstance(parsed_response, AppMessageResponse) + assert not isinstance(parsed_response, ContactMessageResponse) + + assert parsed_response.id == "UPDATE_APP123456789ABCDEFGHIJK" + assert parsed_response.conversation_id == "UPDATE_CONV987654321ZYXWVUTSRQP" + assert parsed_response.contact_id == "UPDATE_CONTACT456789ABCDEFGHIJK" + assert parsed_response.direction == "TO_CONTACT" + assert parsed_response.metadata == "updated_metadata_value" + assert parsed_response.app_message is not None + assert parsed_response.app_message.card_message is not None + assert parsed_response.app_message.card_message.title == "Card title" + assert parsed_response.app_message.card_message.description == "Card description text" + assert parsed_response.app_message.card_message.height == "UNSPECIFIED_HEIGHT" + assert parsed_response.app_message.card_message.choices is not None + assert len(parsed_response.app_message.card_message.choices) == 1 + assert parsed_response.app_message.card_message.choices[0].call_message is not None + assert parsed_response.app_message.card_message.choices[0].call_message.phone_number == "+15551231234" + assert parsed_response.app_message.card_message.choices[0].call_message.title == "Message text" + assert parsed_response.app_message.card_message.media_message is not None + assert parsed_response.app_message.card_message.media_message.url == "https://update.example.com/image.jpg" + assert parsed_response.app_message.card_message.media_message.thumbnail_url == "https://update.example.com/thumb.jpg" + assert parsed_response.app_message.card_message.media_message.filename_override == "updated_image.jpg" + assert parsed_response.app_message.card_message.message_properties is not None + assert parsed_response.app_message.card_message.message_properties.whatsapp_header == "WhatsApp header text" + assert parsed_response.app_message.agent is not None + assert parsed_response.app_message.agent.display_name == "Updated Agent" + assert parsed_response.app_message.agent.type == "HUMAN_AGENT" + assert parsed_response.app_message.agent.picture_url == "https://update.example.com/agent_photo.jpg" + assert parsed_response.channel_identity is not None + assert parsed_response.channel_identity.app_id == "APP123456789ABCDEFGHIJK" + assert parsed_response.channel_identity.channel == "WHATSAPP" + assert parsed_response.channel_identity.identity == "+46701234567" + assert parsed_response.injected is True + assert parsed_response.sender_id == "SENDER123456789ABCDEFGHIJK" + assert parsed_response.processing_mode == "CONVERSATION" + + assert parsed_response.accept_time == datetime( + 2026, 1, 15, 17, 19, 12, 0, tzinfo=timezone.utc + ) From 07467032e9c778ec9d0136bf131f2b9565317b57 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Fri, 16 Jan 2026 18:01:28 +0100 Subject: [PATCH 2/2] solve PR comments --- .../clients/sinch_client_configuration.py | 2 +- .../messages/test_delete_message_endpoint.py | 2 +- .../messages/test_get_message_endpoint.py | 2 +- .../messages/test_update_message_endpoint.py | 32 +++++-------------- tests/unit/test_client.py | 2 +- tests/unit/test_configuration.py | 8 ++--- 6 files changed, 16 insertions(+), 32 deletions(-) diff --git a/sinch/core/clients/sinch_client_configuration.py b/sinch/core/clients/sinch_client_configuration.py index 756c150a..da0551f3 100644 --- a/sinch/core/clients/sinch_client_configuration.py +++ b/sinch/core/clients/sinch_client_configuration.py @@ -46,7 +46,7 @@ def __init__( self._voice_domain = "https://{}.api.sinch.com" self._voice_region = None self._conversation_region = conversation_region - self._conversation_domain = "https://{}.conversation.api.sinch.com/" + self._conversation_domain = "https://{}.conversation.api.sinch.com" self._sms_region = sms_region self._sms_region_with_service_plan_id = sms_region self._sms_domain = "https://zt.{}.sms.api.sinch.com" diff --git a/tests/unit/domains/conversation/v1/endpoints/messages/test_delete_message_endpoint.py b/tests/unit/domains/conversation/v1/endpoints/messages/test_delete_message_endpoint.py index 777cd8cc..6ed477b9 100644 --- a/tests/unit/domains/conversation/v1/endpoints/messages/test_delete_message_endpoint.py +++ b/tests/unit/domains/conversation/v1/endpoints/messages/test_delete_message_endpoint.py @@ -44,7 +44,7 @@ def test_build_url_expects_correct_url(endpoint, mock_sinch_client_conversation) """ assert ( endpoint.build_url(mock_sinch_client_conversation) - == "https://us.conversation.api.sinch.com//v1/projects/test_project_id/messages/01FC66621XXXXX119Z8PMV1QPQ" + == "https://us.conversation.api.sinch.com/v1/projects/test_project_id/messages/01FC66621XXXXX119Z8PMV1QPQ" ) diff --git a/tests/unit/domains/conversation/v1/endpoints/messages/test_get_message_endpoint.py b/tests/unit/domains/conversation/v1/endpoints/messages/test_get_message_endpoint.py index a9abdc25..59a1f3a6 100644 --- a/tests/unit/domains/conversation/v1/endpoints/messages/test_get_message_endpoint.py +++ b/tests/unit/domains/conversation/v1/endpoints/messages/test_get_message_endpoint.py @@ -162,7 +162,7 @@ def test_build_url_expects_correct_url(endpoint, mock_sinch_client_conversation) """ assert ( endpoint.build_url(mock_sinch_client_conversation) - == "https://us.conversation.api.sinch.com//v1/projects/test_project_id/messages/CAPY123456789ABCDEFGHIJKLMNOP" + == "https://us.conversation.api.sinch.com/v1/projects/test_project_id/messages/CAPY123456789ABCDEFGHIJKLMNOP" ) diff --git a/tests/unit/domains/conversation/v1/endpoints/messages/test_update_message_endpoint.py b/tests/unit/domains/conversation/v1/endpoints/messages/test_update_message_endpoint.py index 7912bb6b..9f64d833 100644 --- a/tests/unit/domains/conversation/v1/endpoints/messages/test_update_message_endpoint.py +++ b/tests/unit/domains/conversation/v1/endpoints/messages/test_update_message_endpoint.py @@ -164,50 +164,34 @@ def test_build_url_expects_correct_url(endpoint, mock_sinch_client_conversation) """Test that the URL is built correctly.""" assert ( endpoint.build_url(mock_sinch_client_conversation) - == "https://us.conversation.api.sinch.com//v1/projects/test_project_id/messages/UPDATE123456789ABCDEFGHIJKLMNOP" + == "https://us.conversation.api.sinch.com/v1/projects/test_project_id/messages/UPDATE123456789ABCDEFGHIJKLMNOP" ) -def test_messages_source_query_param_expects_parsed_params(): +def test_messages_source_query_param_expects_parsed_params(request_data): """ Test that the URL is built correctly with messages_source query parameter. metadata is from body application/json, so it should not be in query params. """ - request_data = UpdateMessageMetadataRequest( - message_id="UPDATE123456789ABCDEFGHIJKLMNOP", - metadata="updated_metadata_value", - messages_source="CONVERSATION_SOURCE" - ) + request_data.messages_source = "DISPATCH_SOURCE" endpoint = UpdateMessageMetadataEndpoint("test_project_id", request_data) query_params = endpoint.build_query_params() assert "metadata" not in query_params - assert query_params["messages_source"] == "CONVERSATION_SOURCE" + assert query_params["messages_source"] == "DISPATCH_SOURCE" -def test_request_body_expects_excludes_message_id(request_data): +def test_request_body_expects_excludes_message_id_and_query_params(request_data): """ - Test that message_id is excluded from request body. - """ - endpoint = UpdateMessageMetadataEndpoint("test_project_id", request_data) - body = json.loads(endpoint.request_body()) - - assert "message_id" not in body - assert "metadata" in body - assert body["metadata"] == "updated_metadata_value" - - -def test_request_body_expects_excludes_query_params(request_data): - """ - Test that messages_source is excluded from request body (it's a query param). + Test that message_id and messages_source are excluded from request body. + metadata should always be included in the request body. """ request_data.messages_source = "CONVERSATION_SOURCE" - endpoint = UpdateMessageMetadataEndpoint("test_project_id", request_data) body = json.loads(endpoint.request_body()) - assert "message_id" not in body assert "messages_source" not in body + assert "message_id" not in body assert "metadata" in body assert body["metadata"] == "updated_metadata_value" diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index a7ad97b9..de7efd87 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -50,7 +50,7 @@ def test_sinch_client_expects_to_be_initialized_with_conversation_region(): conversation_region="eu" ) assert sinch_client.configuration.conversation_region == "eu" - assert sinch_client.configuration.conversation_origin == "https://eu.conversation.api.sinch.com/" + assert sinch_client.configuration.conversation_origin == "https://eu.conversation.api.sinch.com" def test_sinch_client_expects_conversation_region_error_when_not_provided(): diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py index 2d92786c..571f9c5f 100644 --- a/tests/unit/test_configuration.py +++ b/tests/unit/test_configuration.py @@ -56,14 +56,14 @@ def test_set_sms_region_with_service_plan_id_property_and_check_that_sms_origin_ def test_set_conversation_region_property_expects_updated_conversation_origin(sinch_client_sync): """ Test that setting the conversation region property updates the conversation origin """ sinch_client_sync.configuration.conversation_region = "us" - assert sinch_client_sync.configuration.conversation_origin == "https://us.conversation.api.sinch.com/" + assert sinch_client_sync.configuration.conversation_origin == "https://us.conversation.api.sinch.com" def test_set_conversation_domain_property_expects_updated_conversation_origin(sinch_client_sync): """ Test that setting the conversation domain property updates the conversation origin """ sinch_client_sync.configuration.conversation_region = "eu" - sinch_client_sync.configuration.conversation_domain = "https://{}.test.conversation.api.sinch.com/" - assert sinch_client_sync.configuration.conversation_origin == "https://eu.test.conversation.api.sinch.com/" + sinch_client_sync.configuration.conversation_domain = "https://{}.test.conversation.api.sinch.com" + assert sinch_client_sync.configuration.conversation_origin == "https://eu.test.conversation.api.sinch.com" def test_if_logger_name_was_preserved_correctly(sinch_client_sync): @@ -225,4 +225,4 @@ def test_configuration_expects_get_conversation_origin_with_region(sinch_client_ actual_origin = client_configuration.get_conversation_origin() assert actual_origin == expected_origin - assert actual_origin == "https://us.conversation.api.sinch.com/" + assert actual_origin == "https://us.conversation.api.sinch.com"