Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import json
import pytest
from sinch.core.models.http_response import HTTPResponse
from sinch.domains.conversation.api.v1.internal import SendMessageEndpoint
from sinch.domains.conversation.api.v1.exceptions import ConversationException
from sinch.domains.conversation.models.v1.messages.internal.request import (
SendMessageRequest,
SendMessageRequestBody,
)
from sinch.domains.conversation.models.v1.messages.internal.request.recipient import (
Recipient,
)
from sinch.domains.conversation.models.v1.messages.categories.text import TextMessage
from sinch.domains.conversation.models.v1.messages.response.types import SendMessageResponse


@pytest.fixture
def request_data():
return SendMessageRequest(
app_id="my app ID",
recipient=Recipient(contact_id="my contact ID"),
message=SendMessageRequestBody(
text_message=TextMessage(text="This is a text message.")
),
)


@pytest.fixture
def mock_send_message_response():
"""Mock response for SendMessageResponse."""
return HTTPResponse(
status_code=200,
body={"message_id": "01FC66621XXXXX119Z8PMV1QPQ"},
headers={"Content-Type": "application/json"},
)


@pytest.fixture
def mock_error_response():
"""Mock error response for send message endpoint."""
return HTTPResponse(
status_code=400,
body={
"error": {
"code": 400,
"message": "Invalid argument",
"status": "INVALID_ARGUMENT"
}
},
headers={"Content-Type": "application/json"},
)


@pytest.fixture
def endpoint(request_data):
return SendMessageEndpoint("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:send"
)


def test_request_body_expects_valid_json_with_app_id_recipient_message(request_data):
"""Test that the endpoint produces a JSON body with app_id, recipient, and message."""
endpoint = SendMessageEndpoint("test_project_id", request_data)
body = json.loads(endpoint.request_body())

assert body["app_id"] == "my app ID"
assert body["recipient"]["contact_id"] == "my contact ID"
assert "text_message" in body["message"]
assert "project_id" not in body


def test_handle_response_expects_send_message_response(endpoint, mock_send_message_response):
"""Test that SendMessageResponse is handled correctly."""
parsed_response = endpoint.handle_response(mock_send_message_response)

assert isinstance(parsed_response, SendMessageResponse)
assert parsed_response.message_id == "01FC66621XXXXX119Z8PMV1QPQ"


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 == 400
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import pytest
from pydantic import ValidationError
from sinch.domains.conversation.models.v1.messages.categories.text import TextMessage
from sinch.domains.conversation.models.v1.messages.internal.request import (
Recipient,
SendMessageRequestBody,
SendMessageRequest,
)


def test_send_message_request_expects_parsed_input():
"""
Test that the model parses input correctly.
"""
request = SendMessageRequest(
app_id="my-app-id",
recipient=Recipient(contact_id="my-contact-id"),
message=SendMessageRequestBody(text_message=TextMessage(text="Hello")),
)

assert request.app_id == "my-app-id"
assert request.recipient.contact_id == "my-contact-id"
assert request.message.text_message is not None
assert request.message.text_message.text == "Hello"


@pytest.mark.parametrize("processing_strategy", ["DEFAULT", "DISPATCH_ONLY"])
def test_send_message_request_expects_accepts_processing_strategy(processing_strategy):
"""
Test that the model accepts processing_strategy with different values.
"""
request = SendMessageRequest(
app_id="my-app-id",
recipient=Recipient(contact_id="my-contact-id"),
message=SendMessageRequestBody(text_message=TextMessage(text="Hello")),
processing_strategy=processing_strategy,
)

assert request.processing_strategy == processing_strategy


@pytest.mark.parametrize("ttl_input,expected_serialized", [(10, "10s"), ("10s", "10s"), ("10", "10s"), (None, None)])
def test_send_message_request_expects_ttl_serialized_to_backend(ttl_input, expected_serialized):
"""
Test that ttl is serialized as "10s" when sent to the backend (int/str normalized to string with 's' suffix).
"""
request = SendMessageRequest(
app_id="my-app-id",
recipient=Recipient(contact_id="my-contact-id"),
message=SendMessageRequestBody(text_message=TextMessage(text="Hello")),
ttl=ttl_input,
)

payload = request.model_dump(mode="json", exclude_none=True)
if expected_serialized is None:
assert "ttl" not in payload
else:
assert payload["ttl"] == expected_serialized


def test_send_message_request_expects_validation_error_for_missing_app_id():
"""
Test that the model raises a ValidationError when app_id field is missing.
"""
data = {
"recipient": Recipient(contact_id="my-contact-id"),
"message": SendMessageRequestBody(text_message=TextMessage(text="Hello")),
}

with pytest.raises(ValidationError) as excinfo:
SendMessageRequest(**data)

error_message = str(excinfo.value)

assert "field required" in error_message.casefold()
assert "app_id" in error_message


def test_send_message_request_expects_validation_error_for_missing_recipient():
"""
Test that the model raises a ValidationError when recipient field is missing.
"""
data = {
"app_id": "my-app-id",
"message": SendMessageRequestBody(text_message=TextMessage(text="Hello")),
}

with pytest.raises(ValidationError) as excinfo:
SendMessageRequest(**data)

error_message = str(excinfo.value)

