diff --git a/novu/dto/__init__.py b/novu/dto/__init__.py index 181cda18..56e1e7aa 100644 --- a/novu/dto/__init__.py +++ b/novu/dto/__init__.py @@ -43,6 +43,8 @@ from novu.dto.step_filter import StepFilterDto from novu.dto.subscriber import ( PaginatedSubscriberDto, + SubscriberChannelSettingsCredentialsDto, + SubscriberChannelSettingsDto, SubscriberDto, SubscriberPreferenceChannelDto, SubscriberPreferenceDto, @@ -92,6 +94,8 @@ "PaginatedTopicDto", "StepFilterDto", "SubscriberDto", + "SubscriberChannelSettingsDto", + "SubscriberChannelSettingsCredentialsDto", "SubscriberPreferenceChannelDto", "SubscriberPreferenceDto", "SubscriberPreferencePreferenceDto", diff --git a/novu/dto/subscriber.py b/novu/dto/subscriber.py index 35af0b9c..e6872400 100644 --- a/novu/dto/subscriber.py +++ b/novu/dto/subscriber.py @@ -1,8 +1,9 @@ """This module is used to gather all DTO definitions related to the Subscriber resource in Novu""" import dataclasses -from typing import Optional +from typing import List, Optional from novu.dto.base import CamelCaseDto, DtoDescriptor, DtoIterableDescriptor +from novu.enums.provider import ProviderIdEnum @dataclasses.dataclass @@ -70,11 +71,44 @@ class SubscriberPreferenceDto(CamelCaseDto["SubscriberPreferenceDto"]): """The identifiers of the template linked to the preferences and its criticality""" +@dataclasses.dataclass +class SubscriberChannelSettingsCredentialsDto(CamelCaseDto["SubscriberChannelSettingsCredentialsDto"]): + """Credentials payload for the specified provider.""" + + webhook_url: str + """Webhook url used by chat app integrations. The webhook should be obtained from the chat app provider""" + + channel: str + """Channel specification for Mattermost chat notifications""" + + device_tokens: Optional[List[str]] = None + """Contains an array of the subscriber device tokens for a given provider. Used on Push integrations""" + + +@dataclasses.dataclass +class SubscriberChannelSettingsDto(CamelCaseDto["SubscriberChannelSettingsDto"]): + """Definition of channel settings for subscriber.""" + + provider_id: ProviderIdEnum + """The provider identifier for the credentials""" + + _integration_id: str + """Id of the integration that is used for this channel""" + + credentials: DtoDescriptor[SubscriberChannelSettingsCredentialsDto] = DtoDescriptor[ + SubscriberChannelSettingsCredentialsDto + ](item_cls=SubscriberChannelSettingsCredentialsDto) + """Credentials of this channel""" + + integration_identifier: Optional[str] = None + """The integration identifier""" + + @dataclasses.dataclass class SubscriberDto(CamelCaseDto["SubscriberDto"]): # pylint: disable=R0902 """Definition of subscriber""" - camel_case_fields = ["subscriber_id", "email", "first_name", "last_name", "phone", "avatar", "locale"] + camel_case_fields = ["subscriber_id", "email", "first_name", "last_name", "phone", "avatar", "locale", "channels"] # Actually, only these fields are editable in Novu, so prevent any activity on others subscriber_id: str @@ -107,7 +141,10 @@ class SubscriberDto(CamelCaseDto["SubscriberDto"]): # pylint: disable=R0902 _organization_id: Optional[str] = None """Organization ID in Novu internal storage system""" - # TODO: add channels + channels: DtoIterableDescriptor[SubscriberChannelSettingsDto] = DtoIterableDescriptor[SubscriberChannelSettingsDto]( + default_factory=list, item_cls=SubscriberChannelSettingsDto + ) + """Subscriber provider specific credentials""" deleted: Optional[bool] = None """If the subscriber is deleted""" diff --git a/tests/api/test_subscriber.py b/tests/api/test_subscriber.py index 08e0849c..56b9bdcd 100644 --- a/tests/api/test_subscriber.py +++ b/tests/api/test_subscriber.py @@ -5,13 +5,15 @@ from novu.config import NovuConfig from novu.dto.subscriber import ( PaginatedSubscriberDto, + SubscriberChannelSettingsCredentialsDto, + SubscriberChannelSettingsDto, SubscriberDto, SubscriberPreferenceChannelDto, SubscriberPreferenceDto, SubscriberPreferencePreferenceDto, SubscriberPreferenceTemplateDto, ) -from novu.enums import Channel +from novu.enums import Channel, ChatProviderIdEnum from tests.factories import MockResponse @@ -50,6 +52,7 @@ def setUpClass(cls) -> None: _id="63dafedbc037e013fd82d37a", _environment_id="63dafed97779f59258e38445", _organization_id="63dafed97779f59258e3843f", + channels=[], deleted=False, created_at="2023-02-02T00:07:55.459Z", updated_at="2023-02-06T23:03:22.645Z", @@ -134,6 +137,7 @@ def test_create_subscriber(self, mock_request: mock.MagicMock) -> None: "phone": None, "avatar": None, "locale": None, + "channels": None, }, params=None, timeout=5, @@ -156,6 +160,77 @@ def test_get_subscriber(self, mock_request: mock.MagicMock) -> None: timeout=5, ) + @mock.patch("requests.request") + def test_get_subscriber_with_credentials_info(self, mock_request: mock.MagicMock) -> None: + mock_request.return_value = MockResponse( + 200, + { + "data": { + "_id": "63dafedbc037e013fd82d37a", + "_organizationId": "63dafed97779f59258e3843f", + "_environmentId": "63dafed97779f59258e38445", + "subscriberId": "63dafed4117f8c850991ec4a", + "channels": [ + { + "provider_id": "slack", + "_integration_id": "64f6d74be166fcd7f2751111", + "credentials": {"webhook_url": "TEST", "channel": "slack", "device_tokens": ["TEST"]}, + "integration_identifier": None, + } + ], + "deleted": False, + "createdAt": "2023-02-02T00:07:55.459Z", + "updatedAt": "2023-02-06T23:03:22.645Z", + "__v": 0, + "isOnline": False, + "email": "oscar.marie-taillefer@spikeelabs.fr", + "lastOnlineAt": "2023-02-06T23:03:22.645Z", + } + }, + ) + + res = self.api.get("subscriber-id") + self.assertIsInstance(res, SubscriberDto) + self.assertEqual( + res, + SubscriberDto( + subscriber_id="63dafed4117f8c850991ec4a", + email="oscar.marie-taillefer@spikeelabs.fr", + first_name=None, + last_name=None, + phone=None, + avatar=None, + locale=None, + _id="63dafedbc037e013fd82d37a", + _environment_id="63dafed97779f59258e38445", + _organization_id="63dafed97779f59258e3843f", + channels=[ + SubscriberChannelSettingsDto( + provider_id=ChatProviderIdEnum.SLACK, + _integration_id="64f6d74be166fcd7f2751111", + credentials=SubscriberChannelSettingsCredentialsDto( + webhook_url="TEST", channel="slack", device_tokens=["TEST"] + ), + integration_identifier=None, + ) + ], + deleted=False, + created_at="2023-02-02T00:07:55.459Z", + updated_at="2023-02-06T23:03:22.645Z", + is_online=False, + last_online_at="2023-02-06T23:03:22.645Z", + ), + ) + + mock_request.assert_called_once_with( + method="GET", + url="sample.novu.com/v1/subscribers/subscriber-id", + headers={"Authorization": "ApiKey api-key"}, + json=None, + params=None, + timeout=5, + ) + @mock.patch("requests.request") def test_update_subscriber(self, mock_request: mock.MagicMock) -> None: mock_request.return_value = MockResponse(200, self.response_get) @@ -176,6 +251,7 @@ def test_update_subscriber(self, mock_request: mock.MagicMock) -> None: "phone": "+33612345678", "avatar": None, "locale": None, + "channels": None, }, params=None, timeout=5,