Skip to content

Commit 8da74b9

Browse files
authored
DEVEXP-794: Conversation Messages - Send Unit Tests (#117)
1 parent f84bb18 commit 8da74b9

5 files changed

Lines changed: 561 additions & 0 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import json
2+
import pytest
3+
from sinch.core.models.http_response import HTTPResponse
4+
from sinch.domains.conversation.api.v1.internal import SendMessageEndpoint
5+
from sinch.domains.conversation.api.v1.exceptions import ConversationException
6+
from sinch.domains.conversation.models.v1.messages.internal.request import (
7+
SendMessageRequest,
8+
SendMessageRequestBody,
9+
)
10+
from sinch.domains.conversation.models.v1.messages.internal.request.recipient import (
11+
Recipient,
12+
)
13+
from sinch.domains.conversation.models.v1.messages.categories.text import TextMessage
14+
from sinch.domains.conversation.models.v1.messages.response.types import SendMessageResponse
15+
16+
17+
@pytest.fixture
18+
def request_data():
19+
return SendMessageRequest(
20+
app_id="my app ID",
21+
recipient=Recipient(contact_id="my contact ID"),
22+
message=SendMessageRequestBody(
23+
text_message=TextMessage(text="This is a text message.")
24+
),
25+
)
26+
27+
28+
@pytest.fixture
29+
def mock_send_message_response():
30+
"""Mock response for SendMessageResponse."""
31+
return HTTPResponse(
32+
status_code=200,
33+
body={"message_id": "01FC66621XXXXX119Z8PMV1QPQ"},
34+
headers={"Content-Type": "application/json"},
35+
)
36+
37+
38+
@pytest.fixture
39+
def mock_error_response():
40+
"""Mock error response for send message endpoint."""
41+
return HTTPResponse(
42+
status_code=400,
43+
body={
44+
"error": {
45+
"code": 400,
46+
"message": "Invalid argument",
47+
"status": "INVALID_ARGUMENT"
48+
}
49+
},
50+
headers={"Content-Type": "application/json"},
51+
)
52+
53+
54+
@pytest.fixture
55+
def endpoint(request_data):
56+
return SendMessageEndpoint("test_project_id", request_data)
57+
58+
59+
def test_build_url_expects_correct_url(endpoint, mock_sinch_client_conversation):
60+
"""Test that the URL is built correctly."""
61+
assert (
62+
endpoint.build_url(mock_sinch_client_conversation)
63+
== "https://us.conversation.api.sinch.com/v1/projects/test_project_id/messages:send"
64+
)
65+
66+
67+
def test_request_body_expects_valid_json_with_app_id_recipient_message(request_data):
68+
"""Test that the endpoint produces a JSON body with app_id, recipient, and message."""
69+
endpoint = SendMessageEndpoint("test_project_id", request_data)
70+
body = json.loads(endpoint.request_body())
71+
72+
assert body["app_id"] == "my app ID"
73+
assert body["recipient"]["contact_id"] == "my contact ID"
74+
assert "text_message" in body["message"]
75+
assert "project_id" not in body
76+
77+
78+
def test_handle_response_expects_send_message_response(endpoint, mock_send_message_response):
79+
"""Test that SendMessageResponse is handled correctly."""
80+
parsed_response = endpoint.handle_response(mock_send_message_response)
81+
82+
assert isinstance(parsed_response, SendMessageResponse)
83+
assert parsed_response.message_id == "01FC66621XXXXX119Z8PMV1QPQ"
84+
85+
86+
def test_handle_response_expects_conversation_exception_on_error(
87+
endpoint, mock_error_response
88+
):
89+
"""Test that ConversationException is raised when server returns an error."""
90+
with pytest.raises(ConversationException) as exc_info:
91+
endpoint.handle_response(mock_error_response)
92+
93+
assert exc_info.value.is_from_server is True
94+
assert exc_info.value.http_response.status_code == 400
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import pytest
2+
from pydantic import ValidationError
3+
from sinch.domains.conversation.models.v1.messages.categories.text import TextMessage
4+
from sinch.domains.conversation.models.v1.messages.internal.request import (
5+
Recipient,
6+
SendMessageRequestBody,
7+
SendMessageRequest,
8+
)
9+
10+
11+
def test_send_message_request_expects_parsed_input():
12+
"""
13+
Test that the model parses input correctly.
14+
"""
15+
request = SendMessageRequest(
16+
app_id="my-app-id",
17+
recipient=Recipient(contact_id="my-contact-id"),
18+
message=SendMessageRequestBody(text_message=TextMessage(text="Hello")),
19+
)
20+
21+
assert request.app_id == "my-app-id"
22+
assert request.recipient.contact_id == "my-contact-id"
23+
assert request.message.text_message is not None
24+
assert request.message.text_message.text == "Hello"
25+
26+
27+
@pytest.mark.parametrize("processing_strategy", ["DEFAULT", "DISPATCH_ONLY"])
28+
def test_send_message_request_expects_accepts_processing_strategy(processing_strategy):
29+
"""
30+
Test that the model accepts processing_strategy with different values.
31+
"""
32+
request = SendMessageRequest(
33+
app_id="my-app-id",
34+
recipient=Recipient(contact_id="my-contact-id"),
35+
message=SendMessageRequestBody(text_message=TextMessage(text="Hello")),
36+
processing_strategy=processing_strategy,
37+
)
38+
39+
assert request.processing_strategy == processing_strategy
40+
41+
42+
@pytest.mark.parametrize("ttl_input,expected_serialized", [(10, "10s"), ("10s", "10s"), ("10", "10s"), (None, None)])
43+
def test_send_message_request_expects_ttl_serialized_to_backend(ttl_input, expected_serialized):
44+
"""
45+
Test that ttl is serialized as "10s" when sent to the backend (int/str normalized to string with 's' suffix).
46+
"""
47+
request = SendMessageRequest(
48+
app_id="my-app-id",
49+
recipient=Recipient(contact_id="my-contact-id"),
50+
message=SendMessageRequestBody(text_message=TextMessage(text="Hello")),
51+
ttl=ttl_input,
52+
)
53+
54+
payload = request.model_dump(mode="json", exclude_none=True)
55+
if expected_serialized is None:
56+
assert "ttl" not in payload
57+
else:
58+
assert payload["ttl"] == expected_serialized
59+
60+
61+
def test_send_message_request_expects_validation_error_for_missing_app_id():
62+
"""
63+
Test that the model raises a ValidationError when app_id field is missing.
64+
"""
65+
data = {
66+
"recipient": Recipient(contact_id="my-contact-id"),
67+
"message": SendMessageRequestBody(text_message=TextMessage(text="Hello")),
68+
}
69+
70+
with pytest.raises(ValidationError) as excinfo:
71+
SendMessageRequest(**data)
72+
73+
error_message = str(excinfo.value)
74+
75+
assert "field required" in error_message.casefold()
76+
assert "app_id" in error_message
77+
78+
79+
def test_send_message_request_expects_validation_error_for_missing_recipient():
80+
"""
81+
Test that the model raises a ValidationError when recipient field is missing.
82+
"""
83+
data = {
84+
"app_id": "my-app-id",
85+
"message": SendMessageRequestBody(text_message=TextMessage(text="Hello")),
86+
}
87+
88+
with pytest.raises(ValidationError) as excinfo:
89+
SendMessageRequest(**data)
90+
91+
error_message = str(excinfo.value)
92+
93+
assert "field required" in error_message.casefold()
94+
assert "recipient" in error_message
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import pytest
2+
from sinch.domains.conversation.models.v1.messages.categories.card.card_message import (
3+
CardMessage,
4+
)
5+
from sinch.domains.conversation.models.v1.messages.categories.carousel.carousel_message import (
6+
CarouselMessage,
7+
)
8+
from sinch.domains.conversation.models.v1.messages.categories.choice.choice_message import (
9+
ChoiceMessage,
10+
)
11+
from sinch.domains.conversation.models.v1.messages.categories.choice.choice_options import (
12+
TextChoiceMessage,
13+
)
14+
from sinch.domains.conversation.models.v1.messages.categories.location.location_message import (
15+
LocationMessage,
16+
)
17+
from sinch.domains.conversation.models.v1.messages.categories.media import (
18+
MediaProperties,
19+
)
20+
from sinch.domains.conversation.models.v1.messages.categories.template import (
21+
TemplateMessage,
22+
TemplateReferenceOmniChannel,
23+
)
24+
from sinch.domains.conversation.models.v1.messages.categories.text import (
25+
TextMessage,
26+
)
27+
from sinch.domains.conversation.models.v1.messages.internal.request import (
28+
SendMessageRequestBody,
29+
)
30+
from sinch.domains.conversation.models.v1.messages.shared.coordinates import (
31+
Coordinates,
32+
)
33+
34+
35+
def test_send_message_request_body_expects_accepts_text_message():
36+
"""
37+
Test that the model accepts text_message with valid content.
38+
"""
39+
body = SendMessageRequestBody(text_message=TextMessage(text="Test message content"))
40+
41+
assert body.text_message.text == "Test message content"
42+
43+
44+
def test_send_message_request_body_expects_accepts_card_message():
45+
"""
46+
Test that the model accepts card_message.
47+
"""
48+
body = SendMessageRequestBody(card_message=CardMessage(title="Card title"))
49+
50+
assert body.card_message is not None
51+
assert body.card_message.title == "Card title"
52+
53+
54+
def test_send_message_request_body_expects_accepts_carousel_message():
55+
"""
56+
Test that the model accepts carousel_message with a list of cards.
57+
"""
58+
body = SendMessageRequestBody(
59+
carousel_message=CarouselMessage(cards=[CardMessage(title="Card 1")])
60+
)
61+
62+
assert body.carousel_message is not None
63+
assert len(body.carousel_message.cards) == 1
64+
assert body.carousel_message.cards[0].title == "Card 1"
65+
66+
67+
def test_send_message_request_body_expects_accepts_choice_message():
68+
"""
69+
Test that the model accepts choice_message with choices.
70+
"""
71+
body = SendMessageRequestBody(
72+
choice_message=ChoiceMessage(
73+
choices=[TextChoiceMessage(text_message=TextMessage(text="Option 1"))]
74+
)
75+
)
76+
77+
assert body.choice_message is not None
78+
assert len(body.choice_message.choices) == 1
79+
assert body.choice_message.choices[0].text_message.text == "Option 1"
80+
81+
82+
def test_send_message_request_body_expects_accepts_location_message():
83+
"""
84+
Test that the model accepts location_message with coordinates and title.
85+
"""
86+
body = SendMessageRequestBody(
87+
location_message=LocationMessage(
88+
coordinates=Coordinates(latitude=59.3293, longitude=18.0686),
89+
title="Stockholm",
90+
)
91+
)
92+
93+
assert body.location_message is not None
94+
assert body.location_message.title == "Stockholm"
95+
assert body.location_message.coordinates.latitude == 59.3293
96+
assert body.location_message.coordinates.longitude == 18.0686
97+
98+
99+
def test_send_message_request_body_expects_accepts_media_message():
100+
"""
101+
Test that the model accepts media_message with url.
102+
"""
103+
body = SendMessageRequestBody(
104+
media_message=MediaProperties(url="https://example.com/image.jpg")
105+
)
106+
107+
assert body.media_message is not None
108+
assert body.media_message.url == "https://example.com/image.jpg"
109+
110+
111+
def test_send_message_request_body_expects_accepts_template_message():
112+
"""
113+
Test that the model accepts template_message with omni_template.
114+
"""
115+
body = SendMessageRequestBody(
116+
template_message=TemplateMessage(
117+
omni_template=TemplateReferenceOmniChannel(
118+
template_id="tpl_123", version="latest"
119+
)
120+
)
121+
)
122+
123+
assert body.template_message is not None
124+
assert body.template_message.omni_template is not None
125+
assert body.template_message.omni_template.template_id == "tpl_123"
126+
assert body.template_message.omni_template.version == "latest"
127+
128+
129+
def test_send_message_request_body_expects_accepts_choice_with_one_message_key():
130+
"""
131+
Parsing from dict: each choice with exactly one message-type key is valid.
132+
Choices array can include Call, Location, Text, URL, Calendar, Request location
133+
(number limited to 10 per spec).
134+
"""
135+
choices = [
136+
{"text_message": {"text": "Option 1"}},
137+
{"call_message": {"title": "Call us", "phone_number": "+46732000000"}},
138+
{"url_message": {"title": "Website", "url": "https://example.com"}},
139+
{
140+
"location_message": {
141+
"title": "Show map",
142+
"coordinates": {"latitude": 59.33, "longitude": 18.07},
143+
}
144+
},
145+
{
146+
"share_location_message": {
147+
"title": "Share location",
148+
"fallback_url": "https://example.com",
149+
}
150+
},
151+
]
152+
body = SendMessageRequestBody(
153+
choice_message=ChoiceMessage(choices=choices)
154+
)
155+
assert body.choice_message is not None
156+
assert len(body.choice_message.choices) == 5
157+
assert body.choice_message.choices[0].text_message.text == "Option 1"
158+
assert body.choice_message.choices[1].call_message.phone_number == "+46732000000"
159+
assert body.choice_message.choices[2].url_message.url == "https://example.com"
160+
assert body.choice_message.choices[3].location_message.title == "Show map"
161+
assert (
162+
body.choice_message.choices[4].share_location_message.title
163+
== "Share location"
164+
)
165+
166+
167+
def test_send_message_request_body_expects_rejects_choice_with_zero_message_keys():
168+
"""
169+
Parsing from dict: choice with no message-type key raises.
170+
"""
171+
with pytest.raises(ValueError, match="exactly one of"):
172+
SendMessageRequestBody(
173+
choice_message=ChoiceMessage(choices=[{"postback_data": "x"}])
174+
)
175+
176+
177+
def test_send_message_request_body_expects_rejects_choice_with_two_message_keys():
178+
"""
179+
Parsing from dict: choice with two message-type keys raises.
180+
"""
181+
with pytest.raises(ValueError, match="exactly one of"):
182+
SendMessageRequestBody(
183+
choice_message=ChoiceMessage(
184+
choices=[
185+
{
186+
"text_message": {"text": "A"},
187+
"call_message": {"title": "Call", "phone_number": "1"},
188+
}
189+
]
190+
)
191+
)

0 commit comments

Comments
 (0)