assert "field required" in error_message.casefold()
assert "recipient" in error_message
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import pytest
from sinch.domains.conversation.models.v1.messages.categories.card.card_message import (
CardMessage,
)
from sinch.domains.conversation.models.v1.messages.categories.carousel.carousel_message import (
CarouselMessage,
)
from sinch.domains.conversation.models.v1.messages.categories.choice.choice_message import (
ChoiceMessage,
)
from sinch.domains.conversation.models.v1.messages.categories.choice.choice_options import (
TextChoiceMessage,
)
from sinch.domains.conversation.models.v1.messages.categories.location.location_message import (
LocationMessage,
)
from sinch.domains.conversation.models.v1.messages.categories.media import (
MediaProperties,
)
from sinch.domains.conversation.models.v1.messages.categories.template import (
TemplateMessage,
TemplateReferenceOmniChannel,
)
from sinch.domains.conversation.models.v1.messages.categories.text import (
TextMessage,
)
from sinch.domains.conversation.models.v1.messages.internal.request import (
SendMessageRequestBody,
)
from sinch.domains.conversation.models.v1.messages.shared.coordinates import (
Coordinates,
)


def test_send_message_request_body_expects_accepts_text_message():
"""
Test that the model accepts text_message with valid content.
"""
body = SendMessageRequestBody(text_message=TextMessage(text="Test message content"))

assert body.text_message.text == "Test message content"


def test_send_message_request_body_expects_accepts_card_message():
"""
Test that the model accepts card_message.
"""
body = SendMessageRequestBody(card_message=CardMessage(title="Card title"))

assert body.card_message is not None
assert body.card_message.title == "Card title"


def test_send_message_request_body_expects_accepts_carousel_message():
"""
Test that the model accepts carousel_message with a list of cards.
"""
body = SendMessageRequestBody(
carousel_message=CarouselMessage(cards=[CardMessage(title="Card 1")])
)

assert body.carousel_message is not None
assert len(body.carousel_message.cards) == 1
assert body.carousel_message.cards[0].title == "Card 1"


def test_send_message_request_body_expects_accepts_choice_message():
"""
Test that the model accepts choice_message with choices.
"""
body = SendMessageRequestBody(
choice_message=ChoiceMessage(
choices=[TextChoiceMessage(text_message=TextMessage(text="Option 1"))]
)
)

assert body.choice_message is not None
assert len(body.choice_message.choices) == 1
assert body.choice_message.choices[0].text_message.text == "Option 1"


def test_send_message_request_body_expects_accepts_location_message():
"""
Test that the model accepts location_message with coordinates and title.
"""
body = SendMessageRequestBody(
location_message=LocationMessage(
coordinates=Coordinates(latitude=59.3293, longitude=18.0686),
title="Stockholm",
)
)

assert body.location_message is not None
assert body.location_message.title == "Stockholm"
assert body.location_message.coordinates.latitude == 59.3293
assert body.location_message.coordinates.longitude == 18.0686


def test_send_message_request_body_expects_accepts_media_message():
"""
Test that the model accepts media_message with url.
"""
body = SendMessageRequestBody(
media_message=MediaProperties(url="https://example.com/image.jpg")
)

assert body.media_message is not None
assert body.media_message.url == "https://example.com/image.jpg"


def test_send_message_request_body_expects_accepts_template_message():
"""
Test that the model accepts template_message with omni_template.
"""
body = SendMessageRequestBody(
template_message=TemplateMessage(
omni_template=TemplateReferenceOmniChannel(
template_id="tpl_123", version="latest"
)
)
)

assert body.template_message is not None
assert body.template_message.omni_template is not None
assert body.template_message.omni_template.template_id == "tpl_123"
assert body.template_message.omni_template.version == "latest"


def test_send_message_request_body_expects_accepts_choice_with_one_message_key():
"""
Parsing from dict: each choice with exactly one message-type key is valid.
Choices array can include Call, Location, Text, URL, Calendar, Request location
(number limited to 10 per spec).
"""
choices = [
{"text_message": {"text": "Option 1"}},
{"call_message": {"title": "Call us", "phone_number": "+46732000000"}},
{"url_message": {"title": "Website", "url": "https://example.com"}},
{
"location_message": {
"title": "Show map",
"coordinates": {"latitude": 59.33, "longitude": 18.07},
}
},
{
"share_location_message": {
"title": "Share location",
"fallback_url": "https://example.com",
}
},
]
body = SendMessageRequestBody(
choice_message=ChoiceMessage(choices=choices)
)
assert body.choice_message is not None
assert len(body.choice_message.choices) == 5
assert body.choice_message.choices[0].text_message.text == "Option 1"
assert body.choice_message.choices[1].call_message.phone_number == "+46732000000"
assert body.choice_message.choices[2].url_message.url == "https://example.com"
assert body.choice_message.choices[3].location_message.title == "Show map"
assert (
body.choice_message.choices[4].share_location_message.title
== "Share location"
)


def test_send_message_request_body_expects_rejects_choice_with_zero_message_keys():
"""
Parsing from dict: choice with no message-type key raises.
"""
with pytest.raises(ValueError, match="exactly one of"):
SendMessageRequestBody(
choice_message=ChoiceMessage(choices=[{"postback_data": "x"}])
)


def test_send_message_request_body_expects_rejects_choice_with_two_message_keys():
"""
Parsing from dict: choice with two message-type keys raises.
"""
with pytest.raises(ValueError, match="exactly one of"):
SendMessageRequestBody(
choice_message=ChoiceMessage(
choices=[
{
"text_message": {"text": "A"},
"call_message": {"title": "Call", "phone_number": "1"},
}
]
)
)
Loading