From cd21bde7df3dd2c33f229b6a6b0e9ef159159c34 Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 11 Mar 2026 11:17:29 +0000 Subject: [PATCH 01/48] DEVX-10006: Adding tests for RCS Suggestion Base Model --- messages/tests/test_rcs_models.py | 48 +++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 4114bcad..9bd05a5f 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -1,3 +1,4 @@ +import pytest from vonage_messages.models import ( RcsCustom, RcsFile, @@ -147,3 +148,50 @@ def test_create_rcs_custom(): } assert rcs_model.model_dump(by_alias=True, exclude_none=True) == rcs_dict + +@pytest.mark.skip(reason="not yet implemented") +def test_rcs_suggestion_base(): + suggestion = RcsSuggestionBase( + text='Reply', + postback_data='postback-data', + ) + suggestion_dict = { + 'text': 'Reply', + 'postback_data': 'postback-data', + } + + assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict + +@pytest.mark.skip(reason="not yet implemented") +def test_rcs_suggestion_base_without_text(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionBase( + postback_data='postback-data', + ) + assert "field required" in err.value.errors[0]['msg'] + +@pytest.mark.skip(reason="not yet implemented") +def test_rcs_suggestion_base_without_postback_data(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionBase( + text='Reply', + ) + assert "field required" in err.value.errors[0]['msg'] + +@pytest.mark.skip(reason="not yet implemented") +def test_rcs_suggestion_base_with_text_too_short(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionBase( + text='', + postback_data='postback-data', + ) + assert "ensure this value has at least 1 characters" in err.value.errors[0]['msg'] + +@pytest.mark.skip(reason="not yet implemented") +def test_rcs_suggestion_base_with_text_too_long(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionBase( + text='A' * 25 + 'B', + postback_data='postback-data', + ) + assert "ensure this value has at most 25 characters" in err.value.errors[0]['msg'] \ No newline at end of file From f1023ab5c6b3a84aff1835cd950c44da767bd91a Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 11 Mar 2026 11:46:17 +0000 Subject: [PATCH 02/48] DEVX-10006: Implementing RCS Suggestion Base model --- .../src/vonage_messages/models/__init__.py | 2 +- messages/src/vonage_messages/models/rcs.py | 12 +++++++++++ messages/tests/test_rcs_models.py | 20 ++++++++++--------- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index bbd6d65d..87dce862 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -10,7 +10,7 @@ MessengerVideo, ) from .mms import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo -from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo +from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo, RcsSuggestionBase from .sms import Sms, SmsOptions from .viber import ( ViberAction, diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index 6ed1e7b7..bd9f2c04 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -17,6 +17,18 @@ class RcsResource(BaseModel): url: str +class RcsSuggestionBase(BaseModel): + """Model for a suggestion in an RCS message. + + Args: + text (str): The text to display on the suggestion chip. + postback_data (str): The data that will be sent via the Inbound Message webhook when the suggestion is selected. + """ + + text: str = Field(..., min_length=1, max_length=25) + postback_data: str + + class BaseRcs(BaseMessage): """Model for a base RCS message. diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 9bd05a5f..7501db77 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -1,4 +1,5 @@ import pytest +from pydantic import ValidationError from vonage_messages.models import ( RcsCustom, RcsFile, @@ -6,6 +7,7 @@ RcsResource, RcsText, RcsVideo, + RcsSuggestionBase, ) @@ -149,7 +151,7 @@ def test_create_rcs_custom(): assert rcs_model.model_dump(by_alias=True, exclude_none=True) == rcs_dict -@pytest.mark.skip(reason="not yet implemented") + def test_rcs_suggestion_base(): suggestion = RcsSuggestionBase( text='Reply', @@ -162,36 +164,36 @@ def test_rcs_suggestion_base(): assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict -@pytest.mark.skip(reason="not yet implemented") + def test_rcs_suggestion_base_without_text(): with pytest.raises(ValidationError) as err: suggestion = RcsSuggestionBase( postback_data='postback-data', ) - assert "field required" in err.value.errors[0]['msg'] + assert "Field required" in str(err.value) + -@pytest.mark.skip(reason="not yet implemented") def test_rcs_suggestion_base_without_postback_data(): with pytest.raises(ValidationError) as err: suggestion = RcsSuggestionBase( text='Reply', ) - assert "field required" in err.value.errors[0]['msg'] + assert "Field required" in str(err.value) + -@pytest.mark.skip(reason="not yet implemented") def test_rcs_suggestion_base_with_text_too_short(): with pytest.raises(ValidationError) as err: suggestion = RcsSuggestionBase( text='', postback_data='postback-data', ) - assert "ensure this value has at least 1 characters" in err.value.errors[0]['msg'] + assert "String should have at least 1 character" in str(err.value) + -@pytest.mark.skip(reason="not yet implemented") def test_rcs_suggestion_base_with_text_too_long(): with pytest.raises(ValidationError) as err: suggestion = RcsSuggestionBase( text='A' * 25 + 'B', postback_data='postback-data', ) - assert "ensure this value has at most 25 characters" in err.value.errors[0]['msg'] \ No newline at end of file + assert "String should have at most 25 characters" in str(err.value) From 3acc1eb1fbff46b33ff918d52c1f38ad26d98f36 Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 11 Mar 2026 11:47:01 +0000 Subject: [PATCH 03/48] DEVX-10006: Adding RCS Suggestion Reply test --- messages/tests/test_rcs_models.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 7501db77..5eee8a07 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -197,3 +197,16 @@ def test_rcs_suggestion_base_with_text_too_long(): postback_data='postback-data', ) assert "String should have at most 25 characters" in str(err.value) + +def test_rcs_suggestion_reply(): + suggestion = RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ) + suggestion_dict = { + 'type': 'reply', + 'text': 'Reply', + 'postback_data': 'postback-data', + } + + assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict \ No newline at end of file From 88e23b114088677af5135c73921d98539ffce8e9 Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 11 Mar 2026 11:56:40 +0000 Subject: [PATCH 04/48] DEVX-10006: Implementing RCS Suggested Reply model --- messages/src/vonage_messages/models/__init__.py | 2 +- messages/src/vonage_messages/models/enums.py | 6 ++++++ messages/src/vonage_messages/models/rcs.py | 13 ++++++++++++- messages/tests/test_rcs_models.py | 1 + 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index 87dce862..412f7181 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -10,7 +10,7 @@ MessengerVideo, ) from .mms import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo -from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo, RcsSuggestionBase +from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo, RcsSuggestionBase, RcsSuggestionReply from .sms import Sms, SmsOptions from .viber import ( ViberAction, diff --git a/messages/src/vonage_messages/models/enums.py b/messages/src/vonage_messages/models/enums.py index f0bb501a..4a1e4386 100644 --- a/messages/src/vonage_messages/models/enums.py +++ b/messages/src/vonage_messages/models/enums.py @@ -37,3 +37,9 @@ class EncodingType(str, Enum): TEXT = 'text' UNICODE = 'unicode' AUTO = 'auto' + + +class SuggestionType(str, Enum): + """The type of RCS suggestion.""" + + REPLY = 'reply' \ No newline at end of file diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index bd9f2c04..d6762bbf 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -4,7 +4,7 @@ from vonage_utils.types import PhoneNumber from .base_message import BaseMessage -from .enums import ChannelType, MessageType +from .enums import ChannelType, MessageType, SuggestionType class RcsResource(BaseModel): @@ -29,6 +29,17 @@ class RcsSuggestionBase(BaseModel): postback_data: str +class RcsSuggestionReply(RcsSuggestionBase): + """Model for a reply suggestion in an RCS message. + + Args: + text (str): The text to display on the suggestion chip. + postback_data (str): The data that will be sent via the Inbound Message webhook when the suggestion is selected. + """ + + type_: SuggestionType = Field(SuggestionType.REPLY, serialization_alias='type') + + class BaseRcs(BaseMessage): """Model for a base RCS message. diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 5eee8a07..e19b73d4 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -8,6 +8,7 @@ RcsText, RcsVideo, RcsSuggestionBase, + RcsSuggestionReply, ) From 267709ad488a63c972056e263f1157e491d992c1 Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 11 Mar 2026 12:46:27 +0000 Subject: [PATCH 05/48] DEVX-10006: Adding tests for RCS suggestion action dial --- messages/tests/test_rcs_models.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index e19b73d4..3c26395c 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -210,4 +210,31 @@ def test_rcs_suggestion_reply(): 'postback_data': 'postback-data', } - assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict \ No newline at end of file + assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict + + +@pytest.mark.skip(reason="Not yet implemented.") +def test_rcs_suggestion_dial(): + suggestion = RcsSuggestionActionDial( + text='Call us', + postback_data='postback-data', + phone_number='447900000000', + ) + suggestion_dict = { + 'type': 'dial', + 'text': 'Call us', + 'postback_data': 'postback-data', + 'phone_number': '447900000000', + } + + assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict + + +@pytest.mark.skip(reason="Not yet implemented.") +def test_rcs_suggestion_action_dial_without_phone_number(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionDial( + text='Call us', + postback_data='postback-data', + ) + assert "Field required" in str(err.value) From 563cc672b169e88fc4f4e82ecbd6562a851e8483 Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 11 Mar 2026 12:56:57 +0000 Subject: [PATCH 06/48] DEVX-10006: Implementing RCS suggestion action dial --- messages/src/vonage_messages/models/__init__.py | 2 +- messages/src/vonage_messages/models/enums.py | 3 ++- messages/src/vonage_messages/models/rcs.py | 12 ++++++++++++ messages/tests/test_rcs_models.py | 3 +-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index 412f7181..8209ee61 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -10,7 +10,7 @@ MessengerVideo, ) from .mms import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo -from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo, RcsSuggestionBase, RcsSuggestionReply +from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo, RcsSuggestionBase, RcsSuggestionReply, RcsSuggestionActionDial from .sms import Sms, SmsOptions from .viber import ( ViberAction, diff --git a/messages/src/vonage_messages/models/enums.py b/messages/src/vonage_messages/models/enums.py index 4a1e4386..4f86e179 100644 --- a/messages/src/vonage_messages/models/enums.py +++ b/messages/src/vonage_messages/models/enums.py @@ -42,4 +42,5 @@ class EncodingType(str, Enum): class SuggestionType(str, Enum): """The type of RCS suggestion.""" - REPLY = 'reply' \ No newline at end of file + REPLY = 'reply' + DIAL = 'dial' \ No newline at end of file diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index d6762bbf..3cb33b15 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -40,6 +40,18 @@ class RcsSuggestionReply(RcsSuggestionBase): type_: SuggestionType = Field(SuggestionType.REPLY, serialization_alias='type') +class RcsSuggestionActionDial(RcsSuggestionBase): + """Model for a dial action suggestion in an RCS message. + + Args: + text (str): The text to display on the suggestion chip. + postback_data (str): The data that will be sent via the Inbound Message webhook when the suggestion is selected. + phone_number (str): The phone number to dial when the suggestion is selected. In E.164 format without the leading plus sign. + """ + + type_: SuggestionType = Field(SuggestionType.DIAL, serialization_alias='type') + phone_number: PhoneNumber + class BaseRcs(BaseMessage): """Model for a base RCS message. diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 3c26395c..f1ba6ff9 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -9,6 +9,7 @@ RcsVideo, RcsSuggestionBase, RcsSuggestionReply, + RcsSuggestionActionDial, ) @@ -213,7 +214,6 @@ def test_rcs_suggestion_reply(): assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict -@pytest.mark.skip(reason="Not yet implemented.") def test_rcs_suggestion_dial(): suggestion = RcsSuggestionActionDial( text='Call us', @@ -230,7 +230,6 @@ def test_rcs_suggestion_dial(): assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict -@pytest.mark.skip(reason="Not yet implemented.") def test_rcs_suggestion_action_dial_without_phone_number(): with pytest.raises(ValidationError) as err: suggestion = RcsSuggestionActionDial( From 1f4ccdb5145e9f9b60281764624edbbc14870c08 Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 11 Mar 2026 14:55:27 +0000 Subject: [PATCH 07/48] DEVX-10006: Adding tests and implementation for RCS view location suggestion action --- .../src/vonage_messages/models/__init__.py | 2 +- messages/src/vonage_messages/models/enums.py | 3 +- messages/src/vonage_messages/models/rcs.py | 20 +++++++ messages/tests/test_rcs_models.py | 58 +++++++++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index 8209ee61..de815a07 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -10,7 +10,7 @@ MessengerVideo, ) from .mms import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo -from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo, RcsSuggestionBase, RcsSuggestionReply, RcsSuggestionActionDial +from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo, RcsSuggestionBase, RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation from .sms import Sms, SmsOptions from .viber import ( ViberAction, diff --git a/messages/src/vonage_messages/models/enums.py b/messages/src/vonage_messages/models/enums.py index 4f86e179..710f4436 100644 --- a/messages/src/vonage_messages/models/enums.py +++ b/messages/src/vonage_messages/models/enums.py @@ -43,4 +43,5 @@ class SuggestionType(str, Enum): """The type of RCS suggestion.""" REPLY = 'reply' - DIAL = 'dial' \ No newline at end of file + DIAL = 'dial' + VIEW_LOCATION = 'view_location' \ No newline at end of file diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index 3cb33b15..56628f3f 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -52,6 +52,26 @@ class RcsSuggestionActionDial(RcsSuggestionBase): type_: SuggestionType = Field(SuggestionType.DIAL, serialization_alias='type') phone_number: PhoneNumber + +class RcsSuggestionActionViewLocation(RcsSuggestionBase): + """Model for a view location action suggestion in an RCS message. + + Args: + text (str): The text to display on the suggestion chip. + postback_data (str): The data that will be sent via the Inbound Message webhook when the suggestion is selected. + latitude (float): The latitude of the location to view when the suggestion is selected. + longitude (float): The longitude of the location to view when the suggestion is selected. + pin_label (str): The label to display on the location pin. + fallback_url (str, Optional): The URL to open if the device doesn't support the view location action. + """ + + type_: SuggestionType = Field('view_location', serialization_alias='type') + latitude: str + longitude: str + pin_label: str + fallback_url: Optional[str] = None + + class BaseRcs(BaseMessage): """Model for a base RCS message. diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index f1ba6ff9..580388e8 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -10,6 +10,7 @@ RcsSuggestionBase, RcsSuggestionReply, RcsSuggestionActionDial, + RcsSuggestionActionViewLocation, ) @@ -237,3 +238,60 @@ def test_rcs_suggestion_action_dial_without_phone_number(): postback_data='postback-data', ) assert "Field required" in str(err.value) + + +def test_rcs_suggestion_action_view_location(): + suggestion = RcsSuggestionActionViewLocation( + text='View location', + postback_data='postback-data', + latitude='51.5074', + longitude='-0.1278', + pin_label='London', + fallback_url='https://example.com/location', + ) + suggestion_dict = { + 'type': 'view_location', + 'text': 'View location', + 'postback_data': 'postback-data', + 'latitude': '51.5074', + 'longitude': '-0.1278', + 'pin_label': 'London', + 'fallback_url': 'https://example.com/location', + } + assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict + + +def test_rcs_suggestion_action_view_location_without_latitude(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionViewLocation( + text='View location', + postback_data='postback-data', + longitude='-0.1278', + pin_label='London', + fallback_url='https://example.com/location', + ) + assert "Field required" in str(err.value) + + +def test_rcs_suggestion_action_view_location_without_longitude(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionViewLocation( + text='View location', + postback_data='postback-data', + latitude='51.5074', + pin_label='London', + fallback_url='https://example.com/location', + ) + assert "Field required" in str(err.value) + + +def test_rcs_suggestion_action_view_location_without_pin_label(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionViewLocation( + text='View location', + postback_data='postback-data', + latitude='51.5074', + longitude='-0.1278', + fallback_url='https://example.com/location', + ) + assert "Field required" in str(err.value) From 9c55ae24d47d9a3cb0b4378e004809d1b2f95c9b Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 11 Mar 2026 15:18:30 +0000 Subject: [PATCH 08/48] DEVX-10006: adding tests and implementation for RCS Share Location suggestion action --- messages/src/vonage_messages/models/__init__.py | 2 +- messages/src/vonage_messages/models/enums.py | 3 ++- messages/src/vonage_messages/models/rcs.py | 13 ++++++++++++- messages/tests/test_rcs_models.py | 14 ++++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index de815a07..c90a03b1 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -10,7 +10,7 @@ MessengerVideo, ) from .mms import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo -from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo, RcsSuggestionBase, RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation +from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo, RcsSuggestionBase, RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation from .sms import Sms, SmsOptions from .viber import ( ViberAction, diff --git a/messages/src/vonage_messages/models/enums.py b/messages/src/vonage_messages/models/enums.py index 710f4436..50138aca 100644 --- a/messages/src/vonage_messages/models/enums.py +++ b/messages/src/vonage_messages/models/enums.py @@ -44,4 +44,5 @@ class SuggestionType(str, Enum): REPLY = 'reply' DIAL = 'dial' - VIEW_LOCATION = 'view_location' \ No newline at end of file + VIEW_LOCATION = 'view_location' + SHARE_LOCATION = 'share_location' \ No newline at end of file diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index 56628f3f..b814685b 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -65,13 +65,24 @@ class RcsSuggestionActionViewLocation(RcsSuggestionBase): fallback_url (str, Optional): The URL to open if the device doesn't support the view location action. """ - type_: SuggestionType = Field('view_location', serialization_alias='type') + type_: SuggestionType = Field(SuggestionType.VIEW_LOCATION, serialization_alias='type') latitude: str longitude: str pin_label: str fallback_url: Optional[str] = None +class RcsSuggestionActionShareLocation(RcsSuggestionBase): + """Model for a share location action suggestion in an RCS message. + + Args: + text (str): The text to display on the suggestion chip. + postback_data (str): The data that will be sent via the Inbound Message webhook when the suggestion is selected. + """ + + type_: SuggestionType = Field(SuggestionType.SHARE_LOCATION, serialization_alias='type') + + class BaseRcs(BaseMessage): """Model for a base RCS message. diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 580388e8..95d71e4b 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -11,6 +11,7 @@ RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, + RcsSuggestionActionShareLocation, ) @@ -295,3 +296,16 @@ def test_rcs_suggestion_action_view_location_without_pin_label(): fallback_url='https://example.com/location', ) assert "Field required" in str(err.value) + + +def test_rcs_suggestion_action_share_location(): + suggestion = RcsSuggestionActionShareLocation( + text='Share location', + postback_data='postback-data', + ) + suggestion_dict = { + 'type': 'share_location', + 'text': 'Share location', + 'postback_data': 'postback-data', + } + assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict \ No newline at end of file From 02114424a3e3c6be4eb1793b11b177ecf9cfa222 Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 11 Mar 2026 15:58:57 +0000 Subject: [PATCH 09/48] DEVX-10006: adding tests and implementation for RCS Open URL suggestion action --- .../src/vonage_messages/models/__init__.py | 2 +- messages/src/vonage_messages/models/enums.py | 3 +- messages/src/vonage_messages/models/rcs.py | 14 +++++ messages/tests/test_rcs_models.py | 62 ++++++++++++++++++- 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index c90a03b1..7c05ade8 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -10,7 +10,7 @@ MessengerVideo, ) from .mms import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo -from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo, RcsSuggestionBase, RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation +from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo, RcsSuggestionBase, RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl from .sms import Sms, SmsOptions from .viber import ( ViberAction, diff --git a/messages/src/vonage_messages/models/enums.py b/messages/src/vonage_messages/models/enums.py index 50138aca..4e99e00a 100644 --- a/messages/src/vonage_messages/models/enums.py +++ b/messages/src/vonage_messages/models/enums.py @@ -45,4 +45,5 @@ class SuggestionType(str, Enum): REPLY = 'reply' DIAL = 'dial' VIEW_LOCATION = 'view_location' - SHARE_LOCATION = 'share_location' \ No newline at end of file + SHARE_LOCATION = 'share_location' + OPEN_URL = 'open_url' \ No newline at end of file diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index b814685b..98250a0c 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -83,6 +83,20 @@ class RcsSuggestionActionShareLocation(RcsSuggestionBase): type_: SuggestionType = Field(SuggestionType.SHARE_LOCATION, serialization_alias='type') +class RcsSuggestionActionOpenUrl(RcsSuggestionBase): + """Model for an open URL action suggestion in an RCS message. + + Args: + text (str): The text to display on the suggestion chip. + postback_data (str): The data that will be sent via the Inbound Message webhook when the suggestion is selected. + url (str): The URL to open when the suggestion is selected. + """ + + type_: SuggestionType = Field(SuggestionType.OPEN_URL, serialization_alias='type') + url: str + description: str = Field(..., min_length=1, max_length=500) + + class BaseRcs(BaseMessage): """Model for a base RCS message. diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 95d71e4b..530cc9e2 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -12,6 +12,7 @@ RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation, + RcsSuggestionActionOpenUrl, ) @@ -308,4 +309,63 @@ def test_rcs_suggestion_action_share_location(): 'text': 'Share location', 'postback_data': 'postback-data', } - assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict \ No newline at end of file + assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict + + +def test_rcs_suggestion_action_open_url(): + suggestion = RcsSuggestionActionOpenUrl( + text='Open URL', + postback_data='postback-data', + url='https://example.com', + description='Click to open the URL', + ) + suggestion_dict = { + 'type': 'open_url', + 'text': 'Open URL', + 'postback_data': 'postback-data', + 'url': 'https://example.com', + 'description': 'Click to open the URL', + } + assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict + + +def test_rcs_suggestion_action_open_url_without_url(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionOpenUrl( + text='Open URL', + postback_data='postback-data', + description='Click to open the URL', + ) + assert "Field required" in str(err.value) + + +def test_rcs_suggestion_action_open_url_without_description(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionOpenUrl( + text='Open URL', + postback_data='postback-data', + url='https://example.com', + ) + assert "Field required" in str(err.value) + + +def test_rcs_suggestion_action_open_url_with_description_too_short(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionOpenUrl( + text='Open URL', + postback_data='postback-data', + url='https://example.com', + description='', + ) + assert "String should have at least 1 character" in str(err.value) + + +def test_rcs_suggestion_action_open_url_with_description_too_long(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionOpenUrl( + text='Open URL', + postback_data='postback-data', + url='https://example.com', + description='A' * 500 + 'B', + ) + assert "String should have at most 500 characters" in str(err.value) \ No newline at end of file From 352b43d75c2000da38281764c6136c35b72c6410 Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 11 Mar 2026 16:29:23 +0000 Subject: [PATCH 10/48] DEVX-10006: Adding tests and implementation for RCS Open URL in Webview suggestion action --- .../src/vonage_messages/models/__init__.py | 2 +- messages/src/vonage_messages/models/enums.py | 11 ++- messages/src/vonage_messages/models/rcs.py | 16 +++- messages/tests/test_rcs_models.py | 80 ++++++++++++++++++- 4 files changed, 105 insertions(+), 4 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index 7c05ade8..5f72b5da 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -10,7 +10,7 @@ MessengerVideo, ) from .mms import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo -from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo, RcsSuggestionBase, RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl +from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo, RcsSuggestionBase, RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview from .sms import Sms, SmsOptions from .viber import ( ViberAction, diff --git a/messages/src/vonage_messages/models/enums.py b/messages/src/vonage_messages/models/enums.py index 4e99e00a..8d57e9b6 100644 --- a/messages/src/vonage_messages/models/enums.py +++ b/messages/src/vonage_messages/models/enums.py @@ -46,4 +46,13 @@ class SuggestionType(str, Enum): DIAL = 'dial' VIEW_LOCATION = 'view_location' SHARE_LOCATION = 'share_location' - OPEN_URL = 'open_url' \ No newline at end of file + OPEN_URL = 'open_url' + OPEN_URL_IN_WEBVIEW = 'open_url_in_webview' + + +class UrlWebviewViewMode(str, Enum): + """The view mode for an RCS suggestion that opens a URL in a webview.""" + + FULL = 'FULL' + TALL = 'TALL' + HALF = 'HALF' diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index 98250a0c..337760e0 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -4,7 +4,7 @@ from vonage_utils.types import PhoneNumber from .base_message import BaseMessage -from .enums import ChannelType, MessageType, SuggestionType +from .enums import ChannelType, MessageType, SuggestionType, UrlWebviewViewMode class RcsResource(BaseModel): @@ -97,6 +97,20 @@ class RcsSuggestionActionOpenUrl(RcsSuggestionBase): description: str = Field(..., min_length=1, max_length=500) +class RcsSuggestionActionOpenUrlWebview(RcsSuggestionActionOpenUrl): + """Model for an open URL in webview action suggestion in an RCS message. + + Args: + text (str): The text to display on the suggestion chip. + postback_data (str): The data that will be sent via the Inbound Message webhook when the suggestion is selected. + url (str): The URL to open in a webview when the suggestion is selected. + view_mode (str, Optional): The view mode for the webview. If not specified, the default view mode will be used. + """ + + type_: SuggestionType = Field(SuggestionType.OPEN_URL_IN_WEBVIEW, serialization_alias='type') + view_mode: Optional[UrlWebviewViewMode] = None + + class BaseRcs(BaseMessage): """Model for a base RCS message. diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 530cc9e2..c1742c0c 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -13,6 +13,7 @@ RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl, + RcsSuggestionActionOpenUrlWebview, ) @@ -368,4 +369,81 @@ def test_rcs_suggestion_action_open_url_with_description_too_long(): url='https://example.com', description='A' * 500 + 'B', ) - assert "String should have at most 500 characters" in str(err.value) \ No newline at end of file + assert "String should have at most 500 characters" in str(err.value) + + +def test_rcs_suggestion_action_open_url_in_webview(): + suggestion = RcsSuggestionActionOpenUrlWebview( + text='Open URL', + postback_data='postback-data', + url='https://example.com', + description='Click to open the URL', + view_mode='FULL', + ) + suggestion_dict = { + 'type': 'open_url_in_webview', + 'text': 'Open URL', + 'postback_data': 'postback-data', + 'url': 'https://example.com', + 'description': 'Click to open the URL', + 'view_mode': 'FULL', + } + assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict + + +# @pytest.mark.skip(reason="Not yet implemented") +def test_rcs_suggestion_action_open_url_in_webview_without_url(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionOpenUrlWebview( + text='Open URL', + postback_data='postback-data', + description='Click to open the URL', + ) + assert "Field required" in str(err.value) + + +# @pytest.mark.skip(reason="Not yet implemented") +def test_rcs_suggestion_action_open_url_in_webview_without_description(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionOpenUrlWebview( + text='Open URL', + postback_data='postback-data', + url='https://example.com', + ) + assert "Field required" in str(err.value) + + +# @pytest.mark.skip(reason="Not yet implemented") +def test_rcs_suggestion_action_open_url_in_webview_with_description_too_short(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionOpenUrlWebview( + text='Open URL', + postback_data='postback-data', + url='https://example.com', + description='', + ) + assert "String should have at least 1 character" in str(err.value) + + +# @pytest.mark.skip(reason="Not yet implemented") +def test_rcs_suggestion_action_open_url_in_webview_with_description_too_long(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionOpenUrlWebview( + text='Open URL', + postback_data='postback-data', + url='https://example.com', + description='A' * 500 + 'B', + ) + assert "String should have at most 500 characters" in str(err.value) + + +def test_rcs_suggestion_action_open_url_in_webview_with_invalid_view_mode(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionOpenUrlWebview( + text='Open URL', + postback_data='postback-data', + url='https://example.com', + description='Click to open the URL', + view_mode='INVALID_VIEW_MODE', + ) + assert "Input should be 'FULL', 'TALL' or 'HALF'" in str(err.value) From e8cbd36abb8cd24172fcab19676a595b8b671bea Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 11 Mar 2026 16:58:54 +0000 Subject: [PATCH 11/48] DEVX-10006: addig tests and implementation for RCS Create Calendar Event suggestion action --- .../src/vonage_messages/models/__init__.py | 17 ++- messages/src/vonage_messages/models/enums.py | 1 + messages/src/vonage_messages/models/rcs.py | 20 +++ messages/tests/test_rcs_models.py | 128 +++++++++++++++++- 4 files changed, 161 insertions(+), 5 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index 5f72b5da..ce672ac5 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -10,7 +10,22 @@ MessengerVideo, ) from .mms import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo -from .rcs import RcsCustom, RcsFile, RcsImage, RcsResource, RcsText, RcsVideo, RcsSuggestionBase, RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview +from .rcs import ( + RcsCustom, + RcsFile, + RcsImage, + RcsResource, + RcsText, + RcsVideo, + RcsSuggestionBase, + RcsSuggestionReply, + RcsSuggestionActionDial, + RcsSuggestionActionViewLocation, + RcsSuggestionActionShareLocation, + RcsSuggestionActionOpenUrl, + RcsSuggestionActionOpenUrlWebview, + RcsSuggestionActionCreateCalendarEvent, +) from .sms import Sms, SmsOptions from .viber import ( ViberAction, diff --git a/messages/src/vonage_messages/models/enums.py b/messages/src/vonage_messages/models/enums.py index 8d57e9b6..a38d37b7 100644 --- a/messages/src/vonage_messages/models/enums.py +++ b/messages/src/vonage_messages/models/enums.py @@ -48,6 +48,7 @@ class SuggestionType(str, Enum): SHARE_LOCATION = 'share_location' OPEN_URL = 'open_url' OPEN_URL_IN_WEBVIEW = 'open_url_in_webview' + CREATE_CALENDAR_EVENT = 'create_calendar_event' class UrlWebviewViewMode(str, Enum): diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index 337760e0..802a5c04 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -110,6 +110,26 @@ class RcsSuggestionActionOpenUrlWebview(RcsSuggestionActionOpenUrl): type_: SuggestionType = Field(SuggestionType.OPEN_URL_IN_WEBVIEW, serialization_alias='type') view_mode: Optional[UrlWebviewViewMode] = None +class RcsSuggestionActionCreateCalendarEvent(RcsSuggestionBase): + """Model for a create calendar event action suggestion in an RCS message. + + Args: + text (str): The text to display on the suggestion chip. + postback_data (str): The data that will be sent via the Inbound Message webhook when the suggestion is selected. + start_time (str): The start time of the calendar event in ISO 8601 format. + end_time (str): The end time of the calendar event in ISO 8601 format + title (str): The title of the calendar event. + description (str): The description of the calendar event. + fallback_url (str, Optional): The URL to open if the device doesn't support the create calendar event action. + """ + + type_: SuggestionType = Field(SuggestionType.CREATE_CALENDAR_EVENT, serialization_alias='type') + start_time: str + end_time: str + title: str = Field(..., min_length=1, max_length=100) + description: str = Field(..., min_length=1, max_length=500) + fallback_url: Optional[str] = None + class BaseRcs(BaseMessage): """Model for a base RCS message. diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index c1742c0c..72b9dfb5 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -14,6 +14,7 @@ RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview, + RcsSuggestionActionCreateCalendarEvent, ) @@ -391,7 +392,6 @@ def test_rcs_suggestion_action_open_url_in_webview(): assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict -# @pytest.mark.skip(reason="Not yet implemented") def test_rcs_suggestion_action_open_url_in_webview_without_url(): with pytest.raises(ValidationError) as err: suggestion = RcsSuggestionActionOpenUrlWebview( @@ -402,7 +402,6 @@ def test_rcs_suggestion_action_open_url_in_webview_without_url(): assert "Field required" in str(err.value) -# @pytest.mark.skip(reason="Not yet implemented") def test_rcs_suggestion_action_open_url_in_webview_without_description(): with pytest.raises(ValidationError) as err: suggestion = RcsSuggestionActionOpenUrlWebview( @@ -413,7 +412,6 @@ def test_rcs_suggestion_action_open_url_in_webview_without_description(): assert "Field required" in str(err.value) -# @pytest.mark.skip(reason="Not yet implemented") def test_rcs_suggestion_action_open_url_in_webview_with_description_too_short(): with pytest.raises(ValidationError) as err: suggestion = RcsSuggestionActionOpenUrlWebview( @@ -425,7 +423,6 @@ def test_rcs_suggestion_action_open_url_in_webview_with_description_too_short(): assert "String should have at least 1 character" in str(err.value) -# @pytest.mark.skip(reason="Not yet implemented") def test_rcs_suggestion_action_open_url_in_webview_with_description_too_long(): with pytest.raises(ValidationError) as err: suggestion = RcsSuggestionActionOpenUrlWebview( @@ -447,3 +444,126 @@ def test_rcs_suggestion_action_open_url_in_webview_with_invalid_view_mode(): view_mode='INVALID_VIEW_MODE', ) assert "Input should be 'FULL', 'TALL' or 'HALF'" in str(err.value) + + +def test_rcs_suggestion_action_create_calendar_event(): + suggestion = RcsSuggestionActionCreateCalendarEvent( + text='Add to calendar', + postback_data='postback-data', + start_time='2024-01-01T12:00:00Z', + end_time='2024-01-01T13:00:00Z', + title='Meeting with Bob', + description='Discuss project updates', + fallback_url='https://example.com/calendar-event', + ) + suggestion_dict = { + 'type': 'create_calendar_event', + 'text': 'Add to calendar', + 'postback_data': 'postback-data', + 'start_time': '2024-01-01T12:00:00Z', + 'end_time': '2024-01-01T13:00:00Z', + 'title': 'Meeting with Bob', + 'description': 'Discuss project updates', + 'fallback_url': 'https://example.com/calendar-event', + } + assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict + + +def test_rcs_suggestion_action_create_calendar_event_without_start_time(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionCreateCalendarEvent( + text='Add to calendar', + postback_data='postback-data', + end_time='2024-01-01T13:00:00Z', + title='Meeting with Bob', + description='Discuss project updates', + ) + assert "Field required" in str(err.value) + + +def test_rcs_suggestion_action_create_calendar_event_without_end_time(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionCreateCalendarEvent( + text='Add to calendar', + postback_data='postback-data', + start_time='2024-01-01T12:00:00Z', + title='Meeting with Bob', + description='Discuss project updates', + ) + assert "Field required" in str(err.value) + + +def test_rcs_suggestion_action_create_calendar_event_without_title(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionCreateCalendarEvent( + text='Add to calendar', + postback_data='postback-data', + start_time='2024-01-01T12:00:00Z', + end_time='2024-01-01T13:00:00Z', + description='Discuss project updates', + ) + assert "Field required" in str(err.value) + + +def test_rcs_suggestion_action_create_calendar_event_without_description(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionCreateCalendarEvent( + text='Add to calendar', + postback_data='postback-data', + start_time='2024-01-01T12:00:00Z', + end_time='2024-01-01T13:00:00Z', + title='Meeting with Bob', + ) + assert "Field required" in str(err.value) + + +def test_rcs_suggestion_action_create_calendar_event_with_title_too_short(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionCreateCalendarEvent( + text='Add to calendar', + postback_data='postback-data', + start_time='2024-01-01T12:00:00Z', + end_time='2024-01-01T13:00:00Z', + title='', + description='Discuss project updates', + ) + assert "String should have at least 1 character" in str(err.value) + + +def test_rcs_suggestion_action_create_calendar_event_with_title_too_long(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionCreateCalendarEvent( + text='Add to calendar', + postback_data='postback-data', + start_time='2024-01-01T12:00:00Z', + end_time='2024-01-01T13:00:00Z', + title='A' * 100 + 'B', + description='Discuss project updates', + ) + assert "String should have at most 100 characters" in str(err.value) + + +def test_rcs_suggestion_action_create_calendar_event_with_description_too_short(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionCreateCalendarEvent( + text='Add to calendar', + postback_data='postback-data', + start_time='2024-01-01T12:00:00Z', + end_time='2024-01-01T13:00:00Z', + title='Meeting with Bob', + description='', + ) + assert "String should have at least 1 character" in str(err.value) + + +def test_rcs_suggestion_action_create_calendar_event_with_description_too_long(): + with pytest.raises(ValidationError) as err: + suggestion = RcsSuggestionActionCreateCalendarEvent( + text='Add to calendar', + postback_data='postback-data', + start_time='2024-01-01T12:00:00Z', + end_time='2024-01-01T13:00:00Z', + title='Meeting with Bob', + description='A' * 500 + 'B', + ) + assert "String should have at most 500 characters" in str(err.value) From efcb0c04c881bfe0174feee1cd558f5f11b90f5d Mon Sep 17 00:00:00 2001 From: superchilled Date: Thu, 12 Mar 2026 17:02:44 +0000 Subject: [PATCH 12/48] DEVX-10006: Adding test and implemention for RCS Category model --- .../src/vonage_messages/models/__init__.py | 1 + messages/src/vonage_messages/models/enums.py | 10 ++++++ messages/src/vonage_messages/models/rcs.py | 13 +++++++- messages/tests/test_rcs_models.py | 31 +++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index ce672ac5..943c5e9a 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -25,6 +25,7 @@ RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview, RcsSuggestionActionCreateCalendarEvent, + RcsOptions, ) from .sms import Sms, SmsOptions from .viber import ( diff --git a/messages/src/vonage_messages/models/enums.py b/messages/src/vonage_messages/models/enums.py index a38d37b7..90bbbf34 100644 --- a/messages/src/vonage_messages/models/enums.py +++ b/messages/src/vonage_messages/models/enums.py @@ -57,3 +57,13 @@ class UrlWebviewViewMode(str, Enum): FULL = 'FULL' TALL = 'TALL' HALF = 'HALF' + + +class RcsCategory(str, Enum): + """The category of an RCS message.""" + + ACKNOWLEDGEMENT = 'acknowledgement' + AUTHENTICATION = 'authentication' + PROMOTION = 'promotion' + SERVICE_REQUEST = 'service-request' + TRANSACTION = 'transaction' \ No newline at end of file diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index 802a5c04..e8fbb735 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -4,7 +4,7 @@ from vonage_utils.types import PhoneNumber from .base_message import BaseMessage -from .enums import ChannelType, MessageType, SuggestionType, UrlWebviewViewMode +from .enums import ChannelType, MessageType, SuggestionType, UrlWebviewViewMode, RcsCategory class RcsResource(BaseModel): @@ -131,6 +131,17 @@ class RcsSuggestionActionCreateCalendarEvent(RcsSuggestionBase): fallback_url: Optional[str] = None + +class RcsOptions(BaseModel): + """Model for RCS message options. + + Args: + category (str, Optional): The category of the RCS message. + """ + + category: Optional[RcsCategory] = None + + class BaseRcs(BaseMessage): """Model for a base RCS message. diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 72b9dfb5..d8aea557 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -15,6 +15,7 @@ RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview, RcsSuggestionActionCreateCalendarEvent, + RcsOptions, ) @@ -567,3 +568,33 @@ def test_rcs_suggestion_action_create_calendar_event_with_description_too_long() description='A' * 500 + 'B', ) assert "String should have at most 500 characters" in str(err.value) + + +def test_create_rcs_options(): + options = RcsOptions( + category='transaction', + ) + options_dict = { + 'category': 'transaction', + } + assert options.model_dump(by_alias=True, exclude_none=True) == options_dict + + +def test_create_rcs_options_with_each_valid_category(): + valid_options = ['acknowledgement', 'authentication', 'promotion', 'service-request', 'transaction'] + for option in valid_options: + options = RcsOptions( + category=option, + ) + options_dict = { + 'category': option, + } + assert options.model_dump(by_alias=True, exclude_none=True) == options_dict + + +def test_create_rcs_options_with_invalid_category(): + with pytest.raises(ValidationError) as err: + options = RcsOptions( + category='invalid-category', + ) + assert "Input should be 'acknowledgement', 'authentication', 'promotion', 'service-request' or 'transaction'" in str(err.value) From c9d4e5baf324dd994e9b13ee3c720e3c486e95a1 Mon Sep 17 00:00:00 2001 From: superchilled Date: Fri, 13 Mar 2026 13:01:32 +0000 Subject: [PATCH 13/48] DEVX-10006: Add tests and implementation for RCS Card Options model --- .../src/vonage_messages/models/__init__.py | 1 + messages/src/vonage_messages/models/enums.py | 16 ++++- messages/src/vonage_messages/models/rcs.py | 14 ++++- messages/tests/test_rcs_models.py | 58 +++++++++++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index 943c5e9a..2874ad04 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -26,6 +26,7 @@ RcsSuggestionActionOpenUrlWebview, RcsSuggestionActionCreateCalendarEvent, RcsOptions, + RcsOptionsCard, ) from .sms import Sms, SmsOptions from .viber import ( diff --git a/messages/src/vonage_messages/models/enums.py b/messages/src/vonage_messages/models/enums.py index 90bbbf34..84caa7d2 100644 --- a/messages/src/vonage_messages/models/enums.py +++ b/messages/src/vonage_messages/models/enums.py @@ -66,4 +66,18 @@ class RcsCategory(str, Enum): AUTHENTICATION = 'authentication' PROMOTION = 'promotion' SERVICE_REQUEST = 'service-request' - TRANSACTION = 'transaction' \ No newline at end of file + TRANSACTION = 'transaction' + + +class RcsCardOrientation(str, Enum): + """The orientation of an RCS card.""" + + VERTICAL = 'VERTICAL' + HORIZONTAL = 'HORIZONTAL' + + +class RcsImageAlignment(str, Enum): + """The alignment of an image on an RCS card.""" + + LEFT = 'LEFT' + RIGHT = 'RIGHT' \ No newline at end of file diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index e8fbb735..e9927378 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -4,7 +4,7 @@ from vonage_utils.types import PhoneNumber from .base_message import BaseMessage -from .enums import ChannelType, MessageType, SuggestionType, UrlWebviewViewMode, RcsCategory +from .enums import ChannelType, MessageType, SuggestionType, UrlWebviewViewMode, RcsCategory, RcsCardOrientation, RcsImageAlignment class RcsResource(BaseModel): @@ -142,6 +142,18 @@ class RcsOptions(BaseModel): category: Optional[RcsCategory] = None +class RcsOptionsCard(RcsOptions): + """Model for an RCS message options card. + + Args: + category (str, Optional): The category of the RCS message. + card_orientation (str): The orientation of the card (HORIZONTAL or VERTICAL). + image_alignment (str): The alignment of the image on the card (LEFT or RIGHT). + """ + + card_orientation: Optional[RcsCardOrientation] = None + image_alignment: Optional[RcsImageAlignment] = None + class BaseRcs(BaseMessage): """Model for a base RCS message. diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index d8aea557..4e0c67fb 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -16,6 +16,7 @@ RcsSuggestionActionOpenUrlWebview, RcsSuggestionActionCreateCalendarEvent, RcsOptions, + RcsOptionsCard, ) @@ -598,3 +599,60 @@ def test_create_rcs_options_with_invalid_category(): category='invalid-category', ) assert "Input should be 'acknowledgement', 'authentication', 'promotion', 'service-request' or 'transaction'" in str(err.value) + + +def test_create_rcs_options_card(): + options = RcsOptionsCard( + card_orientation='HORIZONTAL', + image_alignment='LEFT' + ) + options_dict = { + 'card_orientation': 'HORIZONTAL', + 'image_alignment': 'LEFT', + } + assert options.model_dump(by_alias=True, exclude_none=True) == options_dict + + +def test_create_rcs_options_card_card_orientation_with_each_valid_option(): + valid_orientations = ['VERTICAL', 'HORIZONTAL'] + for orientation in valid_orientations: + options = RcsOptionsCard( + card_orientation=orientation, + image_alignment='LEFT' + ) + options_dict = { + 'card_orientation': orientation, + 'image_alignment': 'LEFT', + } + assert options.model_dump(by_alias=True, exclude_none=True) == options_dict + + +def test_create_rcs_options_card_image_alignment_with_each_valid_option(): + valid_alignments = ['LEFT', 'RIGHT'] + for alignment in valid_alignments: + options = RcsOptionsCard( + card_orientation='HORIZONTAL', + image_alignment=alignment + ) + options_dict = { + 'card_orientation': 'HORIZONTAL', + 'image_alignment': alignment, + } + assert options.model_dump(by_alias=True, exclude_none=True) == options_dict + + +def test_create_rcs_options_card_card_orientation_with_invalid_option(): + with pytest.raises(ValidationError) as err: + options = RcsOptionsCard( + card_orientation='INVALID_ORIENTATION', + image_alignment='LEFT' + ) + assert "Input should be 'VERTICAL' or 'HORIZONTAL'" in str(err.value) + +def test_create_rcs_options_card_image_alignment_with_invalid_option(): + with pytest.raises(ValidationError) as err: + options = RcsOptionsCard( + card_orientation='HORIZONTAL', + image_alignment='INVALID_ALIGNMENT' + ) + assert "Input should be 'LEFT' or 'RIGHT'" in str(err.value) From 4f7b5183a4838c4a1cf16bcc83562eb54c7623c9 Mon Sep 17 00:00:00 2001 From: superchilled Date: Fri, 13 Mar 2026 16:02:56 +0000 Subject: [PATCH 14/48] DEVX-10006: Updating RCS Text model and adding initial RCS Card implementation --- .../src/vonage_messages/models/__init__.py | 2 + messages/src/vonage_messages/models/enums.py | 18 +- messages/src/vonage_messages/models/rcs.py | 72 +++- messages/tests/test_rcs_models.py | 338 ++++++++++++++++++ 4 files changed, 425 insertions(+), 5 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index 2874ad04..9dae982c 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -12,6 +12,7 @@ from .mms import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo from .rcs import ( RcsCustom, + RcsCard, RcsFile, RcsImage, RcsResource, @@ -27,6 +28,7 @@ RcsSuggestionActionCreateCalendarEvent, RcsOptions, RcsOptionsCard, + RcsOptionsCarousel, ) from .sms import Sms, SmsOptions from .viber import ( diff --git a/messages/src/vonage_messages/models/enums.py b/messages/src/vonage_messages/models/enums.py index 84caa7d2..97d09582 100644 --- a/messages/src/vonage_messages/models/enums.py +++ b/messages/src/vonage_messages/models/enums.py @@ -13,6 +13,7 @@ class MessageType(str, Enum): STICKER = 'sticker' CUSTOM = 'custom' VCARD = 'vcard' + CARD = 'card' class ChannelType(str, Enum): @@ -80,4 +81,19 @@ class RcsImageAlignment(str, Enum): """The alignment of an image on an RCS card.""" LEFT = 'LEFT' - RIGHT = 'RIGHT' \ No newline at end of file + RIGHT = 'RIGHT' + + +class RcsCardWidth(str, Enum): + """The width of a card in an RCS carousel.""" + + SMALL = 'SMALL' + MEDIUM = 'MEDIUM' + + +class RcsMediaHeight(str, Enum): + """The height of media on an RCS card.""" + + SHORT = 'SHORT' + MEDIUM = 'MEDIUM' + TALL = 'TALL' diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index e9927378..5c77cc8f 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -1,10 +1,10 @@ -from typing import Optional +from typing import Optional, List, Union from pydantic import BaseModel, Field from vonage_utils.types import PhoneNumber from .base_message import BaseMessage -from .enums import ChannelType, MessageType, SuggestionType, UrlWebviewViewMode, RcsCategory, RcsCardOrientation, RcsImageAlignment +from .enums import ChannelType, MessageType, SuggestionType, UrlWebviewViewMode, RcsCategory, RcsCardOrientation, RcsImageAlignment, RcsCardWidth, RcsMediaHeight class RcsResource(BaseModel): @@ -131,7 +131,6 @@ class RcsSuggestionActionCreateCalendarEvent(RcsSuggestionBase): fallback_url: Optional[str] = None - class RcsOptions(BaseModel): """Model for RCS message options. @@ -143,7 +142,7 @@ class RcsOptions(BaseModel): class RcsOptionsCard(RcsOptions): - """Model for an RCS message options card. + """Model for an RCS card message options. Args: category (str, Optional): The category of the RCS message. @@ -154,6 +153,17 @@ class RcsOptionsCard(RcsOptions): card_orientation: Optional[RcsCardOrientation] = None image_alignment: Optional[RcsImageAlignment] = None + +class RcsOptionsCarousel(RcsOptions): + """Model for an RCS carousel message options. + + Args: + card_width (str): The width of each card in the carousel (SMALL or MEDIUM). + """ + + card_width: RcsCardWidth + + class BaseRcs(BaseMessage): """Model for a base RCS message. @@ -187,6 +197,20 @@ class RcsText(BaseRcs): text: str = Field(..., min_length=1, max_length=3072) message_type: MessageType = MessageType.TEXT + suggestions: Optional[ + List[ + Union[ + RcsSuggestionReply, + RcsSuggestionActionDial, + RcsSuggestionActionViewLocation, + RcsSuggestionActionShareLocation, + RcsSuggestionActionOpenUrl, + RcsSuggestionActionOpenUrlWebview, + RcsSuggestionActionCreateCalendarEvent, + ] + ] + ] = Field(None, min_length=1, max_length=11) + rcs: Optional[RcsOptions] = None class RcsImage(BaseRcs): @@ -240,6 +264,46 @@ class RcsFile(BaseRcs): message_type: MessageType = MessageType.FILE +class RcsCard(BaseRcs): + """Model for an RCS card message. + + Args: + title (str): The title of the card. + description (str): The description of the card. + media (RcsResource, Optional): The media resource for the card. Can be an image or a video. + suggestions (List[Union[RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview, RcsSuggestionActionCreateCalendarEvent], Optional): A list of suggestions to include on the card. Can include up to 8 suggestions. + to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. + from_ (str): The sender's phone number in E.164 format. Don't use a leading plus sign. + ttl (int, Optional): The duration in seconds for which the message is valid. + client_ref (str, Optional): An optional client reference. + webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. + webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. + """ + + title: str = Field(..., min_length=1, max_length=200) + text: str = Field(..., min_length=1, max_length=2000) + media_url: str + media_description: Optional[str] = None + media_height: Optional[RcsMediaHeight] = None + thumbnail_url: Optional[str] = None + media_force_refresh: Optional[bool] = None + suggestions: Optional[ + List[ + Union[ + RcsSuggestionReply, + RcsSuggestionActionDial, + RcsSuggestionActionViewLocation, + RcsSuggestionActionShareLocation, + RcsSuggestionActionOpenUrl, + RcsSuggestionActionOpenUrlWebview, + RcsSuggestionActionCreateCalendarEvent, + ] + ] + ] = Field(None, min_length=1, max_length=8) + rcs: Optional[RcsOptionsCard] = None + message_type: MessageType = MessageType.CARD + + class RcsCustom(BaseRcs): """Model for an RCS custom message. diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 4e0c67fb..e9695ef8 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -2,6 +2,7 @@ from pydantic import ValidationError from vonage_messages.models import ( RcsCustom, + RcsCard, RcsFile, RcsImage, RcsResource, @@ -17,6 +18,7 @@ RcsSuggestionActionCreateCalendarEvent, RcsOptions, RcsOptionsCard, + RcsOptionsCarousel, ) @@ -66,6 +68,9 @@ def test_create_rcs_text_all_fields(): client_ref='client-ref', webhook_url='https://example.com', ttl=600, + rcs=RcsOptions( + category='transaction', + ), ) rcs_dict = { 'to': '1234567890', @@ -76,11 +81,214 @@ def test_create_rcs_text_all_fields(): 'ttl': 600, 'channel': 'rcs', 'message_type': 'text', + 'rcs': { + 'category': 'transaction', + } + } + + assert rcs_model.model_dump(by_alias=True, exclude_none=True) == rcs_dict + + +def test_create_rcs_text_with_suggestions(): + rcs_model = RcsText( + to='1234567890', + from_='asdf1234', + text='Hello, World!', + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + RcsSuggestionActionDial( + text='Call us', + postback_data='postback-data', + phone_number='447900000000', + ), + ], + ) + rcs_dict = { + 'to': '1234567890', + 'from': 'asdf1234', + 'text': 'Hello, World!', + 'suggestions': [ + { + 'type': 'reply', + 'text': 'Reply', + 'postback_data': 'postback-data', + }, + { + 'type': 'dial', + 'text': 'Call us', + 'postback_data': 'postback-data', + 'phone_number': '447900000000', + }, + ], + 'channel': 'rcs', + 'message_type': 'text', } assert rcs_model.model_dump(by_alias=True, exclude_none=True) == rcs_dict +def test_create_rcs_text_with_all_suggestion_types(): + rcs_model = RcsText( + to='1234567890', + from_='asdf1234', + text='Hello, World!', + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + RcsSuggestionActionDial( + text='Call us', + postback_data='postback-data', + phone_number='447900000000', + ), + RcsSuggestionActionViewLocation( + text='View location', + postback_data='postback-data', + latitude='51.5074', + longitude='-0.1278', + pin_label='London', + fallback_url='https://example.com/location', + ), + RcsSuggestionActionShareLocation( + text='Share location', + postback_data='postback-data', + ), + RcsSuggestionActionOpenUrl( + text='Open URL', + postback_data='postback-data', + url='https://example.com', + description='Click to open the URL', + ), + RcsSuggestionActionOpenUrlWebview( + text='Open URL in webview', + postback_data='postback-data', + url='https://example.com', + description='Click to open the URL in a webview', + view_mode='FULL', + ), + RcsSuggestionActionCreateCalendarEvent( + text='Add to calendar', + postback_data='postback-data', + start_time='2024-01-01T12:00:00Z', + end_time='2024-01-01T13:00:00Z', + title='Meeting with Bob', + description='Discuss project updates', + fallback_url='https://example.com/calendar-event', + ), + ], + ) + rcs_dict = { + 'to': '1234567890', + 'from': 'asdf1234', + 'text': 'Hello, World!', + 'suggestions': [ + { + 'type': 'reply', + 'text': 'Reply', + 'postback_data': 'postback-data', + }, + { + 'type': 'dial', + 'text': 'Call us', + 'postback_data': 'postback-data', + 'phone_number': '447900000000', + }, + { + 'type': 'view_location', + 'text': 'View location', + 'postback_data': 'postback-data', + 'latitude': '51.5074', + 'longitude': '-0.1278', + 'pin_label': 'London', + 'fallback_url': 'https://example.com/location', + }, + { + 'type': 'share_location', + 'text': 'Share location', + 'postback_data': 'postback-data', + }, + { + 'type': 'open_url', + 'text': 'Open URL', + 'postback_data': 'postback-data', + 'url': 'https://example.com', + 'description': 'Click to open the URL', + }, + { + 'type': 'open_url_in_webview', + 'text': 'Open URL in webview', + 'postback_data': 'postback-data', + 'url': 'https://example.com', + 'description': 'Click to open the URL in a webview', + 'view_mode': 'FULL', + }, + { + 'type': 'create_calendar_event', + 'text': 'Add to calendar', + 'postback_data': 'postback-data', + 'start_time': '2024-01-01T12:00:00Z', + 'end_time': '2024-01-01T13:00:00Z', + 'title': 'Meeting with Bob', + 'description': 'Discuss project updates', + 'fallback_url': 'https://example.com/calendar-event', + } + ], + 'channel': 'rcs', + 'message_type': 'text', + } + + + assert rcs_model.model_dump(by_alias=True, exclude_none=True) == rcs_dict + + +def test_create_rcs_text_with_insuffient_suggestions(): + with pytest.raises(ValidationError) as err: + rcs_model = RcsText( + to='1234567890', + from_='asdf1234', + text='Hello, World!', + suggestions=[], + ) + assert "List should have at least 1 item" in str(err.value) + + +def test_create_rcs_text_with_too_many_suggestions(): + with pytest.raises(ValidationError) as err: + rcs_model = RcsText( + to='1234567890', + from_='asdf1234', + text='Hello, World!', + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + ] * 12, + ) + assert "List should have at most 11 items" in str(err.value) + + +def test_create_rcs_text_with_inavalid_suggestion_types(): + with pytest.raises(ValidationError) as err: + rcs_model = RcsText( + to='1234567890', + from_='asdf1234', + text='Hello, World!', + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + "Invalid suggestion type", + ], + ) + assert "Input should be a valid dictionary or instance" in str(err.value) + + def test_create_rcs_image(): rcs_model = RcsImage( to='1234567890', @@ -144,6 +352,106 @@ def test_create_rcs_file(): assert rcs_model.model_dump(by_alias=True, exclude_none=True) == rcs_dict +def test_create_rcs_card(): + card = RcsCard( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + ) + card_dict = { + 'to': '1234567890', + 'from': 'asdf1234', + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'channel': 'rcs', + 'message_type': 'card', + } + assert card.model_dump(by_alias=True, exclude_none=True) == card_dict + + +def test_create_rcs_card_with_optional_params(): + card = RcsCard( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_description='Image description', + media_height='MEDIUM', + thumbnail_url='https://example.com/thumbnail.jpg', + media_force_refresh=True, + rcs=RcsOptionsCard( + card_orientation='VERTICAL', + image_alignment='LEFT', + ), + ) + card_dict = { + 'to': '1234567890', + 'from': 'asdf1234', + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'media_description': 'Image description', + 'media_height': 'MEDIUM', + 'thumbnail_url': 'https://example.com/thumbnail.jpg', + 'media_force_refresh': True, + 'rcs': { + 'card_orientation': 'VERTICAL', + 'image_alignment': 'LEFT', + }, + 'channel': 'rcs', + 'message_type': 'card', + } + assert card.model_dump(by_alias=True, exclude_none=True) == card_dict + + +def test_create_rcs_card_with_suggestions(): + card = RcsCard( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + RcsSuggestionActionDial( + text='Call us', + postback_data='postback-data', + phone_number='447900000000', + ), + ], + ) + card_dict = { + 'to': '1234567890', + 'from': 'asdf1234', + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'suggestions': [ + { + 'type': 'reply', + 'text': 'Reply', + 'postback_data': 'postback-data', + }, + { + 'type': 'dial', + 'text': 'Call us', + 'postback_data': 'postback-data', + 'phone_number': '447900000000', + }, + ], + 'channel': 'rcs', + 'message_type': 'card', + } + assert card.model_dump(by_alias=True, exclude_none=True) == card_dict + + def test_create_rcs_custom(): rcs_model = RcsCustom( to='1234567890', @@ -656,3 +964,33 @@ def test_create_rcs_options_card_image_alignment_with_invalid_option(): image_alignment='INVALID_ALIGNMENT' ) assert "Input should be 'LEFT' or 'RIGHT'" in str(err.value) + + +def test_create_rcs_options_carousel(): + options = RcsOptionsCarousel( + card_width='MEDIUM', + ) + options_dict = { + 'card_width': 'MEDIUM', + } + assert options.model_dump(by_alias=True, exclude_none=True) == options_dict + + +def test_create_rcs_options_carousel_card_width_with_each_valid_option(): + valid_widths = ['SMALL', 'MEDIUM'] + for width in valid_widths: + options = RcsOptionsCarousel( + card_width=width, + ) + options_dict = { + 'card_width': width, + } + assert options.model_dump(by_alias=True, exclude_none=True) == options_dict + + +def test_create_rcs_options_carousel_card_width_with_invalid_option(): + with pytest.raises(ValidationError) as err: + options = RcsOptionsCarousel( + card_width='INVALID_WIDTH', + ) + assert "Input should be 'SMALL' or 'MEDIUM'" in str(err.value) From 91d3d339644a327219c664be739c735fa93f8f8e Mon Sep 17 00:00:00 2001 From: superchilled Date: Mon, 16 Mar 2026 10:44:37 +0000 Subject: [PATCH 15/48] DEVX-10006: adding more RCS Card tests --- messages/src/vonage_messages/models/rcs.py | 2 +- messages/tests/test_rcs_models.py | 269 +++++++++++++++++++++ 2 files changed, 270 insertions(+), 1 deletion(-) diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index 5c77cc8f..c421fc0e 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -299,7 +299,7 @@ class RcsCard(BaseRcs): RcsSuggestionActionCreateCalendarEvent, ] ] - ] = Field(None, min_length=1, max_length=8) + ] = Field(None, min_length=1, max_length=4) rcs: Optional[RcsOptionsCard] = None message_type: MessageType = MessageType.CARD diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index e9695ef8..b616e260 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -452,6 +452,275 @@ def test_create_rcs_card_with_suggestions(): assert card.model_dump(by_alias=True, exclude_none=True) == card_dict +def test_create_rcs_cards_with_all_suggestion_types(): + card_1 = RcsCard( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + RcsSuggestionActionDial( + text='Call us', + postback_data='postback-data', + phone_number='447900000000', + ), + RcsSuggestionActionViewLocation( + text='View location', + postback_data='postback-data', + latitude='51.5074', + longitude='-0.1278', + pin_label='London', + fallback_url='https://example.com/location', + ), + RcsSuggestionActionShareLocation( + text='Share location', + postback_data='postback-data', + ), + ], + ) + card_2 = RcsCard( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[ + RcsSuggestionActionOpenUrl( + text='Open URL', + postback_data='postback-data', + url='https://example.com', + description='Click to open the URL', + ), + RcsSuggestionActionOpenUrlWebview( + text='Open URL in webview', + postback_data='postback-data', + url='https://example.com', + description='Click to open the URL in a webview', + view_mode='FULL', + ), + RcsSuggestionActionCreateCalendarEvent( + text='Add to calendar', + postback_data='postback-data', + start_time='2024-01-01T12:00:00Z', + end_time='2024-01-01T13:00:00Z', + title='Meeting with Bob', + description='Discuss project updates', + fallback_url='https://example.com/calendar-event', + ), + ], + ) + card_dict_1 = { + 'to': '1234567890', + 'from': 'asdf1234', + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'suggestions': [ + { + 'type': 'reply', + 'text': 'Reply', + 'postback_data': 'postback-data', + }, + { + 'type': 'dial', + 'text': 'Call us', + 'postback_data': 'postback-data', + 'phone_number': '447900000000', + }, + { + 'type': 'view_location', + 'text': 'View location', + 'postback_data': 'postback-data', + 'latitude': '51.5074', + 'longitude': '-0.1278', + 'pin_label': 'London', + 'fallback_url': 'https://example.com/location', + }, + { + 'type': 'share_location', + 'text': 'Share location', + 'postback_data': 'postback-data', + } + ], + 'channel': 'rcs', + 'message_type': 'card', + } + card_dict_2 = { + 'to': '1234567890', + 'from': 'asdf1234', + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'suggestions': [ + { + 'type': 'open_url', + 'text': 'Open URL', + 'postback_data': 'postback-data', + 'url': 'https://example.com', + 'description': 'Click to open the URL', + }, + { + 'type': 'open_url_in_webview', + 'text': 'Open URL in webview', + 'postback_data': 'postback-data', + 'url': 'https://example.com', + 'description': 'Click to open the URL in a webview', + 'view_mode': 'FULL', + }, + { + 'type': 'create_calendar_event', + 'text': 'Add to calendar', + 'postback_data': 'postback-data', + 'start_time': '2024-01-01T12:00:00Z', + 'end_time': '2024-01-01T13:00:00Z', + 'title': 'Meeting with Bob', + 'description': 'Discuss project updates', + 'fallback_url': 'https://example.com/calendar-event', + } + ], + 'channel': 'rcs', + 'message_type': 'card', + } + assert card_1.model_dump(by_alias=True, exclude_none=True) == card_dict_1 + assert card_2.model_dump(by_alias=True, exclude_none=True) == card_dict_2 + + +def test_create_rcs_card_without_title(): + with pytest.raises(ValidationError) as err: + card = RcsCard( + to='1234567890', + from_='asdf1234', + text='Card description', + media_url='https://example.com/image.jpg', + ) + assert "Field required" in str(err.value) + + +def test_create_rcs_card_without_text(): + with pytest.raises(ValidationError) as err: + card = RcsCard( + to='1234567890', + from_='asdf1234', + title='Card title', + media_url='https://example.com/image.jpg', + ) + assert "Field required" in str(err.value) + + +def test_create_rcs_card_without_media_url(): + with pytest.raises(ValidationError) as err: + card = RcsCard( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + ) + assert "Field required" in str(err.value) + + +def test_create_rcs_card_with_title_too_short(): + with pytest.raises(ValidationError) as err: + card = RcsCard( + to='1234567890', + from_='asdf1234', + title='', + text='Card description', + media_url='https://example.com/image.jpg', + ) + assert "String should have at least 1 character" in str(err.value) + + +def test_create_rcs_card_with_title_too_long(): + with pytest.raises(ValidationError) as err: + card = RcsCard( + to='1234567890', + from_='asdf1234', + title='A' * 200 + 'B', + text='Card description', + media_url='https://example.com/image.jpg', + ) + assert "String should have at most 200 characters" in str(err.value) + + +def test_create_rcs_card_with_text_too_short(): + with pytest.raises(ValidationError) as err: + card = RcsCard( + to='1234567890', + from_='asdf1234', + title='Card title', + text='', + media_url='https://example.com/image.jpg', + ) + assert "String should have at least 1 character" in str(err.value) + + +def test_create_rcs_card_with_text_too_long(): + with pytest.raises(ValidationError) as err: + card = RcsCard( + to='1234567890', + from_='asdf1234', + title='Card title', + text='A' * 2000 + 'B', + media_url='https://example.com/image.jpg', + ) + assert "String should have at most 2000 characters" in str(err.value) + + +def test_create_rcs_card_with_insuffient_suggestions(): + with pytest.raises(ValidationError) as err: + card = RcsCard( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[], + ) + assert "List should have at least 1 item" in str(err.value) + + +def test_create_rcs_card_with_too_many_suggestions(): + with pytest.raises(ValidationError) as err: + card = RcsCard( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + ] * 5, + ) + assert "List should have at most 4 items" in str(err.value) + + +def test_create_rcs_card_with_inavalid_suggestion_types(): + with pytest.raises(ValidationError) as err: + card = RcsCard( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + "Invalid suggestion type", + ], + ) + assert "Input should be a valid dictionary or instance" in str(err.value) + + def test_create_rcs_custom(): rcs_model = RcsCustom( to='1234567890', From 9b8fc7357b51099b596c68cf6939126110cbd3b9 Mon Sep 17 00:00:00 2001 From: superchilled Date: Mon, 16 Mar 2026 12:29:44 +0000 Subject: [PATCH 16/48] DEVX-10006: Adding tests and implementation for RCS CardContent model --- .../src/vonage_messages/models/__init__.py | 2 + messages/src/vonage_messages/models/enums.py | 1 + messages/src/vonage_messages/models/rcs.py | 53 ++- messages/tests/test_rcs_models.py | 364 ++++++++++++++++++ 4 files changed, 418 insertions(+), 2 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index 9dae982c..5f9fae1c 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -12,6 +12,8 @@ from .mms import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo from .rcs import ( RcsCustom, + RcsCarousel, + RcsCardContent, RcsCard, RcsFile, RcsImage, diff --git a/messages/src/vonage_messages/models/enums.py b/messages/src/vonage_messages/models/enums.py index 97d09582..e3baeeaa 100644 --- a/messages/src/vonage_messages/models/enums.py +++ b/messages/src/vonage_messages/models/enums.py @@ -14,6 +14,7 @@ class MessageType(str, Enum): CUSTOM = 'custom' VCARD = 'vcard' CARD = 'card' + CAROUSEL = 'carousel' class ChannelType(str, Enum): diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index c421fc0e..f798ca30 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -263,6 +263,37 @@ class RcsFile(BaseRcs): file: RcsResource message_type: MessageType = MessageType.FILE +class RcsCardContent(BaseModel): + """Model for the content of an RCS card. + + Args: + title (str): The title of the card. + text (str): The text of the card. + media_url (str): The media URL for the card. Can be an image or a video. + suggestions (List[Union[RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview, RcsSuggestionActionCreateCalendarEvent], Optional): A list of suggestions to include on the card. Can include up to 4 suggestions. + """ + + title: str = Field(..., min_length=1, max_length=200) + text: str = Field(..., min_length=1, max_length=2000) + media_url: str + media_description: Optional[str] = None + media_height: Optional[RcsMediaHeight] = None + thumbnail_url: Optional[str] = None + media_force_refresh: Optional[bool] = None + suggestions: Optional[ + List[ + Union[ + RcsSuggestionReply, + RcsSuggestionActionDial, + RcsSuggestionActionViewLocation, + RcsSuggestionActionShareLocation, + RcsSuggestionActionOpenUrl, + RcsSuggestionActionOpenUrlWebview, + RcsSuggestionActionCreateCalendarEvent, + ] + ] + ] = Field(None, min_length=1, max_length=4) + class RcsCard(BaseRcs): """Model for an RCS card message. @@ -270,8 +301,8 @@ class RcsCard(BaseRcs): Args: title (str): The title of the card. description (str): The description of the card. - media (RcsResource, Optional): The media resource for the card. Can be an image or a video. - suggestions (List[Union[RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview, RcsSuggestionActionCreateCalendarEvent], Optional): A list of suggestions to include on the card. Can include up to 8 suggestions. + media_url (str, Optional): The media URL for the card. Can be an image or a video. + suggestions (List[Union[RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview, RcsSuggestionActionCreateCalendarEvent], Optional): A list of suggestions to include on the card. Can include up to 4 suggestions. to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. from_ (str): The sender's phone number in E.164 format. Don't use a leading plus sign. ttl (int, Optional): The duration in seconds for which the message is valid. @@ -304,6 +335,24 @@ class RcsCard(BaseRcs): message_type: MessageType = MessageType.CARD +class RcsCarousel(BaseRcs): + """Model for an RCS carousel message. + + Args: + cards (List[RcsCard]): A list of cards to include in the carousel. Can include up to 10 cards. + to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. + from_ (str): The sender's phone number in E.164 format. Don't use a leading plus sign. + ttl (int, Optional): The duration in seconds for which the message is valid. + client_ref (str, Optional): An optional client reference. + webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. + webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. + """ + + cards: List[RcsCard] = Field(..., min_length=1, max_length=10) + rcs: Optional[RcsOptionsCarousel] = None + message_type: MessageType = MessageType.CAROUSEL + + class RcsCustom(BaseRcs): """Model for an RCS custom message. diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index b616e260..c57009d3 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -2,6 +2,8 @@ from pydantic import ValidationError from vonage_messages.models import ( RcsCustom, + RcsCarousel, + RcsCardContent, RcsCard, RcsFile, RcsImage, @@ -721,6 +723,368 @@ def test_create_rcs_card_with_inavalid_suggestion_types(): assert "Input should be a valid dictionary or instance" in str(err.value) +def test_create_rcs_card_content(): + card_content = RcsCardContent( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + ) + card_content_dict = { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + } + assert card_content.model_dump(by_alias=True, exclude_none=True) == card_content_dict + + +def test_create_rcs_card_content_with_optional_params(): + card_content = RcsCardContent( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_description='Image description', + media_height='MEDIUM', + thumbnail_url='https://example.com/thumbnail.jpg', + media_force_refresh=True, + ) + card_content_dict = { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'media_description': 'Image description', + 'media_height': 'MEDIUM', + 'thumbnail_url': 'https://example.com/thumbnail.jpg', + 'media_force_refresh': True, + } + assert card_content.model_dump(by_alias=True, exclude_none=True) == card_content_dict + + +def test_create_rcs_card_with_suggestions(): + card_content = RcsCardContent( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + RcsSuggestionActionDial( + text='Call us', + postback_data='postback-data', + phone_number='447900000000', + ), + ], + ) + card_content_dict = { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'suggestions': [ + { + 'type': 'reply', + 'text': 'Reply', + 'postback_data': 'postback-data', + }, + { + 'type': 'dial', + 'text': 'Call us', + 'postback_data': 'postback-data', + 'phone_number': '447900000000', + }, + ], + } + assert card_content.model_dump(by_alias=True, exclude_none=True) == card_content_dict + + +def test_create_rcs_cards_with_all_suggestion_types(): + card_content_1 = RcsCardContent( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + RcsSuggestionActionDial( + text='Call us', + postback_data='postback-data', + phone_number='447900000000', + ), + RcsSuggestionActionViewLocation( + text='View location', + postback_data='postback-data', + latitude='51.5074', + longitude='-0.1278', + pin_label='London', + fallback_url='https://example.com/location', + ), + RcsSuggestionActionShareLocation( + text='Share location', + postback_data='postback-data', + ), + ], + ) + card_content_2 = RcsCardContent( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[ + RcsSuggestionActionOpenUrl( + text='Open URL', + postback_data='postback-data', + url='https://example.com', + description='Click to open the URL', + ), + RcsSuggestionActionOpenUrlWebview( + text='Open URL in webview', + postback_data='postback-data', + url='https://example.com', + description='Click to open the URL in a webview', + view_mode='FULL', + ), + RcsSuggestionActionCreateCalendarEvent( + text='Add to calendar', + postback_data='postback-data', + start_time='2024-01-01T12:00:00Z', + end_time='2024-01-01T13:00:00Z', + title='Meeting with Bob', + description='Discuss project updates', + fallback_url='https://example.com/calendar-event', + ), + ], + ) + card_content_dict_1 = { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'suggestions': [ + { + 'type': 'reply', + 'text': 'Reply', + 'postback_data': 'postback-data', + }, + { + 'type': 'dial', + 'text': 'Call us', + 'postback_data': 'postback-data', + 'phone_number': '447900000000', + }, + { + 'type': 'view_location', + 'text': 'View location', + 'postback_data': 'postback-data', + 'latitude': '51.5074', + 'longitude': '-0.1278', + 'pin_label': 'London', + 'fallback_url': 'https://example.com/location', + }, + { + 'type': 'share_location', + 'text': 'Share location', + 'postback_data': 'postback-data', + } + ] + } + card_content_dict_2 = { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'suggestions': [ + { + 'type': 'open_url', + 'text': 'Open URL', + 'postback_data': 'postback-data', + 'url': 'https://example.com', + 'description': 'Click to open the URL', + }, + { + 'type': 'open_url_in_webview', + 'text': 'Open URL in webview', + 'postback_data': 'postback-data', + 'url': 'https://example.com', + 'description': 'Click to open the URL in a webview', + 'view_mode': 'FULL', + }, + { + 'type': 'create_calendar_event', + 'text': 'Add to calendar', + 'postback_data': 'postback-data', + 'start_time': '2024-01-01T12:00:00Z', + 'end_time': '2024-01-01T13:00:00Z', + 'title': 'Meeting with Bob', + 'description': 'Discuss project updates', + 'fallback_url': 'https://example.com/calendar-event', + } + ] + } + assert card_content_1.model_dump(by_alias=True, exclude_none=True) == card_content_dict_1 + assert card_content_2.model_dump(by_alias=True, exclude_none=True) == card_content_dict_2 + + +def test_create_rcs_card_content_without_title(): + with pytest.raises(ValidationError) as err: + card = RcsCardContent( + to='1234567890', + from_='asdf1234', + text='Card description', + media_url='https://example.com/image.jpg', + ) + assert "Field required" in str(err.value) + + +def test_create_rcs_card_content_without_text(): + with pytest.raises(ValidationError) as err: + card = RcsCardContent( + to='1234567890', + from_='asdf1234', + title='Card title', + media_url='https://example.com/image.jpg', + ) + assert "Field required" in str(err.value) + + +def test_create_rcs_card_content_without_media_url(): + with pytest.raises(ValidationError) as err: + card = RcsCardContent( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + ) + assert "Field required" in str(err.value) + + +def test_create_rcs_card_content_with_title_too_short(): + with pytest.raises(ValidationError) as err: + card = RcsCardContent( + to='1234567890', + from_='asdf1234', + title='', + text='Card description', + media_url='https://example.com/image.jpg', + ) + assert "String should have at least 1 character" in str(err.value) + + +def test_create_rcs_card_content_with_title_too_long(): + with pytest.raises(ValidationError) as err: + card = RcsCardContent( + to='1234567890', + from_='asdf1234', + title='A' * 200 + 'B', + text='Card description', + media_url='https://example.com/image.jpg', + ) + assert "String should have at most 200 characters" in str(err.value) + + +def test_create_rcs_card_content_with_text_too_short(): + with pytest.raises(ValidationError) as err: + card = RcsCardContent( + to='1234567890', + from_='asdf1234', + title='Card title', + text='', + media_url='https://example.com/image.jpg', + ) + assert "String should have at least 1 character" in str(err.value) + + +def test_create_rcs_card_content_with_text_too_long(): + with pytest.raises(ValidationError) as err: + card = RcsCardContent( + to='1234567890', + from_='asdf1234', + title='Card title', + text='A' * 2000 + 'B', + media_url='https://example.com/image.jpg', + ) + assert "String should have at most 2000 characters" in str(err.value) + + +def test_create_rcs_card_content_with_insuffient_suggestions(): + with pytest.raises(ValidationError) as err: + card = RcsCardContent( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[], + ) + assert "List should have at least 1 item" in str(err.value) + + +def test_create_rcs_card_content_with_too_many_suggestions(): + with pytest.raises(ValidationError) as err: + card = RcsCardContent( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + ] * 5, + ) + assert "List should have at most 4 items" in str(err.value) + + +def test_create_rcs_card_content_with_inavalid_suggestion_types(): + with pytest.raises(ValidationError) as err: + card = RcsCardContent( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + "Invalid suggestion type", + ], + ) + assert "Input should be a valid dictionary or instance" in str(err.value) + + +@pytest.mark.skip(reason="Not fully implemented yet") +def test_create_rcs_carousel(): + carousel = RcsCarousel( + cards=[ + RcsCard( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + ) + ] * 2, + ) + carousel_dict = { + 'cards': [ + { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'channel': 'rcs', + 'message_type': 'card', + } + ] * 2, + 'channel': 'rcs', + 'message_type': 'carousel', + } + assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict + + def test_create_rcs_custom(): rcs_model = RcsCustom( to='1234567890', From 5a2b335534fa8d595f0e8f59d61d52186b22488a Mon Sep 17 00:00:00 2001 From: superchilled Date: Mon, 16 Mar 2026 12:37:57 +0000 Subject: [PATCH 17/48] DEVX-10006: Refactoring RCS Card model --- messages/src/vonage_messages/models/rcs.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index f798ca30..5387a7ff 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -295,7 +295,7 @@ class RcsCardContent(BaseModel): ] = Field(None, min_length=1, max_length=4) -class RcsCard(BaseRcs): +class RcsCard(RcsCardContent, BaseRcs): """Model for an RCS card message. Args: @@ -311,26 +311,6 @@ class RcsCard(BaseRcs): webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. """ - title: str = Field(..., min_length=1, max_length=200) - text: str = Field(..., min_length=1, max_length=2000) - media_url: str - media_description: Optional[str] = None - media_height: Optional[RcsMediaHeight] = None - thumbnail_url: Optional[str] = None - media_force_refresh: Optional[bool] = None - suggestions: Optional[ - List[ - Union[ - RcsSuggestionReply, - RcsSuggestionActionDial, - RcsSuggestionActionViewLocation, - RcsSuggestionActionShareLocation, - RcsSuggestionActionOpenUrl, - RcsSuggestionActionOpenUrlWebview, - RcsSuggestionActionCreateCalendarEvent, - ] - ] - ] = Field(None, min_length=1, max_length=4) rcs: Optional[RcsOptionsCard] = None message_type: MessageType = MessageType.CARD From 0f313067b4d5c8fb6672b6d7efb92f9a910370cc Mon Sep 17 00:00:00 2001 From: superchilled Date: Mon, 16 Mar 2026 12:52:08 +0000 Subject: [PATCH 18/48] DEVX-10006: Adding initial RCS Carousel tests and implementation --- messages/src/vonage_messages/models/rcs.py | 2 +- messages/tests/test_rcs_models.py | 73 ++++++++++++++-------- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index 5387a7ff..49886ffc 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -328,7 +328,7 @@ class RcsCarousel(BaseRcs): webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. """ - cards: List[RcsCard] = Field(..., min_length=1, max_length=10) + cards: List[RcsCardContent] = Field(..., min_length=1, max_length=10) rcs: Optional[RcsOptionsCarousel] = None message_type: MessageType = MessageType.CAROUSEL diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index c57009d3..712674d1 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -928,8 +928,6 @@ def test_create_rcs_cards_with_all_suggestion_types(): def test_create_rcs_card_content_without_title(): with pytest.raises(ValidationError) as err: card = RcsCardContent( - to='1234567890', - from_='asdf1234', text='Card description', media_url='https://example.com/image.jpg', ) @@ -939,8 +937,6 @@ def test_create_rcs_card_content_without_title(): def test_create_rcs_card_content_without_text(): with pytest.raises(ValidationError) as err: card = RcsCardContent( - to='1234567890', - from_='asdf1234', title='Card title', media_url='https://example.com/image.jpg', ) @@ -950,8 +946,6 @@ def test_create_rcs_card_content_without_text(): def test_create_rcs_card_content_without_media_url(): with pytest.raises(ValidationError) as err: card = RcsCardContent( - to='1234567890', - from_='asdf1234', title='Card title', text='Card description', ) @@ -961,8 +955,6 @@ def test_create_rcs_card_content_without_media_url(): def test_create_rcs_card_content_with_title_too_short(): with pytest.raises(ValidationError) as err: card = RcsCardContent( - to='1234567890', - from_='asdf1234', title='', text='Card description', media_url='https://example.com/image.jpg', @@ -973,8 +965,6 @@ def test_create_rcs_card_content_with_title_too_short(): def test_create_rcs_card_content_with_title_too_long(): with pytest.raises(ValidationError) as err: card = RcsCardContent( - to='1234567890', - from_='asdf1234', title='A' * 200 + 'B', text='Card description', media_url='https://example.com/image.jpg', @@ -985,8 +975,6 @@ def test_create_rcs_card_content_with_title_too_long(): def test_create_rcs_card_content_with_text_too_short(): with pytest.raises(ValidationError) as err: card = RcsCardContent( - to='1234567890', - from_='asdf1234', title='Card title', text='', media_url='https://example.com/image.jpg', @@ -997,8 +985,6 @@ def test_create_rcs_card_content_with_text_too_short(): def test_create_rcs_card_content_with_text_too_long(): with pytest.raises(ValidationError) as err: card = RcsCardContent( - to='1234567890', - from_='asdf1234', title='Card title', text='A' * 2000 + 'B', media_url='https://example.com/image.jpg', @@ -1009,8 +995,6 @@ def test_create_rcs_card_content_with_text_too_long(): def test_create_rcs_card_content_with_insuffient_suggestions(): with pytest.raises(ValidationError) as err: card = RcsCardContent( - to='1234567890', - from_='asdf1234', title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -1022,8 +1006,6 @@ def test_create_rcs_card_content_with_insuffient_suggestions(): def test_create_rcs_card_content_with_too_many_suggestions(): with pytest.raises(ValidationError) as err: card = RcsCardContent( - to='1234567890', - from_='asdf1234', title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -1040,8 +1022,6 @@ def test_create_rcs_card_content_with_too_many_suggestions(): def test_create_rcs_card_content_with_inavalid_suggestion_types(): with pytest.raises(ValidationError) as err: card = RcsCardContent( - to='1234567890', - from_='asdf1234', title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -1056,13 +1036,12 @@ def test_create_rcs_card_content_with_inavalid_suggestion_types(): assert "Input should be a valid dictionary or instance" in str(err.value) -@pytest.mark.skip(reason="Not fully implemented yet") def test_create_rcs_carousel(): carousel = RcsCarousel( + to='1234567890', + from_='asdf1234', cards=[ - RcsCard( - to='1234567890', - from_='asdf1234', + RcsCardContent( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -1070,13 +1049,13 @@ def test_create_rcs_carousel(): ] * 2, ) carousel_dict = { + 'to': '1234567890', + 'from': 'asdf1234', 'cards': [ { 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', - 'channel': 'rcs', - 'message_type': 'card', } ] * 2, 'channel': 'rcs', @@ -1085,6 +1064,48 @@ def test_create_rcs_carousel(): assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict +def test_create_rcs_carousel_with_optional_params(): + carousel = RcsCarousel( + to='1234567890', + from_='asdf1234', + cards=[ + RcsCardContent( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_description='Image description', + media_height='MEDIUM', + thumbnail_url='https://example.com/thumbnail.jpg', + media_force_refresh=True, + ) + ] * 2, + rcs=RcsOptionsCarousel( + card_width='MEDIUM', + ), + ) + carousel_dict = { + 'to': '1234567890', + 'from': 'asdf1234', + 'cards': [ + { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'media_description': 'Image description', + 'media_height': 'MEDIUM', + 'thumbnail_url': 'https://example.com/thumbnail.jpg', + 'media_force_refresh': True, + } + ] * 2, + 'rcs': { + 'card_width': 'MEDIUM', + }, + 'channel': 'rcs', + 'message_type': 'carousel', + } + assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict + + def test_create_rcs_custom(): rcs_model = RcsCustom( to='1234567890', From 9bf8e0efc55e0d17c398819f665089f084285bd1 Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 17 Mar 2026 12:45:13 +0000 Subject: [PATCH 19/48] DEVX-10006: Fixing RCS Card models and tests --- .../src/vonage_messages/models/__init__.py | 5 +- messages/src/vonage_messages/models/rcs.py | 33 +- messages/tests/test_rcs_models.py | 696 ++++++++++++------ 3 files changed, 504 insertions(+), 230 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index 5f9fae1c..e10e4e52 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -13,8 +13,9 @@ from .rcs import ( RcsCustom, RcsCarousel, - RcsCardContent, - RcsCard, + RcsCardItem, + RcsCardMessage, + RcsCardBase, RcsFile, RcsImage, RcsResource, diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index 49886ffc..0256f054 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -263,7 +263,8 @@ class RcsFile(BaseRcs): file: RcsResource message_type: MessageType = MessageType.FILE -class RcsCardContent(BaseModel): + +class RcsCardBase(BaseModel): """Model for the content of an RCS card. Args: @@ -295,7 +296,20 @@ class RcsCardContent(BaseModel): ] = Field(None, min_length=1, max_length=4) -class RcsCard(RcsCardContent, BaseRcs): +class RcsCardItem(RcsCardBase): + """Model for the content of an RCS card. + + Args: + title (str): The title of the card. + text (str): The text of the card. + media_url (str): The media URL for the card. Can be an image or a video. + suggestions (List[Union[RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview, RcsSuggestionActionCreateCalendarEvent], Optional): A list of suggestions to include on the card. Can include up to 4 suggestions. + """ + + media_height: RcsMediaHeight + + +class RcsCardMessage(RcsCardBase, BaseRcs): """Model for an RCS card message. Args: @@ -328,7 +342,20 @@ class RcsCarousel(BaseRcs): webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. """ - cards: List[RcsCardContent] = Field(..., min_length=1, max_length=10) + cards: List[RcsCardItem] = Field(..., min_length=1, max_length=10) + suggestions: Optional[ + List[ + Union[ + RcsSuggestionReply, + RcsSuggestionActionDial, + RcsSuggestionActionViewLocation, + RcsSuggestionActionShareLocation, + RcsSuggestionActionOpenUrl, + RcsSuggestionActionOpenUrlWebview, + RcsSuggestionActionCreateCalendarEvent, + ] + ] + ] = Field(None, min_length=1, max_length=11) rcs: Optional[RcsOptionsCarousel] = None message_type: MessageType = MessageType.CAROUSEL diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 712674d1..ea365280 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -3,8 +3,9 @@ from vonage_messages.models import ( RcsCustom, RcsCarousel, - RcsCardContent, - RcsCard, + RcsCardItem, + RcsCardMessage, + RcsCardBase, RcsFile, RcsImage, RcsResource, @@ -353,31 +354,22 @@ def test_create_rcs_file(): assert rcs_model.model_dump(by_alias=True, exclude_none=True) == rcs_dict - -def test_create_rcs_card(): - card = RcsCard( - to='1234567890', - from_='asdf1234', +def test_create_rcs_card_base(): + card_base = RcsCardBase( title='Card title', text='Card description', media_url='https://example.com/image.jpg', ) - card_dict = { - 'to': '1234567890', - 'from': 'asdf1234', + card_base_dict = { 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', - 'channel': 'rcs', - 'message_type': 'card', } - assert card.model_dump(by_alias=True, exclude_none=True) == card_dict + assert card_base.model_dump(by_alias=True, exclude_none=True) == card_base_dict -def test_create_rcs_card_with_optional_params(): - card = RcsCard( - to='1234567890', - from_='asdf1234', +def test_create_rcs_card_base_with_optional_params(): + card_base = RcsCardBase( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -385,14 +377,8 @@ def test_create_rcs_card_with_optional_params(): media_height='MEDIUM', thumbnail_url='https://example.com/thumbnail.jpg', media_force_refresh=True, - rcs=RcsOptionsCard( - card_orientation='VERTICAL', - image_alignment='LEFT', - ), ) - card_dict = { - 'to': '1234567890', - 'from': 'asdf1234', + card_base_dict = { 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', @@ -400,20 +386,12 @@ def test_create_rcs_card_with_optional_params(): 'media_height': 'MEDIUM', 'thumbnail_url': 'https://example.com/thumbnail.jpg', 'media_force_refresh': True, - 'rcs': { - 'card_orientation': 'VERTICAL', - 'image_alignment': 'LEFT', - }, - 'channel': 'rcs', - 'message_type': 'card', } - assert card.model_dump(by_alias=True, exclude_none=True) == card_dict + assert card_base.model_dump(by_alias=True, exclude_none=True) == card_base_dict -def test_create_rcs_card_with_suggestions(): - card = RcsCard( - to='1234567890', - from_='asdf1234', +def test_create_rcs_card_base_with_suggestions(): + card_base = RcsCardBase( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -429,9 +407,7 @@ def test_create_rcs_card_with_suggestions(): ), ], ) - card_dict = { - 'to': '1234567890', - 'from': 'asdf1234', + card_base_dict = { 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', @@ -448,16 +424,12 @@ def test_create_rcs_card_with_suggestions(): 'phone_number': '447900000000', }, ], - 'channel': 'rcs', - 'message_type': 'card', } - assert card.model_dump(by_alias=True, exclude_none=True) == card_dict + assert card_base.model_dump(by_alias=True, exclude_none=True) == card_base_dict -def test_create_rcs_cards_with_all_suggestion_types(): - card_1 = RcsCard( - to='1234567890', - from_='asdf1234', +def test_create_rcs_card_base_with_all_suggestion_types(): + card_base_1 = RcsCardBase( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -485,9 +457,7 @@ def test_create_rcs_cards_with_all_suggestion_types(): ), ], ) - card_2 = RcsCard( - to='1234567890', - from_='asdf1234', + card_base_2 = RcsCardBase( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -516,9 +486,7 @@ def test_create_rcs_cards_with_all_suggestion_types(): ), ], ) - card_dict_1 = { - 'to': '1234567890', - 'from': 'asdf1234', + card_base_dict_1 = { 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', @@ -549,12 +517,8 @@ def test_create_rcs_cards_with_all_suggestion_types(): 'postback_data': 'postback-data', } ], - 'channel': 'rcs', - 'message_type': 'card', } - card_dict_2 = { - 'to': '1234567890', - 'from': 'asdf1234', + card_base_dict_2 = { 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', @@ -585,51 +549,41 @@ def test_create_rcs_cards_with_all_suggestion_types(): 'fallback_url': 'https://example.com/calendar-event', } ], - 'channel': 'rcs', - 'message_type': 'card', } - assert card_1.model_dump(by_alias=True, exclude_none=True) == card_dict_1 - assert card_2.model_dump(by_alias=True, exclude_none=True) == card_dict_2 + assert card_base_1.model_dump(by_alias=True, exclude_none=True) == card_base_dict_1 + assert card_base_2.model_dump(by_alias=True, exclude_none=True) == card_base_dict_2 -def test_create_rcs_card_without_title(): +def test_create_rcs_card_base_without_title(): with pytest.raises(ValidationError) as err: - card = RcsCard( - to='1234567890', - from_='asdf1234', + card_base = RcsCardBase( text='Card description', media_url='https://example.com/image.jpg', ) assert "Field required" in str(err.value) -def test_create_rcs_card_without_text(): +def test_create_rcs_card_base_without_text(): with pytest.raises(ValidationError) as err: - card = RcsCard( - to='1234567890', - from_='asdf1234', + card_base = RcsCardBase( title='Card title', media_url='https://example.com/image.jpg', ) assert "Field required" in str(err.value) -def test_create_rcs_card_without_media_url(): +def test_create_rcs_card_base_without_media_url(): with pytest.raises(ValidationError) as err: - card = RcsCard( - to='1234567890', - from_='asdf1234', + card_base = RcsCardBase( title='Card title', text='Card description', ) assert "Field required" in str(err.value) -def test_create_rcs_card_with_title_too_short(): +def test_create_rcs_card_base_with_title_too_short(): with pytest.raises(ValidationError) as err: - card = RcsCard( - to='1234567890', - from_='asdf1234', + card_base = RcsCardBase( title='', text='Card description', media_url='https://example.com/image.jpg', @@ -637,11 +591,9 @@ def test_create_rcs_card_with_title_too_short(): assert "String should have at least 1 character" in str(err.value) -def test_create_rcs_card_with_title_too_long(): +def test_create_rcs_card_base_with_title_too_long(): with pytest.raises(ValidationError) as err: - card = RcsCard( - to='1234567890', - from_='asdf1234', + card_base = RcsCardBase( title='A' * 200 + 'B', text='Card description', media_url='https://example.com/image.jpg', @@ -649,9 +601,9 @@ def test_create_rcs_card_with_title_too_long(): assert "String should have at most 200 characters" in str(err.value) -def test_create_rcs_card_with_text_too_short(): +def test_create_rcs_card_base_with_text_too_short(): with pytest.raises(ValidationError) as err: - card = RcsCard( + card_base = RcsCardBase( to='1234567890', from_='asdf1234', title='Card title', @@ -661,9 +613,9 @@ def test_create_rcs_card_with_text_too_short(): assert "String should have at least 1 character" in str(err.value) -def test_create_rcs_card_with_text_too_long(): +def test_create_rcs_card_base_with_text_too_long(): with pytest.raises(ValidationError) as err: - card = RcsCard( + card_base = RcsCardBase( to='1234567890', from_='asdf1234', title='Card title', @@ -673,11 +625,9 @@ def test_create_rcs_card_with_text_too_long(): assert "String should have at most 2000 characters" in str(err.value) -def test_create_rcs_card_with_insuffient_suggestions(): +def test_create_rcs_card_base_with_insuffient_suggestions(): with pytest.raises(ValidationError) as err: - card = RcsCard( - to='1234567890', - from_='asdf1234', + card_base = RcsCardBase( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -686,11 +636,9 @@ def test_create_rcs_card_with_insuffient_suggestions(): assert "List should have at least 1 item" in str(err.value) -def test_create_rcs_card_with_too_many_suggestions(): +def test_create_rcs_card_base_with_too_many_suggestions(): with pytest.raises(ValidationError) as err: - card = RcsCard( - to='1234567890', - from_='asdf1234', + card_base = RcsCardBase( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -704,11 +652,9 @@ def test_create_rcs_card_with_too_many_suggestions(): assert "List should have at most 4 items" in str(err.value) -def test_create_rcs_card_with_inavalid_suggestion_types(): +def test_create_rcs_card_base_with_inavalid_suggestion_types(): with pytest.raises(ValidationError) as err: - card = RcsCard( - to='1234567890', - from_='asdf1234', + card_base = RcsCardBase( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -723,22 +669,30 @@ def test_create_rcs_card_with_inavalid_suggestion_types(): assert "Input should be a valid dictionary or instance" in str(err.value) -def test_create_rcs_card_content(): - card_content = RcsCardContent( +def test_create_rcs_card_message(): + card = RcsCardMessage( + to='1234567890', + from_='asdf1234', title='Card title', text='Card description', media_url='https://example.com/image.jpg', ) - card_content_dict = { + card_dict = { + 'to': '1234567890', + 'from': 'asdf1234', 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', + 'channel': 'rcs', + 'message_type': 'card', } - assert card_content.model_dump(by_alias=True, exclude_none=True) == card_content_dict + assert card.model_dump(by_alias=True, exclude_none=True) == card_dict -def test_create_rcs_card_content_with_optional_params(): - card_content = RcsCardContent( +def test_create_rcs_card_message_with_optional_params(): + card = RcsCardMessage( + to='1234567890', + from_='asdf1234', title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -746,8 +700,14 @@ def test_create_rcs_card_content_with_optional_params(): media_height='MEDIUM', thumbnail_url='https://example.com/thumbnail.jpg', media_force_refresh=True, + rcs=RcsOptionsCard( + card_orientation='VERTICAL', + image_alignment='LEFT', + ), ) - card_content_dict = { + card_dict = { + 'to': '1234567890', + 'from': 'asdf1234', 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', @@ -755,12 +715,20 @@ def test_create_rcs_card_content_with_optional_params(): 'media_height': 'MEDIUM', 'thumbnail_url': 'https://example.com/thumbnail.jpg', 'media_force_refresh': True, + 'rcs': { + 'card_orientation': 'VERTICAL', + 'image_alignment': 'LEFT', + }, + 'channel': 'rcs', + 'message_type': 'card', } - assert card_content.model_dump(by_alias=True, exclude_none=True) == card_content_dict + assert card.model_dump(by_alias=True, exclude_none=True) == card_dict -def test_create_rcs_card_with_suggestions(): - card_content = RcsCardContent( +def test_create_rcs_card_message_with_suggestions(): + card = RcsCardMessage( + to='1234567890', + from_='asdf1234', title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -776,7 +744,9 @@ def test_create_rcs_card_with_suggestions(): ), ], ) - card_content_dict = { + card_dict = { + 'to': '1234567890', + 'from': 'asdf1234', 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', @@ -793,17 +763,187 @@ def test_create_rcs_card_with_suggestions(): 'phone_number': '447900000000', }, ], + 'channel': 'rcs', + 'message_type': 'card', } - assert card_content.model_dump(by_alias=True, exclude_none=True) == card_content_dict + assert card.model_dump(by_alias=True, exclude_none=True) == card_dict -def test_create_rcs_cards_with_all_suggestion_types(): - card_content_1 = RcsCardContent( - to='1234567890', - from_='asdf1234', +def test_create_rcs_card_message_without_title(): + with pytest.raises(ValidationError) as err: + card = RcsCardMessage( + to='1234567890', + from_='asdf1234', + text='Card description', + media_url='https://example.com/image.jpg', + ) + assert "Field required" in str(err.value) + + +def test_create_rcs_card_message_without_text(): + with pytest.raises(ValidationError) as err: + card = RcsCardMessage( + to='1234567890', + from_='asdf1234', + title='Card title', + media_url='https://example.com/image.jpg', + ) + assert "Field required" in str(err.value) + + +def test_create_rcs_card_message_without_media_url(): + with pytest.raises(ValidationError) as err: + card = RcsCardMessage( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + ) + assert "Field required" in str(err.value) + + +def test_create_rcs_card_message_with_title_too_short(): + with pytest.raises(ValidationError) as err: + card = RcsCardMessage( + to='1234567890', + from_='asdf1234', + title='', + text='Card description', + media_url='https://example.com/image.jpg', + ) + assert "String should have at least 1 character" in str(err.value) + + +def test_create_rcs_card_message_with_title_too_long(): + with pytest.raises(ValidationError) as err: + card = RcsCardMessage( + to='1234567890', + from_='asdf1234', + title='A' * 200 + 'B', + text='Card description', + media_url='https://example.com/image.jpg', + ) + assert "String should have at most 200 characters" in str(err.value) + + +def test_create_rcs_card_message_with_text_too_short(): + with pytest.raises(ValidationError) as err: + card = RcsCardMessage( + to='1234567890', + from_='asdf1234', + title='Card title', + text='', + media_url='https://example.com/image.jpg', + ) + assert "String should have at least 1 character" in str(err.value) + + +def test_create_rcs_card_message_with_text_too_long(): + with pytest.raises(ValidationError) as err: + card = RcsCardMessage( + to='1234567890', + from_='asdf1234', + title='Card title', + text='A' * 2000 + 'B', + media_url='https://example.com/image.jpg', + ) + assert "String should have at most 2000 characters" in str(err.value) + + +def test_create_rcs_card_message_with_insuffient_suggestions(): + with pytest.raises(ValidationError) as err: + card = RcsCardMessage( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[], + ) + assert "List should have at least 1 item" in str(err.value) + + +def test_create_rcs_card_message_with_too_many_suggestions(): + with pytest.raises(ValidationError) as err: + card = RcsCardMessage( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + ] * 5, + ) + assert "List should have at most 4 items" in str(err.value) + + +def test_create_rcs_card_message_with_inavalid_suggestion_types(): + with pytest.raises(ValidationError) as err: + card = RcsCardMessage( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + "Invalid suggestion type", + ], + ) + assert "Input should be a valid dictionary or instance" in str(err.value) + + +def test_create_rcs_card_item(): + card_content = RcsCardItem( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + card_item_dict = { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'media_height': 'MEDIUM', + } + assert card_content.model_dump(by_alias=True, exclude_none=True) == card_item_dict + + +def test_create_rcs_card_item_with_optional_params(): + card_content = RcsCardItem( title='Card title', text='Card description', media_url='https://example.com/image.jpg', + media_description='Image description', + media_height='MEDIUM', + thumbnail_url='https://example.com/thumbnail.jpg', + media_force_refresh=True, + ) + card_item_dict = { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'media_description': 'Image description', + 'media_height': 'MEDIUM', + 'thumbnail_url': 'https://example.com/thumbnail.jpg', + 'media_force_refresh': True, + } + assert card_content.model_dump(by_alias=True, exclude_none=True) == card_item_dict + + +def test_create_rcs_card_item_with_suggestions(): + card_content = RcsCardItem( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', suggestions=[ RcsSuggestionReply( text='Reply', @@ -814,53 +954,13 @@ def test_create_rcs_cards_with_all_suggestion_types(): postback_data='postback-data', phone_number='447900000000', ), - RcsSuggestionActionViewLocation( - text='View location', - postback_data='postback-data', - latitude='51.5074', - longitude='-0.1278', - pin_label='London', - fallback_url='https://example.com/location', - ), - RcsSuggestionActionShareLocation( - text='Share location', - postback_data='postback-data', - ), ], ) - card_content_2 = RcsCardContent( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - suggestions=[ - RcsSuggestionActionOpenUrl( - text='Open URL', - postback_data='postback-data', - url='https://example.com', - description='Click to open the URL', - ), - RcsSuggestionActionOpenUrlWebview( - text='Open URL in webview', - postback_data='postback-data', - url='https://example.com', - description='Click to open the URL in a webview', - view_mode='FULL', - ), - RcsSuggestionActionCreateCalendarEvent( - text='Add to calendar', - postback_data='postback-data', - start_time='2024-01-01T12:00:00Z', - end_time='2024-01-01T13:00:00Z', - title='Meeting with Bob', - description='Discuss project updates', - fallback_url='https://example.com/calendar-event', - ), - ], - ) - card_content_dict_1 = { + card_item_dict = { 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', + 'media_height': 'MEDIUM', 'suggestions': [ { 'type': 'reply', @@ -873,88 +973,51 @@ def test_create_rcs_cards_with_all_suggestion_types(): 'postback_data': 'postback-data', 'phone_number': '447900000000', }, - { - 'type': 'view_location', - 'text': 'View location', - 'postback_data': 'postback-data', - 'latitude': '51.5074', - 'longitude': '-0.1278', - 'pin_label': 'London', - 'fallback_url': 'https://example.com/location', - }, - { - 'type': 'share_location', - 'text': 'Share location', - 'postback_data': 'postback-data', - } - ] - } - card_content_dict_2 = { - 'title': 'Card title', - 'text': 'Card description', - 'media_url': 'https://example.com/image.jpg', - 'suggestions': [ - { - 'type': 'open_url', - 'text': 'Open URL', - 'postback_data': 'postback-data', - 'url': 'https://example.com', - 'description': 'Click to open the URL', - }, - { - 'type': 'open_url_in_webview', - 'text': 'Open URL in webview', - 'postback_data': 'postback-data', - 'url': 'https://example.com', - 'description': 'Click to open the URL in a webview', - 'view_mode': 'FULL', - }, - { - 'type': 'create_calendar_event', - 'text': 'Add to calendar', - 'postback_data': 'postback-data', - 'start_time': '2024-01-01T12:00:00Z', - 'end_time': '2024-01-01T13:00:00Z', - 'title': 'Meeting with Bob', - 'description': 'Discuss project updates', - 'fallback_url': 'https://example.com/calendar-event', - } - ] + ], } - assert card_content_1.model_dump(by_alias=True, exclude_none=True) == card_content_dict_1 - assert card_content_2.model_dump(by_alias=True, exclude_none=True) == card_content_dict_2 + assert card_content.model_dump(by_alias=True, exclude_none=True) == card_item_dict -def test_create_rcs_card_content_without_title(): +def test_create_rcs_card_item_without_title(): with pytest.raises(ValidationError) as err: - card = RcsCardContent( + card = RcsCardItem( text='Card description', media_url='https://example.com/image.jpg', ) assert "Field required" in str(err.value) -def test_create_rcs_card_content_without_text(): +def test_create_rcs_card_item_without_text(): with pytest.raises(ValidationError) as err: - card = RcsCardContent( + card = RcsCardItem( title='Card title', media_url='https://example.com/image.jpg', ) assert "Field required" in str(err.value) -def test_create_rcs_card_content_without_media_url(): +def test_create_rcs_card_item_without_media_url(): + with pytest.raises(ValidationError) as err: + card = RcsCardItem( + title='Card title', + text='Card description', + ) + assert "Field required" in str(err.value) + + +def test_create_rcs_card_item_without_media_height(): with pytest.raises(ValidationError) as err: - card = RcsCardContent( + card = RcsCardItem( title='Card title', text='Card description', + media_url='https://example.com/image.jpg' ) assert "Field required" in str(err.value) -def test_create_rcs_card_content_with_title_too_short(): +def test_create_rcs_card_item_with_title_too_short(): with pytest.raises(ValidationError) as err: - card = RcsCardContent( + card = RcsCardItem( title='', text='Card description', media_url='https://example.com/image.jpg', @@ -962,9 +1025,9 @@ def test_create_rcs_card_content_with_title_too_short(): assert "String should have at least 1 character" in str(err.value) -def test_create_rcs_card_content_with_title_too_long(): +def test_create_rcs_card_item_with_title_too_long(): with pytest.raises(ValidationError) as err: - card = RcsCardContent( + card = RcsCardItem( title='A' * 200 + 'B', text='Card description', media_url='https://example.com/image.jpg', @@ -972,9 +1035,9 @@ def test_create_rcs_card_content_with_title_too_long(): assert "String should have at most 200 characters" in str(err.value) -def test_create_rcs_card_content_with_text_too_short(): +def test_create_rcs_card_item_with_text_too_short(): with pytest.raises(ValidationError) as err: - card = RcsCardContent( + card = RcsCardItem( title='Card title', text='', media_url='https://example.com/image.jpg', @@ -982,9 +1045,9 @@ def test_create_rcs_card_content_with_text_too_short(): assert "String should have at least 1 character" in str(err.value) -def test_create_rcs_card_content_with_text_too_long(): +def test_create_rcs_card_item_with_text_too_long(): with pytest.raises(ValidationError) as err: - card = RcsCardContent( + card = RcsCardItem( title='Card title', text='A' * 2000 + 'B', media_url='https://example.com/image.jpg', @@ -992,9 +1055,9 @@ def test_create_rcs_card_content_with_text_too_long(): assert "String should have at most 2000 characters" in str(err.value) -def test_create_rcs_card_content_with_insuffient_suggestions(): +def test_create_rcs_card_item_with_insuffient_suggestions(): with pytest.raises(ValidationError) as err: - card = RcsCardContent( + card = RcsCardItem( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -1003,9 +1066,9 @@ def test_create_rcs_card_content_with_insuffient_suggestions(): assert "List should have at least 1 item" in str(err.value) -def test_create_rcs_card_content_with_too_many_suggestions(): +def test_create_rcs_card_item_with_too_many_suggestions(): with pytest.raises(ValidationError) as err: - card = RcsCardContent( + card = RcsCardItem( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -1019,9 +1082,9 @@ def test_create_rcs_card_content_with_too_many_suggestions(): assert "List should have at most 4 items" in str(err.value) -def test_create_rcs_card_content_with_inavalid_suggestion_types(): +def test_create_rcs_card_item_with_inavalid_suggestion_types(): with pytest.raises(ValidationError) as err: - card = RcsCardContent( + card = RcsCardItem( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -1041,10 +1104,11 @@ def test_create_rcs_carousel(): to='1234567890', from_='asdf1234', cards=[ - RcsCardContent( + RcsCardItem( title='Card title', text='Card description', media_url='https://example.com/image.jpg', + media_height='MEDIUM', ) ] * 2, ) @@ -1056,6 +1120,7 @@ def test_create_rcs_carousel(): 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', + 'media_height': 'MEDIUM', } ] * 2, 'channel': 'rcs', @@ -1069,7 +1134,7 @@ def test_create_rcs_carousel_with_optional_params(): to='1234567890', from_='asdf1234', cards=[ - RcsCardContent( + RcsCardItem( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -1106,6 +1171,187 @@ def test_create_rcs_carousel_with_optional_params(): assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict +def test_create_rcs_carousel_with_suggestions(): + carousel = RcsCarousel( + to='1234567890', + from_='asdf1234', + cards=[ + RcsCardItem( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] * 2, + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + RcsSuggestionActionDial( + text='Call us', + postback_data='postback-data', + phone_number='447900000000', + ), + ], + ) + carousel_dict = { + 'to': '1234567890', + 'from': 'asdf1234', + 'cards': [ + { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'media_height': 'MEDIUM', + } + ] * 2, + 'suggestions': [ + { + 'type': 'reply', + 'text': 'Reply', + 'postback_data': 'postback-data', + }, + { + 'type': 'dial', + 'text': 'Call us', + 'postback_data': 'postback-data', + 'phone_number': '447900000000', + }, + ], + 'channel': 'rcs', + 'message_type': 'carousel', + } + assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict + + +def test_create_rcs_carousel_with_all_suggestion_types(): + carousel = RcsCarousel( + to='1234567890', + from_='asdf1234', + cards=[ + RcsCardItem( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] * 2, + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + RcsSuggestionActionDial( + text='Call us', + postback_data='postback-data', + phone_number='447900000000', + ), + RcsSuggestionActionViewLocation( + text='View location', + postback_data='postback-data', + latitude='51.5074', + longitude='-0.1278', + pin_label='London', + fallback_url='https://example.com/location', + ), + RcsSuggestionActionShareLocation( + text='Share location', + postback_data='postback-data', + ), + RcsSuggestionActionOpenUrl( + text='Open URL', + postback_data='postback-data', + url='https://example.com', + description='Click to open the URL', + ), + RcsSuggestionActionOpenUrlWebview( + text='Open URL in webview', + postback_data='postback-data', + url='https://example.com', + description='Click to open the URL in a webview', + view_mode='FULL', + ), + RcsSuggestionActionCreateCalendarEvent( + text='Add to calendar', + postback_data='postback-data', + start_time='2024-01-01T12:00:00Z', + end_time='2024-01-01T13:00:00Z', + title='Meeting with Bob', + description='Discuss project updates', + fallback_url='https://example.com/calendar-event', + ), + ], + ) + carousel_dict = { + 'to': '1234567890', + 'from': 'asdf1234', + 'cards': [ + { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'media_height': 'MEDIUM', + } + ] * 2, + 'suggestions': [ + { + 'type': 'reply', + 'text': 'Reply', + 'postback_data': 'postback-data', + }, + { + 'type': 'dial', + 'text': 'Call us', + 'postback_data': 'postback-data', + 'phone_number': '447900000000', + }, + { + 'type': 'view_location', + 'text': 'View location', + 'postback_data': 'postback-data', + 'latitude': '51.5074', + 'longitude': '-0.1278', + 'pin_label': 'London', + 'fallback_url': 'https://example.com/location', + }, + { + 'type': 'share_location', + 'text': 'Share location', + 'postback_data': 'postback-data', + }, + { + 'type': 'open_url', + 'text': 'Open URL', + 'postback_data': 'postback-data', + 'url': 'https://example.com', + 'description': 'Click to open the URL', + }, + { + 'type': 'open_url_in_webview', + 'text': 'Open URL in webview', + 'postback_data': 'postback-data', + 'url': 'https://example.com', + 'description': 'Click to open the URL in a webview', + 'view_mode': 'FULL', + }, + { + 'type': 'create_calendar_event', + 'text': 'Add to calendar', + 'postback_data': 'postback-data', + 'start_time': '2024-01-01T12:00:00Z', + 'end_time': '2024-01-01T13:00:00Z', + 'title': 'Meeting with Bob', + 'description': 'Discuss project updates', + 'fallback_url': 'https://example.com/calendar-event', + } + ], + 'channel': 'rcs', + 'message_type': 'carousel', + } + assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict + + def test_create_rcs_custom(): rcs_model = RcsCustom( to='1234567890', From 0b9be71496b1bb4636288f77d4118c6475af00f6 Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 17 Mar 2026 12:51:15 +0000 Subject: [PATCH 20/48] DEVX-10006: fix linting issues --- .../src/vonage_messages/models/__init__.py | 26 ++-- messages/src/vonage_messages/models/rcs.py | 31 ++++- messages/tests/test_rcs_models.py | 112 ++++++++++-------- 3 files changed, 100 insertions(+), 69 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index e10e4e52..dedb747b 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -11,27 +11,27 @@ ) from .mms import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo from .rcs import ( - RcsCustom, - RcsCarousel, + RcsCardBase, RcsCardItem, RcsCardMessage, - RcsCardBase, + RcsCarousel, + RcsCustom, RcsFile, RcsImage, + RcsOptions, + RcsOptionsCard, + RcsOptionsCarousel, RcsResource, - RcsText, - RcsVideo, - RcsSuggestionBase, - RcsSuggestionReply, + RcsSuggestionActionCreateCalendarEvent, RcsSuggestionActionDial, - RcsSuggestionActionViewLocation, - RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview, - RcsSuggestionActionCreateCalendarEvent, - RcsOptions, - RcsOptionsCard, - RcsOptionsCarousel, + RcsSuggestionActionShareLocation, + RcsSuggestionActionViewLocation, + RcsSuggestionBase, + RcsSuggestionReply, + RcsText, + RcsVideo, ) from .sms import Sms, SmsOptions from .viber import ( diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index 0256f054..e227a6c0 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -1,10 +1,20 @@ -from typing import Optional, List, Union +from typing import List, Optional, Union from pydantic import BaseModel, Field from vonage_utils.types import PhoneNumber from .base_message import BaseMessage -from .enums import ChannelType, MessageType, SuggestionType, UrlWebviewViewMode, RcsCategory, RcsCardOrientation, RcsImageAlignment, RcsCardWidth, RcsMediaHeight +from .enums import ( + ChannelType, + MessageType, + RcsCardOrientation, + RcsCardWidth, + RcsCategory, + RcsImageAlignment, + RcsMediaHeight, + SuggestionType, + UrlWebviewViewMode, +) class RcsResource(BaseModel): @@ -65,7 +75,9 @@ class RcsSuggestionActionViewLocation(RcsSuggestionBase): fallback_url (str, Optional): The URL to open if the device doesn't support the view location action. """ - type_: SuggestionType = Field(SuggestionType.VIEW_LOCATION, serialization_alias='type') + type_: SuggestionType = Field( + SuggestionType.VIEW_LOCATION, serialization_alias='type' + ) latitude: str longitude: str pin_label: str @@ -80,7 +92,9 @@ class RcsSuggestionActionShareLocation(RcsSuggestionBase): postback_data (str): The data that will be sent via the Inbound Message webhook when the suggestion is selected. """ - type_: SuggestionType = Field(SuggestionType.SHARE_LOCATION, serialization_alias='type') + type_: SuggestionType = Field( + SuggestionType.SHARE_LOCATION, serialization_alias='type' + ) class RcsSuggestionActionOpenUrl(RcsSuggestionBase): @@ -107,9 +121,12 @@ class RcsSuggestionActionOpenUrlWebview(RcsSuggestionActionOpenUrl): view_mode (str, Optional): The view mode for the webview. If not specified, the default view mode will be used. """ - type_: SuggestionType = Field(SuggestionType.OPEN_URL_IN_WEBVIEW, serialization_alias='type') + type_: SuggestionType = Field( + SuggestionType.OPEN_URL_IN_WEBVIEW, serialization_alias='type' + ) view_mode: Optional[UrlWebviewViewMode] = None + class RcsSuggestionActionCreateCalendarEvent(RcsSuggestionBase): """Model for a create calendar event action suggestion in an RCS message. @@ -123,7 +140,9 @@ class RcsSuggestionActionCreateCalendarEvent(RcsSuggestionBase): fallback_url (str, Optional): The URL to open if the device doesn't support the create calendar event action. """ - type_: SuggestionType = Field(SuggestionType.CREATE_CALENDAR_EVENT, serialization_alias='type') + type_: SuggestionType = Field( + SuggestionType.CREATE_CALENDAR_EVENT, serialization_alias='type' + ) start_time: str end_time: str title: str = Field(..., min_length=1, max_length=100) diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index ea365280..9ab0aee7 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -1,27 +1,27 @@ import pytest from pydantic import ValidationError from vonage_messages.models import ( - RcsCustom, - RcsCarousel, + RcsCardBase, RcsCardItem, RcsCardMessage, - RcsCardBase, + RcsCarousel, + RcsCustom, RcsFile, RcsImage, + RcsOptions, + RcsOptionsCard, + RcsOptionsCarousel, RcsResource, - RcsText, - RcsVideo, - RcsSuggestionBase, - RcsSuggestionReply, + RcsSuggestionActionCreateCalendarEvent, RcsSuggestionActionDial, - RcsSuggestionActionViewLocation, - RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview, - RcsSuggestionActionCreateCalendarEvent, - RcsOptions, - RcsOptionsCard, - RcsOptionsCarousel, + RcsSuggestionActionShareLocation, + RcsSuggestionActionViewLocation, + RcsSuggestionBase, + RcsSuggestionReply, + RcsText, + RcsVideo, ) @@ -86,7 +86,7 @@ def test_create_rcs_text_all_fields(): 'message_type': 'text', 'rcs': { 'category': 'transaction', - } + }, } assert rcs_model.model_dump(by_alias=True, exclude_none=True) == rcs_dict @@ -238,13 +238,12 @@ def test_create_rcs_text_with_all_suggestion_types(): 'title': 'Meeting with Bob', 'description': 'Discuss project updates', 'fallback_url': 'https://example.com/calendar-event', - } + }, ], 'channel': 'rcs', 'message_type': 'text', } - assert rcs_model.model_dump(by_alias=True, exclude_none=True) == rcs_dict @@ -270,7 +269,8 @@ def test_create_rcs_text_with_too_many_suggestions(): text='Reply', postback_data='postback-data', ), - ] * 12, + ] + * 12, ) assert "List should have at most 11 items" in str(err.value) @@ -354,6 +354,7 @@ def test_create_rcs_file(): assert rcs_model.model_dump(by_alias=True, exclude_none=True) == rcs_dict + def test_create_rcs_card_base(): card_base = RcsCardBase( title='Card title', @@ -515,7 +516,7 @@ def test_create_rcs_card_base_with_all_suggestion_types(): 'type': 'share_location', 'text': 'Share location', 'postback_data': 'postback-data', - } + }, ], } card_base_dict_2 = { @@ -547,7 +548,7 @@ def test_create_rcs_card_base_with_all_suggestion_types(): 'title': 'Meeting with Bob', 'description': 'Discuss project updates', 'fallback_url': 'https://example.com/calendar-event', - } + }, ], } assert card_base_1.model_dump(by_alias=True, exclude_none=True) == card_base_dict_1 @@ -647,7 +648,8 @@ def test_create_rcs_card_base_with_too_many_suggestions(): text='Reply', postback_data='postback-data', ), - ] * 5, + ] + * 5, ) assert "List should have at most 4 items" in str(err.value) @@ -876,7 +878,8 @@ def test_create_rcs_card_message_with_too_many_suggestions(): text='Reply', postback_data='postback-data', ), - ] * 5, + ] + * 5, ) assert "List should have at most 4 items" in str(err.value) @@ -1010,7 +1013,7 @@ def test_create_rcs_card_item_without_media_height(): card = RcsCardItem( title='Card title', text='Card description', - media_url='https://example.com/image.jpg' + media_url='https://example.com/image.jpg', ) assert "Field required" in str(err.value) @@ -1077,7 +1080,8 @@ def test_create_rcs_card_item_with_too_many_suggestions(): text='Reply', postback_data='postback-data', ), - ] * 5, + ] + * 5, ) assert "List should have at most 4 items" in str(err.value) @@ -1110,7 +1114,8 @@ def test_create_rcs_carousel(): media_url='https://example.com/image.jpg', media_height='MEDIUM', ) - ] * 2, + ] + * 2, ) carousel_dict = { 'to': '1234567890', @@ -1122,7 +1127,8 @@ def test_create_rcs_carousel(): 'media_url': 'https://example.com/image.jpg', 'media_height': 'MEDIUM', } - ] * 2, + ] + * 2, 'channel': 'rcs', 'message_type': 'carousel', } @@ -1143,7 +1149,8 @@ def test_create_rcs_carousel_with_optional_params(): thumbnail_url='https://example.com/thumbnail.jpg', media_force_refresh=True, ) - ] * 2, + ] + * 2, rcs=RcsOptionsCarousel( card_width='MEDIUM', ), @@ -1161,7 +1168,8 @@ def test_create_rcs_carousel_with_optional_params(): 'thumbnail_url': 'https://example.com/thumbnail.jpg', 'media_force_refresh': True, } - ] * 2, + ] + * 2, 'rcs': { 'card_width': 'MEDIUM', }, @@ -1182,7 +1190,8 @@ def test_create_rcs_carousel_with_suggestions(): media_url='https://example.com/image.jpg', media_height='MEDIUM', ) - ] * 2, + ] + * 2, suggestions=[ RcsSuggestionReply( text='Reply', @@ -1205,7 +1214,8 @@ def test_create_rcs_carousel_with_suggestions(): 'media_url': 'https://example.com/image.jpg', 'media_height': 'MEDIUM', } - ] * 2, + ] + * 2, 'suggestions': [ { 'type': 'reply', @@ -1236,7 +1246,8 @@ def test_create_rcs_carousel_with_all_suggestion_types(): media_url='https://example.com/image.jpg', media_height='MEDIUM', ) - ] * 2, + ] + * 2, suggestions=[ RcsSuggestionReply( text='Reply', @@ -1293,7 +1304,8 @@ def test_create_rcs_carousel_with_all_suggestion_types(): 'media_url': 'https://example.com/image.jpg', 'media_height': 'MEDIUM', } - ] * 2, + ] + * 2, 'suggestions': [ { 'type': 'reply', @@ -1344,7 +1356,7 @@ def test_create_rcs_carousel_with_all_suggestion_types(): 'title': 'Meeting with Bob', 'description': 'Discuss project updates', 'fallback_url': 'https://example.com/calendar-event', - } + }, ], 'channel': 'rcs', 'message_type': 'carousel', @@ -1415,6 +1427,7 @@ def test_rcs_suggestion_base_with_text_too_long(): ) assert "String should have at most 25 characters" in str(err.value) + def test_rcs_suggestion_reply(): suggestion = RcsSuggestionReply( text='Reply', @@ -1790,7 +1803,13 @@ def test_create_rcs_options(): def test_create_rcs_options_with_each_valid_category(): - valid_options = ['acknowledgement', 'authentication', 'promotion', 'service-request', 'transaction'] + valid_options = [ + 'acknowledgement', + 'authentication', + 'promotion', + 'service-request', + 'transaction', + ] for option in valid_options: options = RcsOptions( category=option, @@ -1806,14 +1825,14 @@ def test_create_rcs_options_with_invalid_category(): options = RcsOptions( category='invalid-category', ) - assert "Input should be 'acknowledgement', 'authentication', 'promotion', 'service-request' or 'transaction'" in str(err.value) + assert ( + "Input should be 'acknowledgement', 'authentication', 'promotion', 'service-request' or 'transaction'" + in str(err.value) + ) def test_create_rcs_options_card(): - options = RcsOptionsCard( - card_orientation='HORIZONTAL', - image_alignment='LEFT' - ) + options = RcsOptionsCard(card_orientation='HORIZONTAL', image_alignment='LEFT') options_dict = { 'card_orientation': 'HORIZONTAL', 'image_alignment': 'LEFT', @@ -1824,10 +1843,7 @@ def test_create_rcs_options_card(): def test_create_rcs_options_card_card_orientation_with_each_valid_option(): valid_orientations = ['VERTICAL', 'HORIZONTAL'] for orientation in valid_orientations: - options = RcsOptionsCard( - card_orientation=orientation, - image_alignment='LEFT' - ) + options = RcsOptionsCard(card_orientation=orientation, image_alignment='LEFT') options_dict = { 'card_orientation': orientation, 'image_alignment': 'LEFT', @@ -1838,10 +1854,7 @@ def test_create_rcs_options_card_card_orientation_with_each_valid_option(): def test_create_rcs_options_card_image_alignment_with_each_valid_option(): valid_alignments = ['LEFT', 'RIGHT'] for alignment in valid_alignments: - options = RcsOptionsCard( - card_orientation='HORIZONTAL', - image_alignment=alignment - ) + options = RcsOptionsCard(card_orientation='HORIZONTAL', image_alignment=alignment) options_dict = { 'card_orientation': 'HORIZONTAL', 'image_alignment': alignment, @@ -1852,16 +1865,15 @@ def test_create_rcs_options_card_image_alignment_with_each_valid_option(): def test_create_rcs_options_card_card_orientation_with_invalid_option(): with pytest.raises(ValidationError) as err: options = RcsOptionsCard( - card_orientation='INVALID_ORIENTATION', - image_alignment='LEFT' + card_orientation='INVALID_ORIENTATION', image_alignment='LEFT' ) assert "Input should be 'VERTICAL' or 'HORIZONTAL'" in str(err.value) + def test_create_rcs_options_card_image_alignment_with_invalid_option(): with pytest.raises(ValidationError) as err: options = RcsOptionsCard( - card_orientation='HORIZONTAL', - image_alignment='INVALID_ALIGNMENT' + card_orientation='HORIZONTAL', image_alignment='INVALID_ALIGNMENT' ) assert "Input should be 'LEFT' or 'RIGHT'" in str(err.value) From 5353ffcebcb6f12b4f626552371e941b7216b9a6 Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 17 Mar 2026 12:58:35 +0000 Subject: [PATCH 21/48] DEVX-10006: Updating RCS models exports list --- messages/src/vonage_messages/models/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index dedb747b..e5e5b810 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -85,10 +85,25 @@ 'MmsResource', 'MmsVcard', 'MmsVideo', + 'RcsCardBase', + 'RcsCardItem', + 'RcsCardMessage', + 'RcsCarousel', 'RcsCustom', 'RcsFile', 'RcsImage', + 'RcsOptions', + 'RcsOptionsCard', + 'RcsOptionsCarousel', 'RcsResource', + 'RcsSuggestionActionCreateCalendarEvent', + 'RcsSuggestionActionDial', + 'RcsSuggestionActionOpenUrl', + 'RcsSuggestionActionOpenUrlWebview', + 'RcsSuggestionActionShareLocation', + 'RcsSuggestionActionViewLocation', + 'RcsSuggestionBase', + 'RcsSuggestionReply', 'RcsText', 'RcsVideo', 'Sms', From 5ca3b6d5a04f7aacde04a4517dee26d67a13249e Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 17 Mar 2026 16:42:07 +0000 Subject: [PATCH 22/48] DEVX-10006: updating RCS Carousel implementation and tests --- messages/src/vonage_messages/models/rcs.py | 4 +- messages/tests/test_rcs_models.py | 214 +++++++++++++++++++++ 2 files changed, 216 insertions(+), 2 deletions(-) diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index e227a6c0..6fc64525 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -361,7 +361,7 @@ class RcsCarousel(BaseRcs): webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. """ - cards: List[RcsCardItem] = Field(..., min_length=1, max_length=10) + cards: List[RcsCardItem] = Field(..., min_length=2, max_length=10) suggestions: Optional[ List[ Union[ @@ -375,7 +375,7 @@ class RcsCarousel(BaseRcs): ] ] ] = Field(None, min_length=1, max_length=11) - rcs: Optional[RcsOptionsCarousel] = None + rcs: RcsOptionsCarousel message_type: MessageType = MessageType.CAROUSEL diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 9ab0aee7..5e251638 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -1116,6 +1116,9 @@ def test_create_rcs_carousel(): ) ] * 2, + rcs=RcsOptionsCarousel( + card_width='MEDIUM', + ), ) carousel_dict = { 'to': '1234567890', @@ -1129,6 +1132,9 @@ def test_create_rcs_carousel(): } ] * 2, + 'rcs': { + 'card_width': 'MEDIUM', + }, 'channel': 'rcs', 'message_type': 'carousel', } @@ -1203,6 +1209,9 @@ def test_create_rcs_carousel_with_suggestions(): phone_number='447900000000', ), ], + rcs=RcsOptionsCarousel( + card_width='MEDIUM', + ), ) carousel_dict = { 'to': '1234567890', @@ -1229,6 +1238,9 @@ def test_create_rcs_carousel_with_suggestions(): 'phone_number': '447900000000', }, ], + 'rcs': { + 'card_width': 'MEDIUM', + }, 'channel': 'rcs', 'message_type': 'carousel', } @@ -1293,6 +1305,9 @@ def test_create_rcs_carousel_with_all_suggestion_types(): fallback_url='https://example.com/calendar-event', ), ], + rcs=RcsOptionsCarousel( + card_width='MEDIUM', + ), ) carousel_dict = { 'to': '1234567890', @@ -1358,12 +1373,177 @@ def test_create_rcs_carousel_with_all_suggestion_types(): 'fallback_url': 'https://example.com/calendar-event', }, ], + 'rcs': { + 'card_width': 'MEDIUM', + }, 'channel': 'rcs', 'message_type': 'carousel', } assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict +def test_create_rcs_carousel_without_rcs_options(): + with pytest.raises(ValidationError) as err: + carousel = RcsCarousel( + to='1234567890', + from_='asdf1234', + cards=[ + RcsCardItem( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] * 2, + ) + assert "Field required" in str(err.value) + + +def test_create_rcs_carousel_with_insufficient_cards(): + with pytest.raises(ValidationError) as err: + carousel = RcsCarousel( + to='1234567890', + from_='asdf1234', + cards=[ + RcsCardItem( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ], + rcs=RcsOptionsCarousel( + card_width='MEDIUM', + ), + ) + assert "List should have at least 2 items" in str(err.value) + + +def test_create_rcs_carousel_with_too_many_cards(): + with pytest.raises(ValidationError) as err: + carousel = RcsCarousel( + to='1234567890', + from_='asdf1234', + cards=[ + RcsCardItem( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] + * 11, + rcs=RcsOptionsCarousel( + card_width='MEDIUM', + ), + ) + assert "List should have at most 10 items" in str(err.value) + + +def test_create_rcs_carousel_with_invalid_card_type(): + with pytest.raises(ValidationError) as err: + carousel = RcsCarousel( + to='1234567890', + from_='asdf1234', + cards=[ + RcsCardItem( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ), + RcsCardMessage( + to='1234567890', + from_='asdf1234', + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + ), + ], + rcs=RcsOptionsCarousel( + card_width='MEDIUM', + ), + ) + assert "Input should be a valid dictionary or instance" in str(err.value) + + +def test_create_rcs_carousel_with_insuffient_suggestions(): + with pytest.raises(ValidationError) as err: + carousel = RcsCarousel( + to='1234567890', + from_='asdf1234', + cards=[ + RcsCardItem( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] + * 2, + suggestions=[], + rcs=RcsOptionsCarousel( + card_width='MEDIUM', + ), + ) + assert "List should have at least 1 item" in str(err.value) + + +def test_create_rcs_carousel_with_too_many_suggestions(): + with pytest.raises(ValidationError) as err: + carousel = RcsCarousel( + to='1234567890', + from_='asdf1234', + cards=[ + RcsCardItem( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] + * 2, + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + ] + * 12, + rcs=RcsOptionsCarousel( + card_width='MEDIUM', + ), + ) + assert "List should have at most 11 items" in str(err.value) + + +def test_create_rcs_carousel_with_inavalid_suggestion_types(): + with pytest.raises(ValidationError) as err: + carousel = RcsCarousel( + to='1234567890', + from_='asdf1234', + cards=[ + RcsCardItem( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] * 2, + suggestions=[ + RcsSuggestionReply( + text='Reply', + postback_data='postback-data', + ), + "Invalid suggestion type", + ], + rcs=RcsOptionsCarousel( + card_width='MEDIUM', + ), + ) + assert "Input should be a valid dictionary or instance" in str(err.value) + + def test_create_rcs_custom(): rcs_model = RcsCustom( to='1234567890', @@ -1840,6 +2020,20 @@ def test_create_rcs_options_card(): assert options.model_dump(by_alias=True, exclude_none=True) == options_dict +def test_create_rcs_options_card_with_all_options(): + options = RcsOptionsCard( + card_orientation='HORIZONTAL', + image_alignment='LEFT', + category='transaction', + ) + options_dict = { + 'card_orientation': 'HORIZONTAL', + 'image_alignment': 'LEFT', + 'category': 'transaction', + } + assert options.model_dump(by_alias=True, exclude_none=True) == options_dict + + def test_create_rcs_options_card_card_orientation_with_each_valid_option(): valid_orientations = ['VERTICAL', 'HORIZONTAL'] for orientation in valid_orientations: @@ -1888,6 +2082,26 @@ def test_create_rcs_options_carousel(): assert options.model_dump(by_alias=True, exclude_none=True) == options_dict +def test_create_rcs_options_carousel_with_all_options(): + options = RcsOptionsCarousel( + card_width='MEDIUM', + category='transaction', + ) + options_dict = { + 'card_width': 'MEDIUM', + 'category': 'transaction', + } + assert options.model_dump(by_alias=True, exclude_none=True) == options_dict + + +def test_create_rcs_options_carousel_without_card_width(): + with pytest.raises(ValidationError) as err: + options = RcsOptionsCarousel( + category='transaction', + ) + assert "Field required" in str(err.value) + + def test_create_rcs_options_carousel_card_width_with_each_valid_option(): valid_widths = ['SMALL', 'MEDIUM'] for width in valid_widths: From f4ed88f57d54987b3dea871cbbcfe273349b104f Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 17 Mar 2026 16:43:57 +0000 Subject: [PATCH 23/48] DEVX-10006: linting --- messages/tests/test_rcs_models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 5e251638..95de7ccd 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -1394,7 +1394,8 @@ def test_create_rcs_carousel_without_rcs_options(): media_url='https://example.com/image.jpg', media_height='MEDIUM', ) - ] * 2, + ] + * 2, ) assert "Field required" in str(err.value) @@ -1529,7 +1530,8 @@ def test_create_rcs_carousel_with_inavalid_suggestion_types(): media_url='https://example.com/image.jpg', media_height='MEDIUM', ) - ] * 2, + ] + * 2, suggestions=[ RcsSuggestionReply( text='Reply', From f760a587b3a9c332af937cd1104c0413ac755130 Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 18 Mar 2026 13:05:11 +0000 Subject: [PATCH 24/48] DEVX-10006: Updating doc blocks --- messages/src/vonage_messages/models/rcs.py | 79 +++++++++++++++------- messages/tests/test_rcs_models.py | 2 + 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index 6fc64525..42c855f4 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -28,7 +28,7 @@ class RcsResource(BaseModel): class RcsSuggestionBase(BaseModel): - """Model for a suggestion in an RCS message. + """Base model for a suggestion in an RCS message. Args: text (str): The text to display on the suggestion chip. @@ -57,10 +57,12 @@ class RcsSuggestionActionDial(RcsSuggestionBase): text (str): The text to display on the suggestion chip. postback_data (str): The data that will be sent via the Inbound Message webhook when the suggestion is selected. phone_number (str): The phone number to dial when the suggestion is selected. In E.164 format without the leading plus sign. + fallback_url (str, Optional): The URL to open if the device doesn't support the dial action. """ type_: SuggestionType = Field(SuggestionType.DIAL, serialization_alias='type') phone_number: PhoneNumber + fallback_url: Optional[str] = None class RcsSuggestionActionViewLocation(RcsSuggestionBase): @@ -104,6 +106,7 @@ class RcsSuggestionActionOpenUrl(RcsSuggestionBase): text (str): The text to display on the suggestion chip. postback_data (str): The data that will be sent via the Inbound Message webhook when the suggestion is selected. url (str): The URL to open when the suggestion is selected. + description (str): A short description of the URL for accessibility purposes. """ type_: SuggestionType = Field(SuggestionType.OPEN_URL, serialization_alias='type') @@ -118,7 +121,8 @@ class RcsSuggestionActionOpenUrlWebview(RcsSuggestionActionOpenUrl): text (str): The text to display on the suggestion chip. postback_data (str): The data that will be sent via the Inbound Message webhook when the suggestion is selected. url (str): The URL to open in a webview when the suggestion is selected. - view_mode (str, Optional): The view mode for the webview. If not specified, the default view mode will be used. + description (str): A short description of the URL for accessibility purposes. + view_mode (str, Optional): The view mode for the webview (FULL, TALL, HALF). If not specified, the default view mode for the device will be used. """ type_: SuggestionType = Field( @@ -151,10 +155,10 @@ class RcsSuggestionActionCreateCalendarEvent(RcsSuggestionBase): class RcsOptions(BaseModel): - """Model for RCS message options. + """Base model for RCS message options. Args: - category (str, Optional): The category of the RCS message. + category (str, Optional): The category of the RCS message (authentication, transaction, promotion, service, request, acknowledgement). """ category: Optional[RcsCategory] = None @@ -164,7 +168,7 @@ class RcsOptionsCard(RcsOptions): """Model for an RCS card message options. Args: - category (str, Optional): The category of the RCS message. + category (str, Optional): The category of the RCS message (authentication, transaction, promotion, service, request, acknowledgement). card_orientation (str): The orientation of the card (HORIZONTAL or VERTICAL). image_alignment (str): The alignment of the image on the card (LEFT or RIGHT). """ @@ -177,6 +181,7 @@ class RcsOptionsCarousel(RcsOptions): """Model for an RCS carousel message options. Args: + category (str, Optional): The category of the RCS message (authentication, transaction, promotion, service, request, acknowledgement). card_width (str): The width of each card in the carousel (SMALL or MEDIUM). """ @@ -184,20 +189,22 @@ class RcsOptionsCarousel(RcsOptions): class BaseRcs(BaseMessage): - """Model for a base RCS message. + """Base model for a base RCS message. Args: to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. - from_ (str): The sender's phone number in E.164 format. Don't use a leading plus sign. + from_ (str): The RCS Agent ID. ttl (int, Optional): The duration in seconds for which the message is valid. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. + rcs: RcsOptions, Optional: An optional RcsOptions object to include in the message. """ to: PhoneNumber from_: str = Field(..., serialization_alias='from', pattern='^[a-zA-Z0-9-_&]+$') - ttl: Optional[int] = Field(None, ge=300, le=259200) + ttl: Optional[int] = Field(None, ge=20, le=259200) + rcs: Optional[RcsOptions] = None channel: ChannelType = ChannelType.RCS @@ -205,13 +212,15 @@ class RcsText(BaseRcs): """Model for an RCS text message. Args: - text (str): The text of the message. to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. - from_ (str): The sender's phone number in E.164 format. Don't use a leading plus sign. + from_ (str): The RCS Agent ID. + text (str): The text of the message. + suggestions (List, Optional): An optional list of suggestions to include in the message. Can include up to 11 suggestions. ttl (int, Optional): The duration in seconds for which the message is valid. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. + rcs: (RcsOptions, Optional): An optional RcsOptions object to include in the message. """ text: str = Field(..., min_length=1, max_length=3072) @@ -229,20 +238,20 @@ class RcsText(BaseRcs): ] ] ] = Field(None, min_length=1, max_length=11) - rcs: Optional[RcsOptions] = None class RcsImage(BaseRcs): """Model for an RCS image message. Args: - image (RcsResource): The image resource. to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. - from_ (str): The sender's phone number in E.164 format. Don't use a leading plus sign. + from_ (str): The RCS Agent ID. + image (RcsResource): The image resource. ttl (int, Optional): The duration in seconds for which the message is valid. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. + rcs: (RcsOptions, Optional): An optional RcsOptions object to include in the message. """ image: RcsResource @@ -253,13 +262,14 @@ class RcsVideo(BaseRcs): """Model for an RCS video message. Args: - video (RcsResource): The video resource. to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. - from_ (str): The sender's phone number in E.164 format. Don't use a leading plus sign. + from_ (str): The RCS Agent ID. + video (RcsResource): The video resource. ttl (int, Optional): The duration in seconds for which the message is valid. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. + rcs: (RcsOptions, Optional): An optional RcsOptions object to include in the message. """ video: RcsResource @@ -270,13 +280,14 @@ class RcsFile(BaseRcs): """Model for an RCS file message. Args: - file (RcsResource): The file resource. to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. - from_ (str): The sender's phone number in E.164 format. Don't use a leading plus sign. + from_ (str): The RCS Agent ID. + file (RcsResource): The file resource. ttl (int, Optional): The duration in seconds for which the message is valid. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. + rcs: (RcsOptions, Optional): An optional RcsOptions object to include in the message. """ file: RcsResource @@ -284,13 +295,17 @@ class RcsFile(BaseRcs): class RcsCardBase(BaseModel): - """Model for the content of an RCS card. + """Base model for the content of an RCS card. Args: title (str): The title of the card. text (str): The text of the card. media_url (str): The media URL for the card. Can be an image or a video. - suggestions (List[Union[RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview, RcsSuggestionActionCreateCalendarEvent], Optional): A list of suggestions to include on the card. Can include up to 4 suggestions. + media_height (str, Optional): The height of the media on the card (SHORT, MEDIUM, TALL). + media_description (str, Optional): A description of the media for accessibility purposes. + thumbnail_url (str, Optional): The URL of the thumbnail image for the media. If not specified, the media URL will be used as the thumbnail. + media_force_refresh (bool, Optional): Whether to force refresh the media on the card. If true, the media will be refreshed on the device even if the media URL is the same as a previous message. Defaults to false. + suggestions (List, Optional): An optional list of suggestions to include in the message. A card can include up to 4 suggestions. """ title: str = Field(..., min_length=1, max_length=200) @@ -322,7 +337,11 @@ class RcsCardItem(RcsCardBase): title (str): The title of the card. text (str): The text of the card. media_url (str): The media URL for the card. Can be an image or a video. - suggestions (List[Union[RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview, RcsSuggestionActionCreateCalendarEvent], Optional): A list of suggestions to include on the card. Can include up to 4 suggestions. + media_height (str): The height of the media on the card (SHORT, MEDIUM, TALL). + media_description (str, Optional): A description of the media for accessibility purposes. + thumbnail_url (str, Optional): The URL of the thumbnail image for the media. If not specified, the media URL will be used as the thumbnail. + media_force_refresh (bool, Optional): Whether to force refresh the media on the card. If true, the media will be refreshed on the device even if the media URL is the same as a previous message. Defaults to false. + suggestions (List, Optional): An optional list of suggestions to include in the message. A card can include up to 4 suggestions. """ media_height: RcsMediaHeight @@ -332,16 +351,21 @@ class RcsCardMessage(RcsCardBase, BaseRcs): """Model for an RCS card message. Args: - title (str): The title of the card. - description (str): The description of the card. - media_url (str, Optional): The media URL for the card. Can be an image or a video. - suggestions (List[Union[RcsSuggestionReply, RcsSuggestionActionDial, RcsSuggestionActionViewLocation, RcsSuggestionActionShareLocation, RcsSuggestionActionOpenUrl, RcsSuggestionActionOpenUrlWebview, RcsSuggestionActionCreateCalendarEvent], Optional): A list of suggestions to include on the card. Can include up to 4 suggestions. to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. from_ (str): The sender's phone number in E.164 format. Don't use a leading plus sign. + title (str): The title of the card. + text (str): The text of the card. + media_url (str): The media URL for the card. Can be an image or a video. + media_height (str, Optional): The height of the media on the card (SHORT, MEDIUM, TALL). + media_description (str, Optional): A description of the media for accessibility purposes. + thumbnail_url (str, Optional): The URL of the thumbnail image for the media. If not specified, the media URL will be used as the thumbnail. + media_force_refresh (bool, Optional): Whether to force refresh the media on the card. If true, the media will be refreshed on the device even if the media URL is the same as a previous message. Defaults to false. + suggestions (List, Optional): An optional list of suggestions to include in the message. A card can include up to 4 suggestions. ttl (int, Optional): The duration in seconds for which the message is valid. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. + rcs: (RcsOptionsCard, Optional): An optional RcsOptionsCard object to include in the message. """ rcs: Optional[RcsOptionsCard] = None @@ -352,13 +376,15 @@ class RcsCarousel(BaseRcs): """Model for an RCS carousel message. Args: - cards (List[RcsCard]): A list of cards to include in the carousel. Can include up to 10 cards. to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. from_ (str): The sender's phone number in E.164 format. Don't use a leading plus sign. + cards (List[RcsCardItem]): A list of card items to include in the carousel. Can include up to 10 cards. + suggestions (List, Optional): An optional list of suggestions to include in the message. Can include up to 11 suggestions. ttl (int, Optional): The duration in seconds for which the message is valid. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. + rcs: (RcsOptionsCarousel): An RcsOptionsCarousel object to include in the message. """ cards: List[RcsCardItem] = Field(..., min_length=2, max_length=10) @@ -383,13 +409,14 @@ class RcsCustom(BaseRcs): """Model for an RCS custom message. Args: - custom (dict): The custom message data. to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. from_ (str): The sender's phone number in E.164 format. Don't use a leading plus sign. + custom (dict): The custom message data. ttl (int, Optional): The duration in seconds for which the message is valid. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. + rcs: (RcsOptions, Optional): An optional RcsOptions object to include in the message. """ custom: dict diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 95de7ccd..1df3c348 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -1629,12 +1629,14 @@ def test_rcs_suggestion_dial(): text='Call us', postback_data='postback-data', phone_number='447900000000', + fallback_url='https://example.com/dial', ) suggestion_dict = { 'type': 'dial', 'text': 'Call us', 'postback_data': 'postback-data', 'phone_number': '447900000000', + 'fallback_url': 'https://example.com/dial', } assert suggestion.model_dump(by_alias=True, exclude_none=True) == suggestion_dict From 7a853a9b6ecf14915d73be6b98a4f52c5fca09ca Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 24 Mar 2026 12:36:01 +0000 Subject: [PATCH 25/48] DEVX-10043: Updating implementaton and tests for MMS caption max length --- messages/src/vonage_messages/models/mms.py | 2 +- messages/tests/test_mms_models.py | 51 ++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/messages/src/vonage_messages/models/mms.py b/messages/src/vonage_messages/models/mms.py index 6220ed11..748c03bc 100644 --- a/messages/src/vonage_messages/models/mms.py +++ b/messages/src/vonage_messages/models/mms.py @@ -16,7 +16,7 @@ class MmsResource(BaseModel): """ url: str - caption: Optional[str] = Field(None, min_length=1, max_length=2000) + caption: Optional[str] = Field(None, min_length=1, max_length=3000) class BaseMms(BaseMessage): diff --git a/messages/tests/test_mms_models.py b/messages/tests/test_mms_models.py index c74d6994..3a9720d7 100644 --- a/messages/tests/test_mms_models.py +++ b/messages/tests/test_mms_models.py @@ -1,7 +1,58 @@ +import pytest +from pydantic import ValidationError from vonage_messages.models import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo from vonage_messages.models.enums import WebhookVersion +def test_create_mms_resource(): + mms_resource = MmsResource( + url='https://example.com/resource', + ) + mms_resource_dict = { + 'url': 'https://example.com/resource', + } + + assert mms_resource.model_dump(exclude_none=True) == mms_resource_dict + + +def test_create_mms_resource_with_caption(): + mms_resource = MmsResource( + url='https://example.com/resource', + caption='Resource caption', + ) + mms_resource_dict = { + 'url': 'https://example.com/resource', + 'caption': 'Resource caption', + } + + assert mms_resource.model_dump(exclude_none=True) == mms_resource_dict + + +def test_create_mms_resource_without_url(): + with pytest.raises(ValidationError) as err: + mms_resource = MmsResource( + caption='Resource caption', + ) + assert "Field required" in str(err.value) + + +def test_create_mms_resource_with_caption_too_short(): + with pytest.raises(ValidationError) as err: + mms_resource = MmsResource( + url='https://example.com/resource', + caption='', + ) + assert "String should have at least 1 character" in str(err.value) + + +def test_create_mms_resource_with_caption_too_long(): + with pytest.raises(ValidationError) as err: + mms_resource = MmsResource( + url='https://example.com/resource', + caption='a' * 3001, + ) + assert "String should have at most 3000 characters" in str(err.value) + def test_create_mms_image(): mms_model = MmsImage( to='1234567890', From 4c267fcc074169403197308bc1c87c252895964f Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 24 Mar 2026 12:44:39 +0000 Subject: [PATCH 26/48] DEVX-10815: Adding tests for MMS trusted_recipient param --- messages/tests/test_mms_models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/messages/tests/test_mms_models.py b/messages/tests/test_mms_models.py index 3a9720d7..35fa665b 100644 --- a/messages/tests/test_mms_models.py +++ b/messages/tests/test_mms_models.py @@ -86,6 +86,7 @@ def test_create_mms_image_all_fields(): webhook_url='https://example.com', webhook_version=WebhookVersion.V1, ttl=600, + trusted_recipient=True, ) mms_dict = { 'to': '1234567890', @@ -98,6 +99,7 @@ def test_create_mms_image_all_fields(): 'webhook_url': 'https://example.com', 'webhook_version': 'v1', 'ttl': 600, + 'trusted_recipient': True, 'channel': 'mms', 'message_type': 'image', } @@ -138,6 +140,7 @@ def test_create_mms_vcard_all_fields(): webhook_url='https://example.com', webhook_version=WebhookVersion.V1, ttl=600, + trusted_recipient=True, ) mms_dict = { 'to': '1234567890', @@ -150,6 +153,7 @@ def test_create_mms_vcard_all_fields(): 'webhook_url': 'https://example.com', 'webhook_version': 'v1', 'ttl': 600, + 'trusted_recipient': True, 'channel': 'mms', 'message_type': 'vcard', } @@ -190,6 +194,7 @@ def test_create_mms_audio_all_fields(): webhook_url='https://example.com', webhook_version=WebhookVersion.V1, ttl=600, + trusted_recipient=True, ) mms_dict = { 'to': '1234567890', @@ -202,6 +207,7 @@ def test_create_mms_audio_all_fields(): 'webhook_url': 'https://example.com', 'webhook_version': 'v1', 'ttl': 600, + 'trusted_recipient': True, 'channel': 'mms', 'message_type': 'audio', } @@ -242,6 +248,7 @@ def test_create_mms_video_all_fields(): webhook_url='https://example.com', webhook_version=WebhookVersion.V1, ttl=600, + trusted_recipient=True, ) mms_dict = { 'to': '1234567890', @@ -254,6 +261,7 @@ def test_create_mms_video_all_fields(): 'webhook_url': 'https://example.com', 'webhook_version': 'v1', 'ttl': 600, + 'trusted_recipient': True, 'channel': 'mms', 'message_type': 'video', } From e1edd982c9ffbf2e3853395f4d22702fab155f02 Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 24 Mar 2026 12:45:48 +0000 Subject: [PATCH 27/48] DEVX-10815: adding trusted_recipient param to BaseMms model --- messages/src/vonage_messages/models/mms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/messages/src/vonage_messages/models/mms.py b/messages/src/vonage_messages/models/mms.py index 748c03bc..3c94cb52 100644 --- a/messages/src/vonage_messages/models/mms.py +++ b/messages/src/vonage_messages/models/mms.py @@ -34,6 +34,7 @@ class BaseMms(BaseMessage): to: PhoneNumber from_: Union[PhoneNumber, str] = Field(..., serialization_alias='from') ttl: Optional[int] = Field(None, ge=300, le=259200) + trusted_recipient: Optional[bool] = None channel: ChannelType = ChannelType.MMS From d86f58f3b27a5d460258dba65d7fc78ca0cf19af Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 24 Mar 2026 12:47:26 +0000 Subject: [PATCH 28/48] DEVX-10815: Updating SMS tests for trusted_recipient param --- messages/tests/test_sms_models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/messages/tests/test_sms_models.py b/messages/tests/test_sms_models.py index 49b19771..2e6ab449 100644 --- a/messages/tests/test_sms_models.py +++ b/messages/tests/test_sms_models.py @@ -33,6 +33,7 @@ def test_create_sms_all_fields(): webhook_url='https://example.com', webhook_version=WebhookVersion.V1, ttl=600, + trusted_recipient=True, ) sms_dict = { 'to': '1234567890', @@ -47,6 +48,7 @@ def test_create_sms_all_fields(): 'webhook_url': 'https://example.com', 'webhook_version': 'v1', 'ttl': 600, + 'trusted_recipient': True, 'channel': 'sms', 'message_type': 'text', } From 6f717b695b411f25ee193bbff730e7b3e1975d1a Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 24 Mar 2026 12:48:59 +0000 Subject: [PATCH 29/48] DEVX-10815: updating Sms model to add trusted_recipient param --- messages/src/vonage_messages/models/sms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/messages/src/vonage_messages/models/sms.py b/messages/src/vonage_messages/models/sms.py index dd52541f..bf131e76 100644 --- a/messages/src/vonage_messages/models/sms.py +++ b/messages/src/vonage_messages/models/sms.py @@ -47,6 +47,7 @@ class Sms(BaseMessage): from_: Union[PhoneNumber, str] = Field(..., serialization_alias='from') text: str = Field(..., max_length=1000) ttl: Optional[int] = None + trusted_recipient: Optional[bool] = None sms: Optional[SmsOptions] = None channel: ChannelType = ChannelType.SMS message_type: MessageType = MessageType.TEXT From 1499a67b230798c43a4867e1ccd793424b29f7dd Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 24 Mar 2026 12:51:41 +0000 Subject: [PATCH 30/48] DEVX-10815: updating RCS model tests to include trusted_recipient param --- messages/tests/test_rcs_models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index 1df3c348..f5cf1b0f 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -71,6 +71,7 @@ def test_create_rcs_text_all_fields(): client_ref='client-ref', webhook_url='https://example.com', ttl=600, + trusted_recipient=True, rcs=RcsOptions( category='transaction', ), @@ -82,6 +83,7 @@ def test_create_rcs_text_all_fields(): 'client_ref': 'client-ref', 'webhook_url': 'https://example.com', 'ttl': 600, + 'trusted_recipient': True, 'channel': 'rcs', 'message_type': 'text', 'rcs': { From 83534fa16698cdb7aa4915f9fe2d1c5814719f39 Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 24 Mar 2026 12:54:40 +0000 Subject: [PATCH 31/48] DEVX-10815: updating BaseRcs model to include trusted_recipient param --- messages/src/vonage_messages/models/rcs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index 42c855f4..087832d6 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -204,6 +204,7 @@ class BaseRcs(BaseMessage): to: PhoneNumber from_: str = Field(..., serialization_alias='from', pattern='^[a-zA-Z0-9-_&]+$') ttl: Optional[int] = Field(None, ge=20, le=259200) + trusted_recipient: Optional[bool] = None rcs: Optional[RcsOptions] = None channel: ChannelType = ChannelType.RCS From e5dc5251e1ab68f3bde2d149a5676479227f51de Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 24 Mar 2026 13:02:31 +0000 Subject: [PATCH 32/48] DEVX-10815: updating doc blocks --- messages/src/vonage_messages/models/mms.py | 7 ++++++- messages/src/vonage_messages/models/rcs.py | 8 ++++++++ messages/src/vonage_messages/models/sms.py | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/messages/src/vonage_messages/models/mms.py b/messages/src/vonage_messages/models/mms.py index 3c94cb52..e1620863 100644 --- a/messages/src/vonage_messages/models/mms.py +++ b/messages/src/vonage_messages/models/mms.py @@ -12,7 +12,7 @@ class MmsResource(BaseModel): Args: url (str): The URL of the resource. - caption (str, Optional): Additional text to accompany the resource. + caption (str, Optional): Additional text to accompany the resource, with a maximum length of 3000 characters. """ url: str @@ -26,6 +26,7 @@ class BaseMms(BaseMessage): to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. from_ (Union[PhoneNumber, str]): The sender's phone number in E.164 format. Don't use a leading plus sign. ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. @@ -46,6 +47,7 @@ class MmsImage(BaseMms): to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. from_ (Union[PhoneNumber, str]): The sender's phone number in E.164 format. Don't use a leading plus sign. ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. @@ -63,6 +65,7 @@ class MmsVcard(BaseMms): to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. from_ (Union[PhoneNumber, str]): The sender's phone number in E.164 format. Don't use a leading plus sign. ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. @@ -80,6 +83,7 @@ class MmsAudio(BaseMms): to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. from_ (Union[PhoneNumber, str]): The sender's phone number in E.164 format. Don't use a leading plus sign. ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. @@ -97,6 +101,7 @@ class MmsVideo(BaseMms): to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. from_ (Union[PhoneNumber, str]): The sender's phone number in E.164 format. Don't use a leading plus sign. ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index 087832d6..96aa2e92 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -195,6 +195,7 @@ class BaseRcs(BaseMessage): to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. from_ (str): The RCS Agent ID. ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. @@ -218,6 +219,7 @@ class RcsText(BaseRcs): text (str): The text of the message. suggestions (List, Optional): An optional list of suggestions to include in the message. Can include up to 11 suggestions. ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. @@ -249,6 +251,7 @@ class RcsImage(BaseRcs): from_ (str): The RCS Agent ID. image (RcsResource): The image resource. ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. @@ -267,6 +270,7 @@ class RcsVideo(BaseRcs): from_ (str): The RCS Agent ID. video (RcsResource): The video resource. ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. @@ -285,6 +289,7 @@ class RcsFile(BaseRcs): from_ (str): The RCS Agent ID. file (RcsResource): The file resource. ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. @@ -363,6 +368,7 @@ class RcsCardMessage(RcsCardBase, BaseRcs): media_force_refresh (bool, Optional): Whether to force refresh the media on the card. If true, the media will be refreshed on the device even if the media URL is the same as a previous message. Defaults to false. suggestions (List, Optional): An optional list of suggestions to include in the message. A card can include up to 4 suggestions. ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. @@ -382,6 +388,7 @@ class RcsCarousel(BaseRcs): cards (List[RcsCardItem]): A list of card items to include in the carousel. Can include up to 10 cards. suggestions (List, Optional): An optional list of suggestions to include in the message. Can include up to 11 suggestions. ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. @@ -414,6 +421,7 @@ class RcsCustom(BaseRcs): from_ (str): The sender's phone number in E.164 format. Don't use a leading plus sign. custom (dict): The custom message data. ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. diff --git a/messages/src/vonage_messages/models/sms.py b/messages/src/vonage_messages/models/sms.py index bf131e76..ff559713 100644 --- a/messages/src/vonage_messages/models/sms.py +++ b/messages/src/vonage_messages/models/sms.py @@ -38,6 +38,7 @@ class Sms(BaseMessage): Don't use a leading plus sign. text (str): The text of the message. ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. sms (SmsOptions, Optional): SMS options. client_ref (str, Optional): An optional client reference. webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. From fcfc16d96879e2fdf15c926d73897e699758a209 Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 24 Mar 2026 14:12:19 +0000 Subject: [PATCH 33/48] DEVX-10013: Updating SMS tests to include pool_id param --- messages/tests/test_sms_models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/messages/tests/test_sms_models.py b/messages/tests/test_sms_models.py index 2e6ab449..19e64203 100644 --- a/messages/tests/test_sms_models.py +++ b/messages/tests/test_sms_models.py @@ -34,6 +34,7 @@ def test_create_sms_all_fields(): webhook_version=WebhookVersion.V1, ttl=600, trusted_recipient=True, + pool_id='abc123', ) sms_dict = { 'to': '1234567890', @@ -49,6 +50,7 @@ def test_create_sms_all_fields(): 'webhook_version': 'v1', 'ttl': 600, 'trusted_recipient': True, + 'pool_id': 'abc123', 'channel': 'sms', 'message_type': 'text', } From 0c8556702624b8b343e1b7ed1b008adffec3f6d0 Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 24 Mar 2026 14:14:23 +0000 Subject: [PATCH 34/48] DEVX-10013: implemeting pool_id param in Sms model --- messages/src/vonage_messages/models/sms.py | 1 + messages/tests/test_sms_models.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/messages/src/vonage_messages/models/sms.py b/messages/src/vonage_messages/models/sms.py index ff559713..cd5a5a14 100644 --- a/messages/src/vonage_messages/models/sms.py +++ b/messages/src/vonage_messages/models/sms.py @@ -26,6 +26,7 @@ class SmsOptions(BaseModel): encoding_type: Optional[EncodingType] = None content_id: Optional[str] = None entity_id: Optional[str] = None + pool_id: Optional[str] = None class Sms(BaseMessage): diff --git a/messages/tests/test_sms_models.py b/messages/tests/test_sms_models.py index 19e64203..800bef52 100644 --- a/messages/tests/test_sms_models.py +++ b/messages/tests/test_sms_models.py @@ -28,13 +28,13 @@ def test_create_sms_all_fields(): encoding_type=EncodingType.TEXT, content_id='content-id', entity_id='entity-id', + pool_id='abc123', ), client_ref='client-ref', webhook_url='https://example.com', webhook_version=WebhookVersion.V1, ttl=600, trusted_recipient=True, - pool_id='abc123', ) sms_dict = { 'to': '1234567890', @@ -44,13 +44,13 @@ def test_create_sms_all_fields(): 'encoding_type': 'text', 'content_id': 'content-id', 'entity_id': 'entity-id', + 'pool_id': 'abc123', }, 'client_ref': 'client-ref', 'webhook_url': 'https://example.com', 'webhook_version': 'v1', 'ttl': 600, 'trusted_recipient': True, - 'pool_id': 'abc123', 'channel': 'sms', 'message_type': 'text', } From 123150fd57b617510ebd99b357f5c0cbefd822cd Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 24 Mar 2026 14:17:58 +0000 Subject: [PATCH 35/48] DEVX-10013: updating doc block for pool_id param in SmsOptions model --- messages/src/vonage_messages/models/sms.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/messages/src/vonage_messages/models/sms.py b/messages/src/vonage_messages/models/sms.py index cd5a5a14..a37139db 100644 --- a/messages/src/vonage_messages/models/sms.py +++ b/messages/src/vonage_messages/models/sms.py @@ -21,6 +21,10 @@ class SmsOptions(BaseModel): entity_id (str, Optional): A string parameter that satisfies regulatory requirements when sending an SMS to specific countries. Not needed unless sending SMS in a country that requires a specific entity ID. + pool_id (str, Optional): The ID of the Number Pool to use as the sender of this message. + If specified, a number from the pool will be used as the from number. + The from number is still required even when specifying a pool_id and will be used as a fall-back if the number pool cannot be used. + See the Number Pools documentation for more information: https://developer.vonage.com/numbers/number-pools-api/overview. """ encoding_type: Optional[EncodingType] = None From ec27bc83c1eee0c6acef6d113a42e29c1563b200 Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 24 Mar 2026 14:21:59 +0000 Subject: [PATCH 36/48] DEVX-10043: linting --- messages/tests/test_mms_models.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/messages/tests/test_mms_models.py b/messages/tests/test_mms_models.py index 35fa665b..eb3387a9 100644 --- a/messages/tests/test_mms_models.py +++ b/messages/tests/test_mms_models.py @@ -31,28 +31,29 @@ def test_create_mms_resource_with_caption(): def test_create_mms_resource_without_url(): with pytest.raises(ValidationError) as err: mms_resource = MmsResource( - caption='Resource caption', - ) + caption='Resource caption', + ) assert "Field required" in str(err.value) def test_create_mms_resource_with_caption_too_short(): with pytest.raises(ValidationError) as err: mms_resource = MmsResource( - url='https://example.com/resource', - caption='', - ) + url='https://example.com/resource', + caption='', + ) assert "String should have at least 1 character" in str(err.value) def test_create_mms_resource_with_caption_too_long(): with pytest.raises(ValidationError) as err: mms_resource = MmsResource( - url='https://example.com/resource', - caption='a' * 3001, - ) + url='https://example.com/resource', + caption='a' * 3001, + ) assert "String should have at most 3000 characters" in str(err.value) + def test_create_mms_image(): mms_model = MmsImage( to='1234567890', From e8f1b5be79e472738fed69231a47bea137a7a09e Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 24 Mar 2026 14:36:02 +0000 Subject: [PATCH 37/48] DEVX-9451: Adding tests for new MmsText model --- messages/tests/test_mms_models.py | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/messages/tests/test_mms_models.py b/messages/tests/test_mms_models.py index eb3387a9..1bd3660e 100644 --- a/messages/tests/test_mms_models.py +++ b/messages/tests/test_mms_models.py @@ -54,6 +54,50 @@ def test_create_mms_resource_with_caption_too_long(): assert "String should have at most 3000 characters" in str(err.value) +def test_create_mms_text(): + mms_model = MmsText( + to='1234567890', + from_='1234567890', + text='Hello, world!', + ) + mms_dict = { + 'to': '1234567890', + 'from': '1234567890', + 'text': 'Hello, world!', + 'channel': 'mms', + 'message_type': 'text', + } + + assert mms_model.model_dump(by_alias=True, exclude_none=True) == mms_dict + + +def test_create_mms_text_all_fields(): + mms_model = MmsText( + to='1234567890', + from_='1234567890', + text='Hello, world!', + client_ref='client-ref', + webhook_url='https://example.com', + webhook_version=WebhookVersion.V1, + ttl=600, + trusted_recipient=True, + ) + mms_dict = { + 'to': '1234567890', + 'from': '1234567890', + 'text': 'Hello, world!', + 'client_ref': 'client-ref', + 'webhook_url': 'https://example.com', + 'webhook_version': 'v1', + 'ttl': 600, + 'trusted_recipient': True, + 'channel': 'mms', + 'message_type': 'text', + } + + assert mms_model.model_dump(by_alias=True) == mms_dict + + def test_create_mms_image(): mms_model = MmsImage( to='1234567890', From 5dbe7bef594f7dba5a4f3912ccad3674f9188c7a Mon Sep 17 00:00:00 2001 From: superchilled Date: Tue, 24 Mar 2026 14:41:14 +0000 Subject: [PATCH 38/48] DEVX-9451: Implementing MmsText Model --- messages/src/vonage_messages/models/__init__.py | 3 ++- messages/src/vonage_messages/models/mms.py | 17 +++++++++++++++++ messages/tests/test_mms_models.py | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index e5e5b810..b8372248 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -9,7 +9,7 @@ MessengerText, MessengerVideo, ) -from .mms import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo +from .mms import MmsAudio, MmsImage, MmsResource, MmsText, MmsVcard, MmsVideo from .rcs import ( RcsCardBase, RcsCardItem, @@ -83,6 +83,7 @@ 'MmsAudio', 'MmsImage', 'MmsResource', + 'MmsText', 'MmsVcard', 'MmsVideo', 'RcsCardBase', diff --git a/messages/src/vonage_messages/models/mms.py b/messages/src/vonage_messages/models/mms.py index e1620863..f1c1d5ea 100644 --- a/messages/src/vonage_messages/models/mms.py +++ b/messages/src/vonage_messages/models/mms.py @@ -39,6 +39,23 @@ class BaseMms(BaseMessage): channel: ChannelType = ChannelType.MMS +class MmsText(BaseMms): + """Model for an MMS text message. + + Args: + text (str): The text of the message. + to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. + from_ (Union[PhoneNumber, str]): The sender's phone number in E.164 format. Don't use a leading plus sign. + ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. + client_ref (str, Optional): An optional client reference. + webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. + webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. + """ + text: str + message_type: MessageType = MessageType.TEXT + + class MmsImage(BaseMms): """Model for an MMS image message. diff --git a/messages/tests/test_mms_models.py b/messages/tests/test_mms_models.py index 1bd3660e..7be035cc 100644 --- a/messages/tests/test_mms_models.py +++ b/messages/tests/test_mms_models.py @@ -1,6 +1,6 @@ import pytest from pydantic import ValidationError -from vonage_messages.models import MmsAudio, MmsImage, MmsResource, MmsVcard, MmsVideo +from vonage_messages.models import MmsAudio, MmsImage, MmsResource, MmsText, MmsVcard, MmsVideo from vonage_messages.models.enums import WebhookVersion From cbb55dde98b1de044416589e7db9a0999b7c493c Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 25 Mar 2026 11:20:28 +0000 Subject: [PATCH 39/48] DEVX-9451: Adding tests and implementation for MmsFile model --- .../src/vonage_messages/models/__init__.py | 3 +- messages/src/vonage_messages/models/mms.py | 18 ++++++ messages/tests/test_mms_models.py | 56 ++++++++++++++++++- 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index b8372248..88ce97e6 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -9,7 +9,7 @@ MessengerText, MessengerVideo, ) -from .mms import MmsAudio, MmsImage, MmsResource, MmsText, MmsVcard, MmsVideo +from .mms import MmsAudio, MmsFile, MmsImage, MmsResource, MmsText, MmsVcard, MmsVideo from .rcs import ( RcsCardBase, RcsCardItem, @@ -81,6 +81,7 @@ 'MessengerText', 'MessengerVideo', 'MmsAudio', + 'MmsFile', 'MmsImage', 'MmsResource', 'MmsText', diff --git a/messages/src/vonage_messages/models/mms.py b/messages/src/vonage_messages/models/mms.py index f1c1d5ea..0a1ed414 100644 --- a/messages/src/vonage_messages/models/mms.py +++ b/messages/src/vonage_messages/models/mms.py @@ -126,3 +126,21 @@ class MmsVideo(BaseMms): video: MmsResource message_type: MessageType = MessageType.VIDEO + + +class MmsFile(BaseMms): + """Model for an MMS file message. + + Args: + file (MmsResource): The file resource. + to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. + from_ (Union[PhoneNumber, str]): The sender's phone number in E.164 format. Don't use a leading plus sign. + ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. + client_ref (str, Optional): An optional client reference. + webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. + webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. + """ + + file: MmsResource + message_type: MessageType = MessageType.FILE diff --git a/messages/tests/test_mms_models.py b/messages/tests/test_mms_models.py index 7be035cc..f52e2a0d 100644 --- a/messages/tests/test_mms_models.py +++ b/messages/tests/test_mms_models.py @@ -1,6 +1,6 @@ import pytest from pydantic import ValidationError -from vonage_messages.models import MmsAudio, MmsImage, MmsResource, MmsText, MmsVcard, MmsVideo +from vonage_messages.models import MmsAudio, MmsFile, MmsImage, MmsResource, MmsText, MmsVcard, MmsVideo from vonage_messages.models.enums import WebhookVersion @@ -312,3 +312,57 @@ def test_create_mms_video_all_fields(): } assert mms_model.model_dump(by_alias=True) == mms_dict + + +def test_create_mms_file(): + mms_model = MmsFile( + to='1234567890', + from_='1234567890', + file=MmsResource( + url='https://example.com/file.pdf', + ), + ) + mms_dict = { + 'to': '1234567890', + 'from': '1234567890', + 'file': { + 'url': 'https://example.com/file.pdf', + }, + 'channel': 'mms', + 'message_type': 'file', + } + + assert mms_model.model_dump(by_alias=True, exclude_none=True) == mms_dict + + +def test_create_mms_file_all_fields(): + mms_model = MmsFile( + to='1234567890', + from_='1234567890', + file=MmsResource( + url='https://example.com/file.pdf', + caption='File caption', + ), + client_ref='client-ref', + webhook_url='https://example.com', + webhook_version=WebhookVersion.V1, + ttl=600, + trusted_recipient=True, + ) + mms_dict = { + 'to': '1234567890', + 'from': '1234567890', + 'file': { + 'url': 'https://example.com/file.pdf', + 'caption': 'File caption', + }, + 'client_ref': 'client-ref', + 'webhook_url': 'https://example.com', + 'webhook_version': 'v1', + 'ttl': 600, + 'trusted_recipient': True, + 'channel': 'mms', + 'message_type': 'file', + } + + assert mms_model.model_dump(by_alias=True) == mms_dict From c4df5804ce0381b76f27b68682d0ad516d7f85aa Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 25 Mar 2026 15:21:31 +0000 Subject: [PATCH 40/48] DEVX-9451: adding tests and implementation for MmsContent model --- .../src/vonage_messages/models/__init__.py | 10 +- messages/src/vonage_messages/models/enums.py | 11 ++ messages/src/vonage_messages/models/mms.py | 83 +++++++++- messages/tests/test_mms_models.py | 144 +++++++++++++++++- 4 files changed, 243 insertions(+), 5 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index 88ce97e6..82bbd963 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -1,5 +1,5 @@ from .base_message import BaseMessage -from .enums import ChannelType, EncodingType, MessageType, WebhookVersion +from .enums import ChannelType, EncodingType, MessageType, WebhookVersion, SuggestionType, UrlWebviewViewMode, MmsContentItemType from .messenger import ( MessengerAudio, MessengerFile, @@ -9,7 +9,7 @@ MessengerText, MessengerVideo, ) -from .mms import MmsAudio, MmsFile, MmsImage, MmsResource, MmsText, MmsVcard, MmsVideo +from .mms import MmsAudio, MmsContent, MmsContentItemImage, MmsContentItemAudio, MmsContentItemVideo, MmsContentItemFile, MmsContentItemVcard, MmsFile, MmsImage, MmsResource, MmsText, MmsVcard, MmsVideo from .rcs import ( RcsCardBase, RcsCardItem, @@ -81,6 +81,12 @@ 'MessengerText', 'MessengerVideo', 'MmsAudio', + 'MmsContent', + 'MmsContentItemImage', + 'MmsContentItemAudio', + 'MmsContentItemVideo', + 'MmsContentItemFile', + 'MmsContentItemVcard', 'MmsFile', 'MmsImage', 'MmsResource', diff --git a/messages/src/vonage_messages/models/enums.py b/messages/src/vonage_messages/models/enums.py index e3baeeaa..f1146b68 100644 --- a/messages/src/vonage_messages/models/enums.py +++ b/messages/src/vonage_messages/models/enums.py @@ -15,6 +15,7 @@ class MessageType(str, Enum): VCARD = 'vcard' CARD = 'card' CAROUSEL = 'carousel' + CONTENT = 'content' class ChannelType(str, Enum): @@ -98,3 +99,13 @@ class RcsMediaHeight(str, Enum): SHORT = 'SHORT' MEDIUM = 'MEDIUM' TALL = 'TALL' + + +class MmsContentItemType(str, Enum): + """The type of a content item in an MMS Content message.""" + + IMAGE = 'image' + AUDIO = 'audio' + VIDEO = 'video' + FILE = 'file' + VCARD = 'vcard' \ No newline at end of file diff --git a/messages/src/vonage_messages/models/mms.py b/messages/src/vonage_messages/models/mms.py index 0a1ed414..a07d74f6 100644 --- a/messages/src/vonage_messages/models/mms.py +++ b/messages/src/vonage_messages/models/mms.py @@ -4,7 +4,7 @@ from vonage_utils.types import PhoneNumber from .base_message import BaseMessage -from .enums import ChannelType, MessageType +from .enums import ChannelType, MessageType, MmsContentItemType class MmsResource(BaseModel): @@ -144,3 +144,84 @@ class MmsFile(BaseMms): file: MmsResource message_type: MessageType = MessageType.FILE + + +class MmsContent(BaseMms): + """Model for an MMS message with content that can be of various types. + + Args: + content (list[MmsContentItem]): A list of content items for the message (images, audio, video, files, or vCards). + to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. + from_ (Union[PhoneNumber, str]): The sender's phone number in E.164 format. Don't use a leading plus sign. + ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. + client_ref (str, Optional): An optional client reference. + webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. + webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. + """ + + content: list[ + Union[ + MmsContentItemImage, + MmsContentItemAudio, + MmsContentItemVideo, + MmsContentItemFile, + MmsContentItemVcard + ] + ] + message_type: MessageType = MessageType.CONTENT + + +class MmsContentItemImage(MmsResource): + """Model for an image content item in an MMS Content message. + + Args: + url (str): The URL of the content item. + caption (str, Optional): Additional text to accompany the content item, with a maximum length of 3000 characters. + """ + + type_: MmsContentItemType = Field(MmsContentItemType.IMAGE, serialization_alias='type') + + +class MmsContentItemAudio(MmsResource): + """Model for an audio content item in an MMS Content message. + + Args: + url (str): The URL of the content item. + caption (str, Optional): Additional text to accompany the content item, with a maximum length of 3000 characters. + """ + + type_: MmsContentItemType = Field(MmsContentItemType.AUDIO, serialization_alias='type') + + +class MmsContentItemVideo(MmsResource): + """Model for a video content item in an MMS Content message. + + Args: + url (str): The URL of the content item. + caption (str, Optional): Additional text to accompany the content item, with a maximum length of 3000 characters. + """ + + type_: MmsContentItemType = Field(MmsContentItemType.VIDEO, serialization_alias='type') + + +class MmsContentItemFile(MmsResource): + """Model for a file content item in an MMS Content message. + + Args: + url (str): The URL of the content item. + caption (str, Optional): Additional text to accompany the content item, with a maximum length of 3000 characters. + """ + + type_: MmsContentItemType = Field(MmsContentItemType.FILE, serialization_alias='type') + + +class MmsContentItemVcard(MmsResource): + """Model for a vCard content item in an MMS Content message. + + Args: + url (str): The URL of the content item. + caption (str, Optional): Additional text to accompany the content item, with a maximum length of 3000 characters. + """ + + type_: MmsContentItemType = Field(MmsContentItemType.VCARD, serialization_alias='type') diff --git a/messages/tests/test_mms_models.py b/messages/tests/test_mms_models.py index f52e2a0d..e7d263ae 100644 --- a/messages/tests/test_mms_models.py +++ b/messages/tests/test_mms_models.py @@ -1,6 +1,6 @@ import pytest from pydantic import ValidationError -from vonage_messages.models import MmsAudio, MmsFile, MmsImage, MmsResource, MmsText, MmsVcard, MmsVideo +from vonage_messages.models import MmsAudio, MmsContent, MmsContentItemImage, MmsContentItemAudio, MmsContentItemVideo, MmsContentItemFile, MmsContentItemVcard, MmsFile, MmsImage, MmsResource, MmsText, MmsVcard, MmsVideo from vonage_messages.models.enums import WebhookVersion @@ -365,4 +365,144 @@ def test_create_mms_file_all_fields(): 'message_type': 'file', } - assert mms_model.model_dump(by_alias=True) == mms_dict + assert mms_model.model_dump(by_alias=True, exclude_none=True) == mms_dict + + +def test_create_mms_content(): + mms_model = MmsContent( + to='1234567890', + from_='1234567890', + content=[ + MmsContentItemImage( + url='https://example.com/image.jpg', + caption='Image caption', + ), + ], + ) + mms_dict = { + 'to': '1234567890', + 'from': '1234567890', + 'content': [ + { + 'type': 'image', + 'url': 'https://example.com/image.jpg', + 'caption': 'Image caption', + }, + ], + 'channel': 'mms', + 'message_type': 'content', + } + + assert mms_model.model_dump(by_alias=True, exclude_none=True) == mms_dict + + +def test_create_mms_content_all_fields(): + mms_model = MmsContent( + to='1234567890', + from_='1234567890', + content=[ + MmsContentItemImage( + url='https://example.com/image.jpg', + caption='Image caption', + ), + ], + client_ref='client-ref', + webhook_url='https://example.com', + webhook_version=WebhookVersion.V1, + ttl=600, + trusted_recipient=True, + ) + mms_dict = { + 'to': '1234567890', + 'from': '1234567890', + 'content': [ + { + 'type': 'image', + 'url': 'https://example.com/image.jpg', + 'caption': 'Image caption', + }, + ], + 'client_ref': 'client-ref', + 'webhook_url': 'https://example.com', + 'webhook_version': 'v1', + 'ttl': 600, + 'trusted_recipient': True, + 'channel': 'mms', + 'message_type': 'content', + } + + +def test_create_mms_content_all_content_types(): + mms_model = MmsContent( + to='1234567890', + from_='1234567890', + content=[ + MmsContentItemImage( + url='https://example.com/image.jpg', + caption='Image caption', + ), + MmsContentItemAudio( + url='https://example.com/audio.mp3', + caption='Audio caption', + ), + MmsContentItemVideo( + url='https://example.com/video.mp4', + caption='Video caption', + ), + MmsContentItemFile( + url='https://example.com/file.pdf', + caption='File caption', + ), + MmsContentItemVcard( + url='https://example.com/vcard.vcf', + caption='Vcard caption', + ), + ], + ) + mms_dict = { + 'to': '1234567890', + 'from': '1234567890', + 'content': [ + { + 'type': 'image', + 'url': 'https://example.com/image.jpg', + 'caption': 'Image caption', + }, + { + 'type': 'audio', + 'url': 'https://example.com/audio.mp3', + 'caption': 'Audio caption', + }, + { + 'type': 'video', + 'url': 'https://example.com/video.mp4', + 'caption': 'Video caption', + }, + { + 'type': 'file', + 'url': 'https://example.com/file.pdf', + 'caption': 'File caption', + }, + { + 'type': 'vcard', + 'url': 'https://example.com/vcard.vcf', + 'caption': 'Vcard caption', + }, + ], + 'channel': 'mms', + 'message_type': 'content', + } + + +def test_create_mms_content_with_invalid_content_item(): + with pytest.raises(ValidationError) as err: + mms_model = MmsContent( + to='1234567890', + from_='1234567890', + content=[ + MmsResource( + url='https://example.com/resource', + ), + ], + ) + assert "Input should be a valid dictionary or instance" in str(err.value) From 95648e2f56c36b7d6e9fea882013bd0e8026bb99 Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 25 Mar 2026 15:26:39 +0000 Subject: [PATCH 41/48] DEVX-9451: updating imports lists --- .../src/vonage_messages/models/__init__.py | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index 82bbd963..43284300 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -1,5 +1,18 @@ from .base_message import BaseMessage -from .enums import ChannelType, EncodingType, MessageType, WebhookVersion, SuggestionType, UrlWebviewViewMode, MmsContentItemType +from .enums import ( + ChannelType, + EncodingType, + MessageType, + WebhookVersion, + SuggestionType, + UrlWebviewViewMode, + MmsContentItemType, + RcsCategory, + RcsCardOrientation, + RcsImageAlignment, + RcsCardWidth, + RcsMediaHeight, +) from .messenger import ( MessengerAudio, MessengerFile, @@ -9,7 +22,21 @@ MessengerText, MessengerVideo, ) -from .mms import MmsAudio, MmsContent, MmsContentItemImage, MmsContentItemAudio, MmsContentItemVideo, MmsContentItemFile, MmsContentItemVcard, MmsFile, MmsImage, MmsResource, MmsText, MmsVcard, MmsVideo +from .mms import ( + MmsAudio, + MmsContent, + MmsContentItemImage, + MmsContentItemAudio, + MmsContentItemVideo, + MmsContentItemFile, + MmsContentItemVcard, + MmsFile, + MmsImage, + MmsResource, + MmsText, + MmsVcard, + MmsVideo, +) from .rcs import ( RcsCardBase, RcsCardItem, @@ -87,6 +114,7 @@ 'MmsContentItemVideo', 'MmsContentItemFile', 'MmsContentItemVcard', + 'MmsContentItemType', 'MmsFile', 'MmsImage', 'MmsResource', @@ -114,6 +142,11 @@ 'RcsSuggestionReply', 'RcsText', 'RcsVideo', + 'RcsCategory', + 'RcsCardOrientation', + 'RcsImageAlignment', + 'RcsCardWidth', + 'RcsMediaHeight', 'Sms', 'SmsOptions', 'ViberAction', From 819475d26603e08b0945f8c27cb3fdf937d16935 Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 25 Mar 2026 15:28:24 +0000 Subject: [PATCH 42/48] DEVX-9451: linting --- .../src/vonage_messages/models/__init__.py | 12 +++++------- messages/src/vonage_messages/models/enums.py | 2 +- messages/src/vonage_messages/models/mms.py | 19 ++++++++++++++----- messages/tests/test_mms_models.py | 16 +++++++++++++++- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index 43284300..ba920ac2 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -3,15 +3,13 @@ ChannelType, EncodingType, MessageType, - WebhookVersion, - SuggestionType, - UrlWebviewViewMode, MmsContentItemType, - RcsCategory, RcsCardOrientation, - RcsImageAlignment, RcsCardWidth, + RcsCategory, + RcsImageAlignment, RcsMediaHeight, + WebhookVersion, ) from .messenger import ( MessengerAudio, @@ -25,11 +23,11 @@ from .mms import ( MmsAudio, MmsContent, - MmsContentItemImage, MmsContentItemAudio, - MmsContentItemVideo, MmsContentItemFile, + MmsContentItemImage, MmsContentItemVcard, + MmsContentItemVideo, MmsFile, MmsImage, MmsResource, diff --git a/messages/src/vonage_messages/models/enums.py b/messages/src/vonage_messages/models/enums.py index f1146b68..80fc9877 100644 --- a/messages/src/vonage_messages/models/enums.py +++ b/messages/src/vonage_messages/models/enums.py @@ -108,4 +108,4 @@ class MmsContentItemType(str, Enum): AUDIO = 'audio' VIDEO = 'video' FILE = 'file' - VCARD = 'vcard' \ No newline at end of file + VCARD = 'vcard' diff --git a/messages/src/vonage_messages/models/mms.py b/messages/src/vonage_messages/models/mms.py index a07d74f6..9f85a16d 100644 --- a/messages/src/vonage_messages/models/mms.py +++ b/messages/src/vonage_messages/models/mms.py @@ -52,6 +52,7 @@ class MmsText(BaseMms): webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. """ + text: str message_type: MessageType = MessageType.TEXT @@ -166,7 +167,7 @@ class MmsContent(BaseMms): MmsContentItemAudio, MmsContentItemVideo, MmsContentItemFile, - MmsContentItemVcard + MmsContentItemVcard, ] ] message_type: MessageType = MessageType.CONTENT @@ -180,7 +181,9 @@ class MmsContentItemImage(MmsResource): caption (str, Optional): Additional text to accompany the content item, with a maximum length of 3000 characters. """ - type_: MmsContentItemType = Field(MmsContentItemType.IMAGE, serialization_alias='type') + type_: MmsContentItemType = Field( + MmsContentItemType.IMAGE, serialization_alias='type' + ) class MmsContentItemAudio(MmsResource): @@ -191,7 +194,9 @@ class MmsContentItemAudio(MmsResource): caption (str, Optional): Additional text to accompany the content item, with a maximum length of 3000 characters. """ - type_: MmsContentItemType = Field(MmsContentItemType.AUDIO, serialization_alias='type') + type_: MmsContentItemType = Field( + MmsContentItemType.AUDIO, serialization_alias='type' + ) class MmsContentItemVideo(MmsResource): @@ -202,7 +207,9 @@ class MmsContentItemVideo(MmsResource): caption (str, Optional): Additional text to accompany the content item, with a maximum length of 3000 characters. """ - type_: MmsContentItemType = Field(MmsContentItemType.VIDEO, serialization_alias='type') + type_: MmsContentItemType = Field( + MmsContentItemType.VIDEO, serialization_alias='type' + ) class MmsContentItemFile(MmsResource): @@ -224,4 +231,6 @@ class MmsContentItemVcard(MmsResource): caption (str, Optional): Additional text to accompany the content item, with a maximum length of 3000 characters. """ - type_: MmsContentItemType = Field(MmsContentItemType.VCARD, serialization_alias='type') + type_: MmsContentItemType = Field( + MmsContentItemType.VCARD, serialization_alias='type' + ) diff --git a/messages/tests/test_mms_models.py b/messages/tests/test_mms_models.py index e7d263ae..09b0b11e 100644 --- a/messages/tests/test_mms_models.py +++ b/messages/tests/test_mms_models.py @@ -1,6 +1,20 @@ import pytest from pydantic import ValidationError -from vonage_messages.models import MmsAudio, MmsContent, MmsContentItemImage, MmsContentItemAudio, MmsContentItemVideo, MmsContentItemFile, MmsContentItemVcard, MmsFile, MmsImage, MmsResource, MmsText, MmsVcard, MmsVideo +from vonage_messages.models import ( + MmsAudio, + MmsContent, + MmsContentItemAudio, + MmsContentItemFile, + MmsContentItemImage, + MmsContentItemVcard, + MmsContentItemVideo, + MmsFile, + MmsImage, + MmsResource, + MmsText, + MmsVcard, + MmsVideo, +) from vonage_messages.models.enums import WebhookVersion From 3eed3e6b527cacdc326fe7ab53fd0b5e0ac26723 Mon Sep 17 00:00:00 2001 From: superchilled Date: Wed, 25 Mar 2026 15:41:02 +0000 Subject: [PATCH 43/48] DEVX-9451: class ordering --- messages/src/vonage_messages/models/mms.py | 52 +++++++++++----------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/messages/src/vonage_messages/models/mms.py b/messages/src/vonage_messages/models/mms.py index 9f85a16d..bdc54be5 100644 --- a/messages/src/vonage_messages/models/mms.py +++ b/messages/src/vonage_messages/models/mms.py @@ -147,32 +147,6 @@ class MmsFile(BaseMms): message_type: MessageType = MessageType.FILE -class MmsContent(BaseMms): - """Model for an MMS message with content that can be of various types. - - Args: - content (list[MmsContentItem]): A list of content items for the message (images, audio, video, files, or vCards). - to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. - from_ (Union[PhoneNumber, str]): The sender's phone number in E.164 format. Don't use a leading plus sign. - ttl (int, Optional): The duration in seconds for which the message is valid. - trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. - client_ref (str, Optional): An optional client reference. - webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. - webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. - """ - - content: list[ - Union[ - MmsContentItemImage, - MmsContentItemAudio, - MmsContentItemVideo, - MmsContentItemFile, - MmsContentItemVcard, - ] - ] - message_type: MessageType = MessageType.CONTENT - - class MmsContentItemImage(MmsResource): """Model for an image content item in an MMS Content message. @@ -234,3 +208,29 @@ class MmsContentItemVcard(MmsResource): type_: MmsContentItemType = Field( MmsContentItemType.VCARD, serialization_alias='type' ) + + +class MmsContent(BaseMms): + """Model for an MMS message with content that can be of various types. + + Args: + content (list[MmsContentItem]): A list of content items for the message (images, audio, video, files, or vCards). + to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. + from_ (Union[PhoneNumber, str]): The sender's phone number in E.164 format. Don't use a leading plus sign. + ttl (int, Optional): The duration in seconds for which the message is valid. + trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. + client_ref (str, Optional): An optional client reference. + webhook_url (str, Optional): The URL to which Status Webhook messages will be sent for this particular message. + webhook_version (WebhookVersion, Optional): Which version of the Messages API will be used to send Status Webhook messages for this particular message. + """ + + content: list[ + Union[ + MmsContentItemImage, + MmsContentItemAudio, + MmsContentItemVideo, + MmsContentItemFile, + MmsContentItemVcard, + ] + ] + message_type: MessageType = MessageType.CONTENT From 2e3a263c403dac2ed7f5cd71ecdc05b4901d50ee Mon Sep 17 00:00:00 2001 From: superchilled Date: Fri, 27 Mar 2026 12:55:28 +0000 Subject: [PATCH 44/48] DEVX-10770: adding tests and implementation for typing indicators --- messages/src/vonage_messages/messages.py | 11 +++++-- .../src/vonage_messages/models/__init__.py | 2 ++ messages/src/vonage_messages/models/enums.py | 6 ++++ .../src/vonage_messages/models/whatsapp.py | 15 +++++++++- messages/tests/test_messages.py | 29 +++++++++++++++++++ messages/tests/test_whatsapp_models.py | 12 ++++++++ 6 files changed, 71 insertions(+), 4 deletions(-) diff --git a/messages/src/vonage_messages/messages.py b/messages/src/vonage_messages/messages.py index 6ba05b98..93da6919 100644 --- a/messages/src/vonage_messages/messages.py +++ b/messages/src/vonage_messages/messages.py @@ -1,7 +1,7 @@ from pydantic import validate_call from vonage_http_client.http_client import HttpClient -from .models import BaseMessage +from .models import BaseMessage, ReplyingIndicatorText from .responses import SendMessageResponse @@ -66,7 +66,7 @@ def send( return SendMessageResponse(**response) @validate_call - def mark_whatsapp_message_read(self, message_uuid: str) -> None: + def mark_whatsapp_message_read(self, message_uuid: str, replying_indicator: ReplyingIndicatorText = None) -> None: """Mark a WhatsApp message as read. Note: to use this method, update the `api_host` attribute of the @@ -78,11 +78,16 @@ def mark_whatsapp_message_read(self, message_uuid: str) -> None: Args: message_uuid (str): The unique identifier of the WhatsApp message to mark as read. + replying_indicator (ReplyingIndicatorText, optional): An object indicating whether to show the replying indicator on the WhatsApp message. """ + body = {'status': 'read'} + if replying_indicator is not None: + body['replying_indicator'] = replying_indicator.model_dump(by_alias=True, exclude_none=True) + self._http_client.patch( self._http_client.api_host, f'/v1/messages/{message_uuid}', - {'status': 'read'}, + body, self._auth_type, ) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index ba920ac2..cf35548e 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -74,6 +74,7 @@ ViberVideoResource, ) from .whatsapp import ( + ReplyingIndicatorText, WhatsappAudio, WhatsappAudioResource, WhatsappContext, @@ -145,6 +146,7 @@ 'RcsImageAlignment', 'RcsCardWidth', 'RcsMediaHeight', + 'ReplyingIndicatorText', 'Sms', 'SmsOptions', 'ViberAction', diff --git a/messages/src/vonage_messages/models/enums.py b/messages/src/vonage_messages/models/enums.py index 80fc9877..d6aeec16 100644 --- a/messages/src/vonage_messages/models/enums.py +++ b/messages/src/vonage_messages/models/enums.py @@ -109,3 +109,9 @@ class MmsContentItemType(str, Enum): VIDEO = 'video' FILE = 'file' VCARD = 'vcard' + + +class ReplyingIndicatorType(str, Enum): + """The type of a WhatsApp replying indicator.""" + + TEXT = 'text' diff --git a/messages/src/vonage_messages/models/whatsapp.py b/messages/src/vonage_messages/models/whatsapp.py index d4f4582e..a7595bd7 100644 --- a/messages/src/vonage_messages/models/whatsapp.py +++ b/messages/src/vonage_messages/models/whatsapp.py @@ -4,7 +4,7 @@ from vonage_utils.types import PhoneNumber from .base_message import BaseMessage -from .enums import ChannelType, MessageType +from .enums import ChannelType, MessageType, ReplyingIndicatorType class WhatsappContext(BaseModel): @@ -361,3 +361,16 @@ class WhatsappCustom(BaseWhatsapp): custom: Optional[dict] = None message_type: MessageType = MessageType.CUSTOM + + +class ReplyingIndicatorText(BaseModel): + """Model for the replying indicator of type `text` in a WhatsApp conversation. + + This is used to indicate activity within the WhatsApp UI that a message is being replied to. + + Args: + show (bool): Must be set to True to activate the replying indicator. If not included or set to False, the replying indicator will not be shown in the WhatsApp UI. + """ + + show: bool + type_: ReplyingIndicatorType = Field(ReplyingIndicatorType.TEXT, serialization_alias='type') diff --git a/messages/tests/test_messages.py b/messages/tests/test_messages.py index 5cde1bc0..6afae52e 100644 --- a/messages/tests/test_messages.py +++ b/messages/tests/test_messages.py @@ -2,6 +2,7 @@ from os.path import abspath import responses +import re from pytest import raises from vonage_http_client import Auth, HttpClient, HttpClientOptions, HttpRequestError from vonage_messages import ( @@ -9,6 +10,7 @@ MessengerImage, MessengerOptions, MessengerResource, + ReplyingIndicatorText, SendMessageResponse, Sms, ) @@ -191,6 +193,33 @@ def test_mark_whatsapp_message_read_not_found(): assert e.value.response.json()['title'] == 'Not Found' +@responses.activate +def test_mark_whatsapp_message_read_with_replying_indicator(): + responses.add( + responses.PATCH, + 'https://api-eu.vonage.com/v1/messages/asdf', + ) + messages = Messages( + HttpClient(get_mock_jwt_auth(), HttpClientOptions(api_host='api-eu.vonage.com')) + ) + messages.http_client.http_client_options.api_host = 'api-eu.vonage.com' + messages.mark_whatsapp_message_read( + message_uuid='asdf', + replying_indicator=ReplyingIndicatorText( + show=True, + ), + ) + + request_body = loads(responses.calls[0].request.body) + assert request_body == { + "status": "read", + "replying_indicator": { + "show": True, + "type": "text" + } + } + + @responses.activate def test_revoke_rcs_message(): responses.add( diff --git a/messages/tests/test_whatsapp_models.py b/messages/tests/test_whatsapp_models.py index 6967d9dc..3d96beca 100644 --- a/messages/tests/test_whatsapp_models.py +++ b/messages/tests/test_whatsapp_models.py @@ -1,6 +1,7 @@ from copy import deepcopy from vonage_messages.models import ( + ReplyingIndicatorText, WhatsappAudio, WhatsappAudioResource, WhatsappContext, @@ -382,3 +383,14 @@ def test_whatsapp_custom_all_fields(): } assert whatsapp_model.model_dump(by_alias=True) == whatsapp_dict + + +def test_create_replying_indicator(): + whatsapp_model = ReplyingIndicatorText( + show=True, + ) + whatsapp_dict = { + 'show': True, + 'type': 'text', + } + assert whatsapp_model.model_dump(by_alias=True, exclude_none=True) == whatsapp_dict From afc78970f11758ee928b446052e055492b7ce5c2 Mon Sep 17 00:00:00 2001 From: superchilled Date: Fri, 27 Mar 2026 12:57:01 +0000 Subject: [PATCH 45/48] DEVX-10770: linting --- messages/src/vonage_messages/messages.py | 8 ++++++-- messages/src/vonage_messages/models/whatsapp.py | 4 +++- messages/tests/test_messages.py | 6 +----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/messages/src/vonage_messages/messages.py b/messages/src/vonage_messages/messages.py index 93da6919..e284fc00 100644 --- a/messages/src/vonage_messages/messages.py +++ b/messages/src/vonage_messages/messages.py @@ -66,7 +66,9 @@ def send( return SendMessageResponse(**response) @validate_call - def mark_whatsapp_message_read(self, message_uuid: str, replying_indicator: ReplyingIndicatorText = None) -> None: + def mark_whatsapp_message_read( + self, message_uuid: str, replying_indicator: ReplyingIndicatorText = None + ) -> None: """Mark a WhatsApp message as read. Note: to use this method, update the `api_host` attribute of the @@ -82,7 +84,9 @@ def mark_whatsapp_message_read(self, message_uuid: str, replying_indicator: Repl """ body = {'status': 'read'} if replying_indicator is not None: - body['replying_indicator'] = replying_indicator.model_dump(by_alias=True, exclude_none=True) + body['replying_indicator'] = replying_indicator.model_dump( + by_alias=True, exclude_none=True + ) self._http_client.patch( self._http_client.api_host, diff --git a/messages/src/vonage_messages/models/whatsapp.py b/messages/src/vonage_messages/models/whatsapp.py index a7595bd7..a6b44980 100644 --- a/messages/src/vonage_messages/models/whatsapp.py +++ b/messages/src/vonage_messages/models/whatsapp.py @@ -373,4 +373,6 @@ class ReplyingIndicatorText(BaseModel): """ show: bool - type_: ReplyingIndicatorType = Field(ReplyingIndicatorType.TEXT, serialization_alias='type') + type_: ReplyingIndicatorType = Field( + ReplyingIndicatorType.TEXT, serialization_alias='type' + ) diff --git a/messages/tests/test_messages.py b/messages/tests/test_messages.py index 6afae52e..b367b3e4 100644 --- a/messages/tests/test_messages.py +++ b/messages/tests/test_messages.py @@ -2,7 +2,6 @@ from os.path import abspath import responses -import re from pytest import raises from vonage_http_client import Auth, HttpClient, HttpClientOptions, HttpRequestError from vonage_messages import ( @@ -213,10 +212,7 @@ def test_mark_whatsapp_message_read_with_replying_indicator(): request_body = loads(responses.calls[0].request.body) assert request_body == { "status": "read", - "replying_indicator": { - "show": True, - "type": "text" - } + "replying_indicator": {"show": True, "type": "text"}, } From f86e7fe4112dfc8d5a7e21661e1ced9ff0009a4e Mon Sep 17 00:00:00 2001 From: superchilled Date: Mon, 30 Mar 2026 15:15:01 +0100 Subject: [PATCH 46/48] DEVX-10006: fixing RCS card and carousel implementation --- .../src/vonage_messages/models/__init__.py | 8 +- messages/src/vonage_messages/models/rcs.py | 47 +- messages/tests/test_rcs_models.py | 1023 ++++++----------- 3 files changed, 371 insertions(+), 707 deletions(-) diff --git a/messages/src/vonage_messages/models/__init__.py b/messages/src/vonage_messages/models/__init__.py index cf35548e..212d2113 100644 --- a/messages/src/vonage_messages/models/__init__.py +++ b/messages/src/vonage_messages/models/__init__.py @@ -36,10 +36,10 @@ MmsVideo, ) from .rcs import ( - RcsCardBase, - RcsCardItem, + RcsCard, RcsCardMessage, RcsCarousel, + RcsCarouselMessage, RcsCustom, RcsFile, RcsImage, @@ -120,10 +120,10 @@ 'MmsText', 'MmsVcard', 'MmsVideo', - 'RcsCardBase', - 'RcsCardItem', + 'RcsCard', 'RcsCardMessage', 'RcsCarousel', + 'RcsCarouselMessage', 'RcsCustom', 'RcsFile', 'RcsImage', diff --git a/messages/src/vonage_messages/models/rcs.py b/messages/src/vonage_messages/models/rcs.py index 96aa2e92..0b906523 100644 --- a/messages/src/vonage_messages/models/rcs.py +++ b/messages/src/vonage_messages/models/rcs.py @@ -300,7 +300,7 @@ class RcsFile(BaseRcs): message_type: MessageType = MessageType.FILE -class RcsCardBase(BaseModel): +class RcsCard(BaseModel): """Base model for the content of an RCS card. Args: @@ -336,37 +336,13 @@ class RcsCardBase(BaseModel): ] = Field(None, min_length=1, max_length=4) -class RcsCardItem(RcsCardBase): - """Model for the content of an RCS card. - - Args: - title (str): The title of the card. - text (str): The text of the card. - media_url (str): The media URL for the card. Can be an image or a video. - media_height (str): The height of the media on the card (SHORT, MEDIUM, TALL). - media_description (str, Optional): A description of the media for accessibility purposes. - thumbnail_url (str, Optional): The URL of the thumbnail image for the media. If not specified, the media URL will be used as the thumbnail. - media_force_refresh (bool, Optional): Whether to force refresh the media on the card. If true, the media will be refreshed on the device even if the media URL is the same as a previous message. Defaults to false. - suggestions (List, Optional): An optional list of suggestions to include in the message. A card can include up to 4 suggestions. - """ - - media_height: RcsMediaHeight - - -class RcsCardMessage(RcsCardBase, BaseRcs): +class RcsCardMessage(BaseRcs): """Model for an RCS card message. Args: to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. from_ (str): The sender's phone number in E.164 format. Don't use a leading plus sign. - title (str): The title of the card. - text (str): The text of the card. - media_url (str): The media URL for the card. Can be an image or a video. - media_height (str, Optional): The height of the media on the card (SHORT, MEDIUM, TALL). - media_description (str, Optional): A description of the media for accessibility purposes. - thumbnail_url (str, Optional): The URL of the thumbnail image for the media. If not specified, the media URL will be used as the thumbnail. - media_force_refresh (bool, Optional): Whether to force refresh the media on the card. If true, the media will be refreshed on the device even if the media URL is the same as a previous message. Defaults to false. - suggestions (List, Optional): An optional list of suggestions to include in the message. A card can include up to 4 suggestions. + card (RcsCard): The content of the card. ttl (int, Optional): The duration in seconds for which the message is valid. trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. client_ref (str, Optional): An optional client reference. @@ -375,17 +351,28 @@ class RcsCardMessage(RcsCardBase, BaseRcs): rcs: (RcsOptionsCard, Optional): An optional RcsOptionsCard object to include in the message. """ + card: RcsCard rcs: Optional[RcsOptionsCard] = None message_type: MessageType = MessageType.CARD -class RcsCarousel(BaseRcs): +class RcsCarousel(BaseModel): + """Model for the content of an RCS carousel. + + Args: + cards (List[RcsCard]): A list of card items to include in the carousel. Can include up to 10 cards. + """ + + cards: List[RcsCard] = Field(..., min_length=2, max_length=10) + + +class RcsCarouselMessage(BaseRcs): """Model for an RCS carousel message. Args: to (PhoneNumber): The recipient's phone number in E.164 format. Don't use a leading plus sign. from_ (str): The sender's phone number in E.164 format. Don't use a leading plus sign. - cards (List[RcsCardItem]): A list of card items to include in the carousel. Can include up to 10 cards. + carousel (RcsCarousel): The content of the carousel. suggestions (List, Optional): An optional list of suggestions to include in the message. Can include up to 11 suggestions. ttl (int, Optional): The duration in seconds for which the message is valid. trusted_recipient (bool, Optional): Whether the recipient is a trusted recipient. Setting this parameter to true overrides, on a per-message basis, any protections set up via Fraud Defender. Defaults to false. @@ -395,7 +382,7 @@ class RcsCarousel(BaseRcs): rcs: (RcsOptionsCarousel): An RcsOptionsCarousel object to include in the message. """ - cards: List[RcsCardItem] = Field(..., min_length=2, max_length=10) + carousel: RcsCarousel suggestions: Optional[ List[ Union[ diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index f5cf1b0f..b999fad1 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -1,10 +1,10 @@ import pytest from pydantic import ValidationError from vonage_messages.models import ( - RcsCardBase, - RcsCardItem, + RcsCard, RcsCardMessage, RcsCarousel, + RcsCarouselMessage, RcsCustom, RcsFile, RcsImage, @@ -357,22 +357,22 @@ def test_create_rcs_file(): assert rcs_model.model_dump(by_alias=True, exclude_none=True) == rcs_dict -def test_create_rcs_card_base(): - card_base = RcsCardBase( +def test_create_rcs_card(): + card = RcsCard( title='Card title', text='Card description', media_url='https://example.com/image.jpg', ) - card_base_dict = { + card_dict = { 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', } - assert card_base.model_dump(by_alias=True, exclude_none=True) == card_base_dict + assert card.model_dump(by_alias=True, exclude_none=True) == card_dict -def test_create_rcs_card_base_with_optional_params(): - card_base = RcsCardBase( +def test_create_rcs_card_with_optional_params(): + card = RcsCard( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -381,7 +381,7 @@ def test_create_rcs_card_base_with_optional_params(): thumbnail_url='https://example.com/thumbnail.jpg', media_force_refresh=True, ) - card_base_dict = { + card_dict = { 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', @@ -390,11 +390,11 @@ def test_create_rcs_card_base_with_optional_params(): 'thumbnail_url': 'https://example.com/thumbnail.jpg', 'media_force_refresh': True, } - assert card_base.model_dump(by_alias=True, exclude_none=True) == card_base_dict + assert card.model_dump(by_alias=True, exclude_none=True) == card_dict -def test_create_rcs_card_base_with_suggestions(): - card_base = RcsCardBase( +def test_create_rcs_card_with_suggestions(): + card = RcsCard( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -410,7 +410,7 @@ def test_create_rcs_card_base_with_suggestions(): ), ], ) - card_base_dict = { + card_dict = { 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', @@ -428,11 +428,11 @@ def test_create_rcs_card_base_with_suggestions(): }, ], } - assert card_base.model_dump(by_alias=True, exclude_none=True) == card_base_dict + assert card.model_dump(by_alias=True, exclude_none=True) == card_dict -def test_create_rcs_card_base_with_all_suggestion_types(): - card_base_1 = RcsCardBase( +def test_create_rcs_card_with_all_suggestion_types(): + card_1 = RcsCard( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -460,7 +460,7 @@ def test_create_rcs_card_base_with_all_suggestion_types(): ), ], ) - card_base_2 = RcsCardBase( + card_2 = RcsCard( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -489,7 +489,7 @@ def test_create_rcs_card_base_with_all_suggestion_types(): ), ], ) - card_base_dict_1 = { + card_dict_1 = { 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', @@ -521,7 +521,7 @@ def test_create_rcs_card_base_with_all_suggestion_types(): }, ], } - card_base_dict_2 = { + card_dict_2 = { 'title': 'Card title', 'text': 'Card description', 'media_url': 'https://example.com/image.jpg', @@ -553,40 +553,40 @@ def test_create_rcs_card_base_with_all_suggestion_types(): }, ], } - assert card_base_1.model_dump(by_alias=True, exclude_none=True) == card_base_dict_1 - assert card_base_2.model_dump(by_alias=True, exclude_none=True) == card_base_dict_2 + assert card_1.model_dump(by_alias=True, exclude_none=True) == card_dict_1 + assert card_2.model_dump(by_alias=True, exclude_none=True) == card_dict_2 -def test_create_rcs_card_base_without_title(): +def test_create_rcs_card_without_title(): with pytest.raises(ValidationError) as err: - card_base = RcsCardBase( + card = RcsCard( text='Card description', media_url='https://example.com/image.jpg', ) assert "Field required" in str(err.value) -def test_create_rcs_card_base_without_text(): +def test_create_rcs_card_without_text(): with pytest.raises(ValidationError) as err: - card_base = RcsCardBase( + card = RcsCard( title='Card title', media_url='https://example.com/image.jpg', ) assert "Field required" in str(err.value) -def test_create_rcs_card_base_without_media_url(): +def test_create_rcs_card_without_media_url(): with pytest.raises(ValidationError) as err: - card_base = RcsCardBase( + card = RcsCard( title='Card title', text='Card description', ) assert "Field required" in str(err.value) -def test_create_rcs_card_base_with_title_too_short(): +def test_create_rcs_card_with_title_too_short(): with pytest.raises(ValidationError) as err: - card_base = RcsCardBase( + card = RcsCard( title='', text='Card description', media_url='https://example.com/image.jpg', @@ -594,9 +594,9 @@ def test_create_rcs_card_base_with_title_too_short(): assert "String should have at least 1 character" in str(err.value) -def test_create_rcs_card_base_with_title_too_long(): +def test_create_rcs_card_with_title_too_long(): with pytest.raises(ValidationError) as err: - card_base = RcsCardBase( + card = RcsCard( title='A' * 200 + 'B', text='Card description', media_url='https://example.com/image.jpg', @@ -604,11 +604,9 @@ def test_create_rcs_card_base_with_title_too_long(): assert "String should have at most 200 characters" in str(err.value) -def test_create_rcs_card_base_with_text_too_short(): +def test_create_rcs_card_with_text_too_short(): with pytest.raises(ValidationError) as err: - card_base = RcsCardBase( - to='1234567890', - from_='asdf1234', + card = RcsCard( title='Card title', text='', media_url='https://example.com/image.jpg', @@ -616,11 +614,9 @@ def test_create_rcs_card_base_with_text_too_short(): assert "String should have at least 1 character" in str(err.value) -def test_create_rcs_card_base_with_text_too_long(): +def test_create_rcs_card_with_text_too_long(): with pytest.raises(ValidationError) as err: - card_base = RcsCardBase( - to='1234567890', - from_='asdf1234', + card = RcsCard( title='Card title', text='A' * 2000 + 'B', media_url='https://example.com/image.jpg', @@ -628,9 +624,9 @@ def test_create_rcs_card_base_with_text_too_long(): assert "String should have at most 2000 characters" in str(err.value) -def test_create_rcs_card_base_with_insuffient_suggestions(): +def test_create_rcs_card_with_insuffient_suggestions(): with pytest.raises(ValidationError) as err: - card_base = RcsCardBase( + card = RcsCard( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -639,9 +635,9 @@ def test_create_rcs_card_base_with_insuffient_suggestions(): assert "List should have at least 1 item" in str(err.value) -def test_create_rcs_card_base_with_too_many_suggestions(): +def test_create_rcs_card_with_too_many_suggestions(): with pytest.raises(ValidationError) as err: - card_base = RcsCardBase( + card = RcsCard( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -656,9 +652,9 @@ def test_create_rcs_card_base_with_too_many_suggestions(): assert "List should have at most 4 items" in str(err.value) -def test_create_rcs_card_base_with_inavalid_suggestion_types(): +def test_create_rcs_card_with_inavalid_suggestion_types(): with pytest.raises(ValidationError) as err: - card_base = RcsCardBase( + card = RcsCard( title='Card title', text='Card description', media_url='https://example.com/image.jpg', @@ -677,16 +673,20 @@ def test_create_rcs_card_message(): card = RcsCardMessage( to='1234567890', from_='asdf1234', - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', + card=RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + ), ) card_dict = { 'to': '1234567890', 'from': 'asdf1234', - 'title': 'Card title', - 'text': 'Card description', - 'media_url': 'https://example.com/image.jpg', + 'card': { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + }, 'channel': 'rcs', 'message_type': 'card', } @@ -697,13 +697,11 @@ def test_create_rcs_card_message_with_optional_params(): card = RcsCardMessage( to='1234567890', from_='asdf1234', - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_description='Image description', - media_height='MEDIUM', - thumbnail_url='https://example.com/thumbnail.jpg', - media_force_refresh=True, + card=RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + ), rcs=RcsOptionsCard( card_orientation='VERTICAL', image_alignment='LEFT', @@ -712,13 +710,11 @@ def test_create_rcs_card_message_with_optional_params(): card_dict = { 'to': '1234567890', 'from': 'asdf1234', - 'title': 'Card title', - 'text': 'Card description', - 'media_url': 'https://example.com/image.jpg', - 'media_description': 'Image description', - 'media_height': 'MEDIUM', - 'thumbnail_url': 'https://example.com/thumbnail.jpg', - 'media_force_refresh': True, + 'card': { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + }, 'rcs': { 'card_orientation': 'VERTICAL', 'image_alignment': 'LEFT', @@ -729,13 +725,200 @@ def test_create_rcs_card_message_with_optional_params(): assert card.model_dump(by_alias=True, exclude_none=True) == card_dict -def test_create_rcs_card_message_with_suggestions(): - card = RcsCardMessage( +def test_create_rcs_card_message_without_card(): + with pytest.raises(ValidationError) as err: + card = RcsCardMessage( + to='1234567890', + from_='asdf1234', + ) + assert "Field required" in str(err.value) + + +def test_create_rcs_carousel(): + carousel = RcsCarousel( + cards=[ + RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] + * 2, + ) + carousel_dict = { + 'cards': [ + { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'media_height': 'MEDIUM', + } + ] + * 2, + } + assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict + + +def test_create_rcs_carousel_with_insufficient_cards(): + with pytest.raises(ValidationError) as err: + carousel = RcsCarousel( + cards=[ + RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ], + ) + assert "List should have at least 2 items" in str(err.value) + + +def test_create_rcs_carousel_with_too_many_cards(): + with pytest.raises(ValidationError) as err: + carousel = RcsCarousel( + cards=[ + RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] * 11, + ) + assert "List should have at most 10 items" in str(err.value) + + +def test_create_rcs_carousel_with_invalid_card_type(): + with pytest.raises(ValidationError) as err: + carousel = RcsCarousel( + cards=[ + RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ), + RcsCardMessage( + to='1234567890', + from_='asdf1234', + card=RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + ), + ), + ], + ) + assert "Input should be a valid dictionary or instance" in str(err.value) + + +def test_create_rcs_carousel_message(): + carousel = RcsCarouselMessage( to='1234567890', from_='asdf1234', - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', + carousel=RcsCarousel( + cards=[ + RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] + * 2, + ), + rcs=RcsOptionsCarousel( + card_width='MEDIUM', + ), + ) + carousel_dict = { + 'to': '1234567890', + 'from': 'asdf1234', + 'carousel': { + 'cards': [ + { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'media_height': 'MEDIUM', + } + ] + * 2, + }, + 'rcs': { + 'card_width': 'MEDIUM', + }, + 'channel': 'rcs', + 'message_type': 'carousel', + } + assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict + + +def test_create_rcs_carousel_message_with_optional_params(): + carousel = RcsCarouselMessage( + to='1234567890', + from_='asdf1234', + carousel=RcsCarousel( + cards=[ + RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_description='Image description', + media_height='MEDIUM', + thumbnail_url='https://example.com/thumbnail.jpg', + media_force_refresh=True, + ) + ] + * 2, + ), + rcs=RcsOptionsCarousel( + card_width='MEDIUM', + ), + ) + carousel_dict = { + 'to': '1234567890', + 'from': 'asdf1234', + 'carousel': { + 'cards': [ + { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'media_description': 'Image description', + 'media_height': 'MEDIUM', + 'thumbnail_url': 'https://example.com/thumbnail.jpg', + 'media_force_refresh': True, + } + ] + * 2, + }, + 'rcs': { + 'card_width': 'MEDIUM', + }, + 'channel': 'rcs', + 'message_type': 'carousel', + } + assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict + + +def test_create_rcs_carousel_message_with_suggestions(): + carousel = RcsCarouselMessage( + to='1234567890', + from_='asdf1234', + carousel=RcsCarousel( + cards=[ + RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] + * 2, + ), suggestions=[ RcsSuggestionReply( text='Reply', @@ -747,13 +930,24 @@ def test_create_rcs_card_message_with_suggestions(): phone_number='447900000000', ), ], + rcs=RcsOptionsCarousel( + card_width='MEDIUM', + ), ) - card_dict = { + carousel_dict = { 'to': '1234567890', 'from': 'asdf1234', - 'title': 'Card title', - 'text': 'Card description', - 'media_url': 'https://example.com/image.jpg', + 'carousel': { + 'cards': [ + { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'media_height': 'MEDIUM', + } + ] + * 2, + }, 'suggestions': [ { 'type': 'reply', @@ -767,501 +961,30 @@ def test_create_rcs_card_message_with_suggestions(): 'phone_number': '447900000000', }, ], + 'rcs': { + 'card_width': 'MEDIUM', + }, 'channel': 'rcs', - 'message_type': 'card', + 'message_type': 'carousel', } - assert card.model_dump(by_alias=True, exclude_none=True) == card_dict - - -def test_create_rcs_card_message_without_title(): - with pytest.raises(ValidationError) as err: - card = RcsCardMessage( - to='1234567890', - from_='asdf1234', - text='Card description', - media_url='https://example.com/image.jpg', - ) - assert "Field required" in str(err.value) - - -def test_create_rcs_card_message_without_text(): - with pytest.raises(ValidationError) as err: - card = RcsCardMessage( - to='1234567890', - from_='asdf1234', - title='Card title', - media_url='https://example.com/image.jpg', - ) - assert "Field required" in str(err.value) - - -def test_create_rcs_card_message_without_media_url(): - with pytest.raises(ValidationError) as err: - card = RcsCardMessage( - to='1234567890', - from_='asdf1234', - title='Card title', - text='Card description', - ) - assert "Field required" in str(err.value) - - -def test_create_rcs_card_message_with_title_too_short(): - with pytest.raises(ValidationError) as err: - card = RcsCardMessage( - to='1234567890', - from_='asdf1234', - title='', - text='Card description', - media_url='https://example.com/image.jpg', - ) - assert "String should have at least 1 character" in str(err.value) + assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict -def test_create_rcs_card_message_with_title_too_long(): - with pytest.raises(ValidationError) as err: - card = RcsCardMessage( - to='1234567890', - from_='asdf1234', - title='A' * 200 + 'B', - text='Card description', - media_url='https://example.com/image.jpg', - ) - assert "String should have at most 200 characters" in str(err.value) - - -def test_create_rcs_card_message_with_text_too_short(): - with pytest.raises(ValidationError) as err: - card = RcsCardMessage( - to='1234567890', - from_='asdf1234', - title='Card title', - text='', - media_url='https://example.com/image.jpg', - ) - assert "String should have at least 1 character" in str(err.value) - - -def test_create_rcs_card_message_with_text_too_long(): - with pytest.raises(ValidationError) as err: - card = RcsCardMessage( - to='1234567890', - from_='asdf1234', - title='Card title', - text='A' * 2000 + 'B', - media_url='https://example.com/image.jpg', - ) - assert "String should have at most 2000 characters" in str(err.value) - - -def test_create_rcs_card_message_with_insuffient_suggestions(): - with pytest.raises(ValidationError) as err: - card = RcsCardMessage( - to='1234567890', - from_='asdf1234', - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - suggestions=[], - ) - assert "List should have at least 1 item" in str(err.value) - - -def test_create_rcs_card_message_with_too_many_suggestions(): - with pytest.raises(ValidationError) as err: - card = RcsCardMessage( - to='1234567890', - from_='asdf1234', - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - suggestions=[ - RcsSuggestionReply( - text='Reply', - postback_data='postback-data', - ), - ] - * 5, - ) - assert "List should have at most 4 items" in str(err.value) - - -def test_create_rcs_card_message_with_inavalid_suggestion_types(): - with pytest.raises(ValidationError) as err: - card = RcsCardMessage( - to='1234567890', - from_='asdf1234', - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - suggestions=[ - RcsSuggestionReply( - text='Reply', - postback_data='postback-data', - ), - "Invalid suggestion type", - ], - ) - assert "Input should be a valid dictionary or instance" in str(err.value) - - -def test_create_rcs_card_item(): - card_content = RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_height='MEDIUM', - ) - card_item_dict = { - 'title': 'Card title', - 'text': 'Card description', - 'media_url': 'https://example.com/image.jpg', - 'media_height': 'MEDIUM', - } - assert card_content.model_dump(by_alias=True, exclude_none=True) == card_item_dict - - -def test_create_rcs_card_item_with_optional_params(): - card_content = RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_description='Image description', - media_height='MEDIUM', - thumbnail_url='https://example.com/thumbnail.jpg', - media_force_refresh=True, - ) - card_item_dict = { - 'title': 'Card title', - 'text': 'Card description', - 'media_url': 'https://example.com/image.jpg', - 'media_description': 'Image description', - 'media_height': 'MEDIUM', - 'thumbnail_url': 'https://example.com/thumbnail.jpg', - 'media_force_refresh': True, - } - assert card_content.model_dump(by_alias=True, exclude_none=True) == card_item_dict - - -def test_create_rcs_card_item_with_suggestions(): - card_content = RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_height='MEDIUM', - suggestions=[ - RcsSuggestionReply( - text='Reply', - postback_data='postback-data', - ), - RcsSuggestionActionDial( - text='Call us', - postback_data='postback-data', - phone_number='447900000000', - ), - ], - ) - card_item_dict = { - 'title': 'Card title', - 'text': 'Card description', - 'media_url': 'https://example.com/image.jpg', - 'media_height': 'MEDIUM', - 'suggestions': [ - { - 'type': 'reply', - 'text': 'Reply', - 'postback_data': 'postback-data', - }, - { - 'type': 'dial', - 'text': 'Call us', - 'postback_data': 'postback-data', - 'phone_number': '447900000000', - }, - ], - } - assert card_content.model_dump(by_alias=True, exclude_none=True) == card_item_dict - - -def test_create_rcs_card_item_without_title(): - with pytest.raises(ValidationError) as err: - card = RcsCardItem( - text='Card description', - media_url='https://example.com/image.jpg', - ) - assert "Field required" in str(err.value) - - -def test_create_rcs_card_item_without_text(): - with pytest.raises(ValidationError) as err: - card = RcsCardItem( - title='Card title', - media_url='https://example.com/image.jpg', - ) - assert "Field required" in str(err.value) - - -def test_create_rcs_card_item_without_media_url(): - with pytest.raises(ValidationError) as err: - card = RcsCardItem( - title='Card title', - text='Card description', - ) - assert "Field required" in str(err.value) - - -def test_create_rcs_card_item_without_media_height(): - with pytest.raises(ValidationError) as err: - card = RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - ) - assert "Field required" in str(err.value) - - -def test_create_rcs_card_item_with_title_too_short(): - with pytest.raises(ValidationError) as err: - card = RcsCardItem( - title='', - text='Card description', - media_url='https://example.com/image.jpg', - ) - assert "String should have at least 1 character" in str(err.value) - - -def test_create_rcs_card_item_with_title_too_long(): - with pytest.raises(ValidationError) as err: - card = RcsCardItem( - title='A' * 200 + 'B', - text='Card description', - media_url='https://example.com/image.jpg', - ) - assert "String should have at most 200 characters" in str(err.value) - - -def test_create_rcs_card_item_with_text_too_short(): - with pytest.raises(ValidationError) as err: - card = RcsCardItem( - title='Card title', - text='', - media_url='https://example.com/image.jpg', - ) - assert "String should have at least 1 character" in str(err.value) - - -def test_create_rcs_card_item_with_text_too_long(): - with pytest.raises(ValidationError) as err: - card = RcsCardItem( - title='Card title', - text='A' * 2000 + 'B', - media_url='https://example.com/image.jpg', - ) - assert "String should have at most 2000 characters" in str(err.value) - - -def test_create_rcs_card_item_with_insuffient_suggestions(): - with pytest.raises(ValidationError) as err: - card = RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - suggestions=[], - ) - assert "List should have at least 1 item" in str(err.value) - - -def test_create_rcs_card_item_with_too_many_suggestions(): - with pytest.raises(ValidationError) as err: - card = RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - suggestions=[ - RcsSuggestionReply( - text='Reply', - postback_data='postback-data', - ), - ] - * 5, - ) - assert "List should have at most 4 items" in str(err.value) - - -def test_create_rcs_card_item_with_inavalid_suggestion_types(): - with pytest.raises(ValidationError) as err: - card = RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - suggestions=[ - RcsSuggestionReply( - text='Reply', - postback_data='postback-data', - ), - "Invalid suggestion type", - ], - ) - assert "Input should be a valid dictionary or instance" in str(err.value) - - -def test_create_rcs_carousel(): - carousel = RcsCarousel( +def test_create_rcs_carousel_message_with_all_suggestion_types(): + carousel = RcsCarouselMessage( to='1234567890', from_='asdf1234', - cards=[ - RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_height='MEDIUM', - ) - ] - * 2, - rcs=RcsOptionsCarousel( - card_width='MEDIUM', - ), - ) - carousel_dict = { - 'to': '1234567890', - 'from': 'asdf1234', - 'cards': [ - { - 'title': 'Card title', - 'text': 'Card description', - 'media_url': 'https://example.com/image.jpg', - 'media_height': 'MEDIUM', - } - ] - * 2, - 'rcs': { - 'card_width': 'MEDIUM', - }, - 'channel': 'rcs', - 'message_type': 'carousel', - } - assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict - - -def test_create_rcs_carousel_with_optional_params(): - carousel = RcsCarousel( - to='1234567890', - from_='asdf1234', - cards=[ - RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_description='Image description', - media_height='MEDIUM', - thumbnail_url='https://example.com/thumbnail.jpg', - media_force_refresh=True, - ) - ] - * 2, - rcs=RcsOptionsCarousel( - card_width='MEDIUM', - ), - ) - carousel_dict = { - 'to': '1234567890', - 'from': 'asdf1234', - 'cards': [ - { - 'title': 'Card title', - 'text': 'Card description', - 'media_url': 'https://example.com/image.jpg', - 'media_description': 'Image description', - 'media_height': 'MEDIUM', - 'thumbnail_url': 'https://example.com/thumbnail.jpg', - 'media_force_refresh': True, - } - ] - * 2, - 'rcs': { - 'card_width': 'MEDIUM', - }, - 'channel': 'rcs', - 'message_type': 'carousel', - } - assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict - - -def test_create_rcs_carousel_with_suggestions(): - carousel = RcsCarousel( - to='1234567890', - from_='asdf1234', - cards=[ - RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_height='MEDIUM', - ) - ] - * 2, - suggestions=[ - RcsSuggestionReply( - text='Reply', - postback_data='postback-data', - ), - RcsSuggestionActionDial( - text='Call us', - postback_data='postback-data', - phone_number='447900000000', - ), - ], - rcs=RcsOptionsCarousel( - card_width='MEDIUM', + carousel=RcsCarousel( + cards=[ + RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] + * 2, ), - ) - carousel_dict = { - 'to': '1234567890', - 'from': 'asdf1234', - 'cards': [ - { - 'title': 'Card title', - 'text': 'Card description', - 'media_url': 'https://example.com/image.jpg', - 'media_height': 'MEDIUM', - } - ] - * 2, - 'suggestions': [ - { - 'type': 'reply', - 'text': 'Reply', - 'postback_data': 'postback-data', - }, - { - 'type': 'dial', - 'text': 'Call us', - 'postback_data': 'postback-data', - 'phone_number': '447900000000', - }, - ], - 'rcs': { - 'card_width': 'MEDIUM', - }, - 'channel': 'rcs', - 'message_type': 'carousel', - } - assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict - - -def test_create_rcs_carousel_with_all_suggestion_types(): - carousel = RcsCarousel( - to='1234567890', - from_='asdf1234', - cards=[ - RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_height='MEDIUM', - ) - ] - * 2, suggestions=[ RcsSuggestionReply( text='Reply', @@ -1314,15 +1037,17 @@ def test_create_rcs_carousel_with_all_suggestion_types(): carousel_dict = { 'to': '1234567890', 'from': 'asdf1234', - 'cards': [ - { - 'title': 'Card title', - 'text': 'Card description', - 'media_url': 'https://example.com/image.jpg', - 'media_height': 'MEDIUM', - } - ] - * 2, + 'carousel': { + 'cards': [ + { + 'title': 'Card title', + 'text': 'Card description', + 'media_url': 'https://example.com/image.jpg', + 'media_height': 'MEDIUM', + } + ] + * 2, + }, 'suggestions': [ { 'type': 'reply', @@ -1384,106 +1109,54 @@ def test_create_rcs_carousel_with_all_suggestion_types(): assert carousel.model_dump(by_alias=True, exclude_none=True) == carousel_dict -def test_create_rcs_carousel_without_rcs_options(): - with pytest.raises(ValidationError) as err: - carousel = RcsCarousel( - to='1234567890', - from_='asdf1234', - cards=[ - RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_height='MEDIUM', - ) - ] - * 2, - ) - assert "Field required" in str(err.value) - - -def test_create_rcs_carousel_with_insufficient_cards(): +def test_create_rcs_carousel_message_without_carousel(): with pytest.raises(ValidationError) as err: - carousel = RcsCarousel( + carousel = RcsCarouselMessage( to='1234567890', from_='asdf1234', - cards=[ - RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_height='MEDIUM', - ) - ], rcs=RcsOptionsCarousel( card_width='MEDIUM', ), ) - assert "List should have at least 2 items" in str(err.value) + assert "Field required" in str(err.value) -def test_create_rcs_carousel_with_too_many_cards(): +def test_create_rcs_carousel_message_without_rcs_options(): with pytest.raises(ValidationError) as err: - carousel = RcsCarousel( + carousel = RcsCarouselMessage( to='1234567890', from_='asdf1234', - cards=[ - RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_height='MEDIUM', - ) - ] - * 11, - rcs=RcsOptionsCarousel( - card_width='MEDIUM', + carousel=RcsCarousel( + cards=[ + RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] + * 2, ), ) - assert "List should have at most 10 items" in str(err.value) + assert "Field required" in str(err.value) -def test_create_rcs_carousel_with_invalid_card_type(): +def test_create_rcs_carousel_message_with_insuffient_suggestions(): with pytest.raises(ValidationError) as err: - carousel = RcsCarousel( + carousel = RcsCarouselMessage( to='1234567890', from_='asdf1234', - cards=[ - RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_height='MEDIUM', - ), - RcsCardMessage( - to='1234567890', - from_='asdf1234', - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - ), - ], - rcs=RcsOptionsCarousel( - card_width='MEDIUM', + carousel=RcsCarousel( + cards=[ + RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] + * 2, ), - ) - assert "Input should be a valid dictionary or instance" in str(err.value) - - -def test_create_rcs_carousel_with_insuffient_suggestions(): - with pytest.raises(ValidationError) as err: - carousel = RcsCarousel( - to='1234567890', - from_='asdf1234', - cards=[ - RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_height='MEDIUM', - ) - ] - * 2, suggestions=[], rcs=RcsOptionsCarousel( card_width='MEDIUM', @@ -1492,20 +1165,22 @@ def test_create_rcs_carousel_with_insuffient_suggestions(): assert "List should have at least 1 item" in str(err.value) -def test_create_rcs_carousel_with_too_many_suggestions(): +def test_create_rcs_carousel_message_with_too_many_suggestions(): with pytest.raises(ValidationError) as err: - carousel = RcsCarousel( + carousel = RcsCarouselMessage( to='1234567890', from_='asdf1234', - cards=[ - RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_height='MEDIUM', - ) - ] - * 2, + carousel=RcsCarousel( + cards=[ + RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] + * 2, + ), suggestions=[ RcsSuggestionReply( text='Reply', @@ -1520,20 +1195,22 @@ def test_create_rcs_carousel_with_too_many_suggestions(): assert "List should have at most 11 items" in str(err.value) -def test_create_rcs_carousel_with_inavalid_suggestion_types(): +def test_create_rcs_carousel_message_with_inavalid_suggestion_types(): with pytest.raises(ValidationError) as err: - carousel = RcsCarousel( + carousel = RcsCarouselMessage( to='1234567890', from_='asdf1234', - cards=[ - RcsCardItem( - title='Card title', - text='Card description', - media_url='https://example.com/image.jpg', - media_height='MEDIUM', - ) - ] - * 2, + carousel=RcsCarousel( + cards=[ + RcsCard( + title='Card title', + text='Card description', + media_url='https://example.com/image.jpg', + media_height='MEDIUM', + ) + ] + * 2, + ), suggestions=[ RcsSuggestionReply( text='Reply', From 294fe5aade602641ce718253808c1af46e492fd3 Mon Sep 17 00:00:00 2001 From: superchilled Date: Mon, 30 Mar 2026 15:21:14 +0100 Subject: [PATCH 47/48] DEVX-10006: linting --- messages/tests/test_rcs_models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index b999fad1..a97f0df8 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -785,7 +785,8 @@ def test_create_rcs_carousel_with_too_many_cards(): media_url='https://example.com/image.jpg', media_height='MEDIUM', ) - ] * 11, + ] + * 11, ) assert "List should have at most 10 items" in str(err.value) @@ -804,7 +805,7 @@ def test_create_rcs_carousel_with_invalid_card_type(): to='1234567890', from_='asdf1234', card=RcsCard( - title='Card title', + title='Card title', text='Card description', media_url='https://example.com/image.jpg', ), From 0ba7029d925e2f6bea72939303b03e42150be13c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:31:31 +0000 Subject: [PATCH 48/48] feat: add missing validation tests for SMS, MMS, RCS and WhatsApp models Agent-Logs-Url: https://github.com/Vonage/vonage-python-sdk/sessions/50d9d0a2-7556-4233-8b65-abc9902d5b7f Co-authored-by: dragonmantank <108948+dragonmantank@users.noreply.github.com> --- messages/tests/test_mms_models.py | 22 +++++++++++ messages/tests/test_rcs_models.py | 52 +++++++++++++++++++++++++ messages/tests/test_sms_models.py | 23 +++++++++++ messages/tests/test_whatsapp_models.py | 54 ++++++++++++++++++++++++++ 4 files changed, 151 insertions(+) diff --git a/messages/tests/test_mms_models.py b/messages/tests/test_mms_models.py index 09b0b11e..72ca6059 100644 --- a/messages/tests/test_mms_models.py +++ b/messages/tests/test_mms_models.py @@ -520,3 +520,25 @@ def test_create_mms_content_with_invalid_content_item(): ], ) assert "Input should be a valid dictionary or instance" in str(err.value) + + +def test_create_mms_with_ttl_too_low(): + with pytest.raises(ValidationError) as err: + MmsImage( + to='1234567890', + from_='1234567890', + image=MmsResource(url='https://example.com/image.jpg'), + ttl=299, + ) + assert 'greater than or equal to 300' in str(err.value) + + +def test_create_mms_with_ttl_too_high(): + with pytest.raises(ValidationError) as err: + MmsImage( + to='1234567890', + from_='1234567890', + image=MmsResource(url='https://example.com/image.jpg'), + ttl=259201, + ) + assert 'less than or equal to 259200' in str(err.value) diff --git a/messages/tests/test_rcs_models.py b/messages/tests/test_rcs_models.py index a97f0df8..30dc0a03 100644 --- a/messages/tests/test_rcs_models.py +++ b/messages/tests/test_rcs_models.py @@ -1804,3 +1804,55 @@ def test_create_rcs_options_carousel_card_width_with_invalid_option(): card_width='INVALID_WIDTH', ) assert "Input should be 'SMALL' or 'MEDIUM'" in str(err.value) + + +def test_create_rcs_text_too_short(): + with pytest.raises(ValidationError) as err: + RcsText( + to='1234567890', + from_='asdf1234', + text='', + ) + assert 'String should have at least 1 character' in str(err.value) + + +def test_create_rcs_text_too_long(): + with pytest.raises(ValidationError) as err: + RcsText( + to='1234567890', + from_='asdf1234', + text='a' * 3073, + ) + assert 'String should have at most 3072 characters' in str(err.value) + + +def test_create_rcs_with_ttl_too_low(): + with pytest.raises(ValidationError) as err: + RcsText( + to='1234567890', + from_='asdf1234', + text='Hello, World!', + ttl=19, + ) + assert 'greater than or equal to 20' in str(err.value) + + +def test_create_rcs_with_ttl_too_high(): + with pytest.raises(ValidationError) as err: + RcsText( + to='1234567890', + from_='asdf1234', + text='Hello, World!', + ttl=259201, + ) + assert 'less than or equal to 259200' in str(err.value) + + +def test_create_rcs_with_invalid_from_field(): + with pytest.raises(ValidationError) as err: + RcsText( + to='1234567890', + from_='invalid from!', + text='Hello, World!', + ) + assert 'String should match pattern' in str(err.value) diff --git a/messages/tests/test_sms_models.py b/messages/tests/test_sms_models.py index 800bef52..4068f1ec 100644 --- a/messages/tests/test_sms_models.py +++ b/messages/tests/test_sms_models.py @@ -1,3 +1,5 @@ +import pytest +from pydantic import ValidationError from vonage_messages.models import Sms, SmsOptions from vonage_messages.models.enums import EncodingType, WebhookVersion @@ -56,3 +58,24 @@ def test_create_sms_all_fields(): } assert sms_model.model_dump(by_alias=True) == sms_dict + + +def test_create_sms_text_too_long(): + with pytest.raises(ValidationError) as err: + Sms( + to='1234567890', + from_='1234567890', + text='a' * 1001, + ) + assert 'String should have at most 1000 characters' in str(err.value) + + +def test_create_sms_with_invalid_encoding_type(): + with pytest.raises(ValidationError) as err: + Sms( + to='1234567890', + from_='1234567890', + text='Hello, World!', + sms=SmsOptions(encoding_type='invalid'), + ) + assert 'Input should be' in str(err.value) diff --git a/messages/tests/test_whatsapp_models.py b/messages/tests/test_whatsapp_models.py index 3d96beca..c0ae7e58 100644 --- a/messages/tests/test_whatsapp_models.py +++ b/messages/tests/test_whatsapp_models.py @@ -1,5 +1,7 @@ from copy import deepcopy +import pytest +from pydantic import ValidationError from vonage_messages.models import ( ReplyingIndicatorText, WhatsappAudio, @@ -394,3 +396,55 @@ def test_create_replying_indicator(): 'type': 'text', } assert whatsapp_model.model_dump(by_alias=True, exclude_none=True) == whatsapp_dict + + +def test_whatsapp_text_too_long(): + with pytest.raises(ValidationError) as err: + WhatsappText( + to='1234567890', + from_='1234567890', + text='a' * 4097, + ) + assert 'String should have at most 4096 characters' in str(err.value) + + +def test_whatsapp_audio_url_too_short(): + with pytest.raises(ValidationError) as err: + WhatsappAudio( + to='1234567890', + from_='1234567890', + audio=WhatsappAudioResource(url='short'), + ) + assert 'String should have at least 10 characters' in str(err.value) + + +def test_whatsapp_audio_url_too_long(): + with pytest.raises(ValidationError) as err: + WhatsappAudio( + to='1234567890', + from_='1234567890', + audio=WhatsappAudioResource(url='https://' + 'a' * 2000), + ) + assert 'String should have at most 2000 characters' in str(err.value) + + +def test_whatsapp_image_caption_too_short(): + with pytest.raises(ValidationError) as err: + WhatsappImage( + to='1234567890', + from_='1234567890', + image=WhatsappImageResource(url='https://example.com/image.jpg', caption=''), + ) + assert 'String should have at least 1 character' in str(err.value) + + +def test_whatsapp_image_caption_too_long(): + with pytest.raises(ValidationError) as err: + WhatsappImage( + to='1234567890', + from_='1234567890', + image=WhatsappImageResource( + url='https://example.com/image.jpg', caption='a' * 3001 + ), + ) + assert 'String should have at most 3000 characters' in str(err.value)