diff --git a/mpt_api_client/resources/notifications/notifications.py b/mpt_api_client/resources/notifications/notifications.py index 3dcacb22..34c7b78f 100644 --- a/mpt_api_client/resources/notifications/notifications.py +++ b/mpt_api_client/resources/notifications/notifications.py @@ -16,6 +16,10 @@ AsyncSubscribersService, SubscribersService, ) +from mpt_api_client.resources.notifications.templates import ( + AsyncTemplatesService, + TemplatesService, +) from mpt_api_client.resources.notifications.webhooks import ( AsyncWebhooksService, WebhooksService, @@ -80,6 +84,11 @@ def subscribers(self) -> SubscribersService: """Subscriptions service.""" return SubscribersService(http_client=self.http_client) + @property + def templates(self) -> TemplatesService: + """Templates service.""" + return TemplatesService(http_client=self.http_client) + @property def webhooks(self) -> WebhooksService: """Webhooks service.""" @@ -144,6 +153,11 @@ def subscribers(self) -> AsyncSubscribersService: """Subscriptions service.""" return AsyncSubscribersService(http_client=self.http_client) + @property + def templates(self) -> AsyncTemplatesService: + """Async Templates service.""" + return AsyncTemplatesService(http_client=self.http_client) + @property def webhooks(self) -> AsyncWebhooksService: """Async Webhooks service.""" diff --git a/mpt_api_client/resources/notifications/template_variants.py b/mpt_api_client/resources/notifications/template_variants.py new file mode 100644 index 00000000..753d1489 --- /dev/null +++ b/mpt_api_client/resources/notifications/template_variants.py @@ -0,0 +1,73 @@ +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.http.mixins import ( + AsyncCollectionMixin, + AsyncDisableMixin, + AsyncManagedResourceMixin, + CollectionMixin, + DisableMixin, + ManagedResourceMixin, +) +from mpt_api_client.models import Model, ResourceData +from mpt_api_client.models.model import BaseModel + + +class TemplateVariant(Model): + """Notifications Template Variant resource. + + Attributes: + body: Body content of the variant. + default: Whether this variant is the default one of its template. + language_code: Language code of the variant. + template: Reference to the parent template. + status: Variant status. + subject: Subject of the messages created from this variant. + audit: Audit information (created, updated events). + """ + + body: str | None + default: bool | None + language_code: str | None + template: BaseModel | None + status: str | None + subject: str | None + audit: BaseModel | None + + +class TemplateVariantsServiceConfig: + """Notifications Template Variants service configuration.""" + + _endpoint = "/public/v1/notifications/templates/{template_id}/variants" + _model_class = TemplateVariant + _collection_key = "data" + + +class TemplateVariantsService( + DisableMixin[TemplateVariant], + ManagedResourceMixin[TemplateVariant], + CollectionMixin[TemplateVariant], + Service[TemplateVariant], + TemplateVariantsServiceConfig, +): + """Notifications Template Variants service.""" + + def activate( + self, resource_id: str, resource_data: ResourceData | None = None + ) -> TemplateVariant: + """Switch template variant to active state.""" + return self._resource(resource_id).post("activate", json=resource_data) + + +class AsyncTemplateVariantsService( + AsyncDisableMixin[TemplateVariant], + AsyncManagedResourceMixin[TemplateVariant], + AsyncCollectionMixin[TemplateVariant], + AsyncService[TemplateVariant], + TemplateVariantsServiceConfig, +): + """Async Notifications Template Variants service.""" + + async def activate( + self, resource_id: str, resource_data: ResourceData | None = None + ) -> TemplateVariant: + """Switch template variant to active state.""" + return await self._resource(resource_id).post("activate", json=resource_data) diff --git a/mpt_api_client/resources/notifications/templates.py b/mpt_api_client/resources/notifications/templates.py new file mode 100644 index 00000000..9315faa7 --- /dev/null +++ b/mpt_api_client/resources/notifications/templates.py @@ -0,0 +1,117 @@ +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.http.mixins import ( + AsyncCollectionMixin, + AsyncDisableMixin, + AsyncManagedResourceMixin, + CollectionMixin, + DisableMixin, + ManagedResourceMixin, +) +from mpt_api_client.models import Model, ResourceData +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.notifications.template_variants import ( + AsyncTemplateVariantsService, + TemplateVariantsService, +) + + +class Template(Model): + """Notifications Template resource. + + Attributes: + name: Template name. + category: Reference to the notification category of the template. + criteria: Criteria triggering the template automatically. + default_variant: Reference to the default variant of the template. + description: Template description. + last_used: Timestamp of the last time the template was used. + owner: Reference to the account owning the template. + schedule: Reference to the schedule triggering the template. + statistics: Usage statistics of the template. + status: Template status. + type: How the template is triggered (Event, Scheduled or Manual). + variants: References to the language-specific variants of the template. + external_id: External identifier of the template. + audit: Audit information (created, updated events). + """ + + name: str | None + category: BaseModel | None + criteria: BaseModel | None + default_variant: BaseModel | None + description: str | None + last_used: str | None + owner: BaseModel | None + schedule: BaseModel | None + statistics: BaseModel | None + status: str | None + type: str | None + variants: list[BaseModel] | None + external_id: str | None + audit: BaseModel | None + + +class TemplatesServiceConfig: + """Notifications Templates service configuration.""" + + _endpoint = "/public/v1/notifications/templates" + _model_class = Template + _collection_key = "data" + + +class TemplatesService( + DisableMixin[Template], + ManagedResourceMixin[Template], + CollectionMixin[Template], + Service[Template], + TemplatesServiceConfig, +): + """Notifications Templates service.""" + + def activate(self, resource_id: str, resource_data: ResourceData | None = None) -> Template: + """Switch template to active state.""" + return self._resource(resource_id).post("activate", json=resource_data) + + def variants(self, template_id: str) -> TemplateVariantsService: + """Access template variants service. + + Args: + template_id: Template ID. + + Returns: + TemplateVariantsService + """ + return TemplateVariantsService( + http_client=self.http_client, + endpoint_params={"template_id": template_id}, + ) + + +class AsyncTemplatesService( + AsyncDisableMixin[Template], + AsyncManagedResourceMixin[Template], + AsyncCollectionMixin[Template], + AsyncService[Template], + TemplatesServiceConfig, +): + """Async Notifications Templates service.""" + + async def activate( + self, resource_id: str, resource_data: ResourceData | None = None + ) -> Template: + """Switch template to active state.""" + return await self._resource(resource_id).post("activate", json=resource_data) + + def variants(self, template_id: str) -> AsyncTemplateVariantsService: + """Access async template variants service. + + Args: + template_id: Template ID. + + Returns: + AsyncTemplateVariantsService + """ + return AsyncTemplateVariantsService( + http_client=self.http_client, + endpoint_params={"template_id": template_id}, + ) diff --git a/tests/e2e/notifications/templates/__init__.py b/tests/e2e/notifications/templates/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/e2e/notifications/templates/conftest.py b/tests/e2e/notifications/templates/conftest.py new file mode 100644 index 00000000..9965bf09 --- /dev/null +++ b/tests/e2e/notifications/templates/conftest.py @@ -0,0 +1,92 @@ +import pytest + +from tests.e2e.helper import ( + async_create_fixture_resource_and_delete, + create_fixture_resource_and_delete, +) + +# A template can only be activated once it has a default variant, and the first variant +# activated on a template becomes its default. Deleting a template soft-deletes its variants, +# so the variant fixtures only create and leave the cleanup to the template teardown. + + +@pytest.fixture +def templates_service(mpt_ops): + return mpt_ops.notifications.templates + + +@pytest.fixture +def async_templates_service(async_mpt_ops): + return async_mpt_ops.notifications.templates + + +@pytest.fixture +def template_data(short_uuid, category_id): + return { + "name": f"e2e template {short_uuid}", + "description": f"e2e template - please delete {short_uuid}", + "type": "Manual", + "category": {"id": category_id}, + } + + +@pytest.fixture +def template_variant_data(short_uuid): + return { + "languageCode": "en-US", + "subject": f"e2e template variant {short_uuid}", + "body": f"e2e template variant body {short_uuid}", + } + + +@pytest.fixture +def created_template(templates_service, template_data): + with create_fixture_resource_and_delete(templates_service, template_data) as template: + yield template + + +@pytest.fixture +async def async_created_template(async_templates_service, template_data): + async with async_create_fixture_resource_and_delete( + async_templates_service, template_data + ) as template: + yield template + + +@pytest.fixture +def template_variants_service(templates_service, created_template): + return templates_service.variants(created_template.id) + + +@pytest.fixture +def async_template_variants_service(async_templates_service, async_created_template): + return async_templates_service.variants(async_created_template.id) + + +@pytest.fixture +def created_template_variant(template_variants_service, template_variant_data): + return template_variants_service.create(template_variant_data) + + +@pytest.fixture +async def async_created_template_variant(async_template_variants_service, template_variant_data): + return await async_template_variants_service.create(template_variant_data) + + +@pytest.fixture +def active_template( + templates_service, template_variants_service, created_template, created_template_variant +): + template_variants_service.activate(created_template_variant.id) + return templates_service.activate(created_template.id) + + +@pytest.fixture +async def async_active_template( + async_templates_service, + async_template_variants_service, + async_created_template, + async_created_template_variant, +): + await async_template_variants_service.activate(async_created_template_variant.id) + return await async_templates_service.activate(async_created_template.id) diff --git a/tests/e2e/notifications/templates/test_async_templates.py b/tests/e2e/notifications/templates/test_async_templates.py new file mode 100644 index 00000000..23d90cd8 --- /dev/null +++ b/tests/e2e/notifications/templates/test_async_templates.py @@ -0,0 +1,84 @@ +from http import HTTPStatus + +import pytest + +from mpt_api_client.exceptions import MPTAPIError +from tests.e2e.helper import ( + assert_async_service_filter_with_iterate, + assert_async_update_resource, +) + +pytestmark = [pytest.mark.flaky] + + +def test_create_template(async_created_template, template_data): + result = async_created_template.name + + assert result == template_data["name"] + + +async def test_get_template(async_templates_service, async_created_template): + result = await async_templates_service.get(async_created_template.id) + + assert result.id == async_created_template.id + + +async def test_get_template_not_found(async_templates_service): + bogus_id = "NTM-0000-0000" + + with pytest.raises(MPTAPIError) as error: + await async_templates_service.get(bogus_id) + + assert error.value.status_code == HTTPStatus.NOT_FOUND + + +async def test_update_template(async_templates_service, async_created_template, short_uuid): + new_description = f"e2e template updated {short_uuid}" + + await assert_async_update_resource( # act + async_templates_service, async_created_template.id, "description", new_description + ) + + +async def test_activate_template( + async_templates_service, + async_template_variants_service, + async_created_template, + async_created_template_variant, +): + await async_template_variants_service.activate(async_created_template_variant.id) + + result = await async_templates_service.activate(async_created_template.id) + + assert result.status == "Active" + assert result.default_variant.id == async_created_template_variant.id + + +async def test_disable_template(async_templates_service, async_active_template): + result = await async_templates_service.disable(async_active_template.id) + + assert result.status == "Disabled" + + +async def test_delete_template(async_templates_service, async_created_template): + await async_templates_service.delete(async_created_template.id) + + result = await async_templates_service.get(async_created_template.id) + + assert result.status == "Deleted" + + +async def test_delete_template_not_found(async_templates_service): + # The platform answers a delete on an unknown id with 400, not 404. + bogus_id = "NTM-0000-0000" + + with pytest.raises(MPTAPIError) as error: + await async_templates_service.delete(bogus_id) + + assert error.value.status_code == HTTPStatus.BAD_REQUEST + + +async def test_filter_templates(async_templates_service, async_created_template): + await assert_async_service_filter_with_iterate( # act + async_templates_service, async_created_template.id, None + ) diff --git a/tests/e2e/notifications/templates/test_sync_templates.py b/tests/e2e/notifications/templates/test_sync_templates.py new file mode 100644 index 00000000..78ea7b68 --- /dev/null +++ b/tests/e2e/notifications/templates/test_sync_templates.py @@ -0,0 +1,76 @@ +from http import HTTPStatus + +import pytest + +from mpt_api_client.exceptions import MPTAPIError +from tests.e2e.helper import assert_service_filter_with_iterate, assert_update_resource + +pytestmark = [pytest.mark.flaky] + + +def test_create_template(created_template, template_data): + result = created_template.name + + assert result == template_data["name"] + + +def test_get_template(templates_service, created_template): + result = templates_service.get(created_template.id) + + assert result.id == created_template.id + + +def test_get_template_not_found(templates_service): + bogus_id = "NTM-0000-0000" + + with pytest.raises(MPTAPIError) as error: + templates_service.get(bogus_id) + + assert error.value.status_code == HTTPStatus.NOT_FOUND + + +def test_update_template(templates_service, created_template, short_uuid): + new_description = f"e2e template updated {short_uuid}" + + assert_update_resource( # act + templates_service, created_template.id, "description", new_description + ) + + +def test_activate_template( + templates_service, template_variants_service, created_template, created_template_variant +): + template_variants_service.activate(created_template_variant.id) + + result = templates_service.activate(created_template.id) + + assert result.status == "Active" + assert result.default_variant.id == created_template_variant.id + + +def test_disable_template(templates_service, active_template): + result = templates_service.disable(active_template.id) + + assert result.status == "Disabled" + + +def test_delete_template(templates_service, created_template): + templates_service.delete(created_template.id) + + result = templates_service.get(created_template.id) + + assert result.status == "Deleted" + + +def test_delete_template_not_found(templates_service): + # The platform answers a delete on an unknown id with 400, not 404. + bogus_id = "NTM-0000-0000" + + with pytest.raises(MPTAPIError) as error: + templates_service.delete(bogus_id) + + assert error.value.status_code == HTTPStatus.BAD_REQUEST + + +def test_filter_templates(templates_service, created_template): + assert_service_filter_with_iterate(templates_service, created_template.id, None) # act diff --git a/tests/e2e/notifications/templates/variants/__init__.py b/tests/e2e/notifications/templates/variants/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/e2e/notifications/templates/variants/conftest.py b/tests/e2e/notifications/templates/variants/conftest.py new file mode 100644 index 00000000..2d004568 --- /dev/null +++ b/tests/e2e/notifications/templates/variants/conftest.py @@ -0,0 +1,10 @@ +import pytest + + +@pytest.fixture +def secondary_template_variant_data(short_uuid): + return { + "languageCode": "de-DE", + "subject": f"e2e secondary template variant {short_uuid}", + "body": f"e2e secondary template variant body {short_uuid}", + } diff --git a/tests/e2e/notifications/templates/variants/test_async_template_variants.py b/tests/e2e/notifications/templates/variants/test_async_template_variants.py new file mode 100644 index 00000000..dab9eed8 --- /dev/null +++ b/tests/e2e/notifications/templates/variants/test_async_template_variants.py @@ -0,0 +1,98 @@ +from http import HTTPStatus + +import pytest + +from mpt_api_client.exceptions import MPTAPIError +from tests.e2e.helper import ( + assert_async_service_filter_with_iterate, + assert_async_update_resource, +) + +pytestmark = [pytest.mark.flaky] + + +def test_create_template_variant(async_created_template_variant, template_variant_data): + result = async_created_template_variant.subject + + assert result == template_variant_data["subject"] + + +async def test_get_template_variant( + async_template_variants_service, async_created_template_variant +): + result = await async_template_variants_service.get(async_created_template_variant.id) + + assert result.id == async_created_template_variant.id + + +async def test_get_template_variant_not_found(async_template_variants_service): + bogus_id = "NTL-0000-0000-0000" + + with pytest.raises(MPTAPIError) as error: + await async_template_variants_service.get(bogus_id) + + assert error.value.status_code == HTTPStatus.NOT_FOUND + + +async def test_update_template_variant( + async_template_variants_service, async_created_template_variant, short_uuid +): + new_subject = f"e2e template variant updated {short_uuid}" + + await assert_async_update_resource( # act + async_template_variants_service, async_created_template_variant.id, "subject", new_subject + ) + + +async def test_activate_template_variant( + async_template_variants_service, async_created_template_variant +): + result = await async_template_variants_service.activate(async_created_template_variant.id) + + assert result.status == "Active" + assert result.default + + +async def test_disable_template_variant( + async_template_variants_service, + async_created_template_variant, + secondary_template_variant_data, +): + # The default variant cannot be disabled, so the test disables a second active variant. + await async_template_variants_service.activate(async_created_template_variant.id) + secondary_variant = await async_template_variants_service.create( + secondary_template_variant_data + ) + await async_template_variants_service.activate(secondary_variant.id) + + result = await async_template_variants_service.disable(secondary_variant.id) + + assert result.status == "Disabled" + + +async def test_delete_template_variant( + async_template_variants_service, async_created_template_variant +): + await async_template_variants_service.delete(async_created_template_variant.id) + + result = await async_template_variants_service.get(async_created_template_variant.id) + + assert result.status == "Deleted" + + +async def test_delete_template_variant_not_found(async_template_variants_service): + # The platform answers a delete on an unknown id with 400, not 404. + bogus_id = "NTL-0000-0000-0000" + + with pytest.raises(MPTAPIError) as error: + await async_template_variants_service.delete(bogus_id) + + assert error.value.status_code == HTTPStatus.BAD_REQUEST + + +async def test_filter_template_variants( + async_template_variants_service, async_created_template_variant +): + await assert_async_service_filter_with_iterate( # act + async_template_variants_service, async_created_template_variant.id, None + ) diff --git a/tests/e2e/notifications/templates/variants/test_sync_template_variants.py b/tests/e2e/notifications/templates/variants/test_sync_template_variants.py new file mode 100644 index 00000000..8a6eb9b8 --- /dev/null +++ b/tests/e2e/notifications/templates/variants/test_sync_template_variants.py @@ -0,0 +1,81 @@ +from http import HTTPStatus + +import pytest + +from mpt_api_client.exceptions import MPTAPIError +from tests.e2e.helper import assert_service_filter_with_iterate, assert_update_resource + +pytestmark = [pytest.mark.flaky] + + +def test_create_template_variant(created_template_variant, template_variant_data): + result = created_template_variant.subject + + assert result == template_variant_data["subject"] + + +def test_get_template_variant(template_variants_service, created_template_variant): + result = template_variants_service.get(created_template_variant.id) + + assert result.id == created_template_variant.id + + +def test_get_template_variant_not_found(template_variants_service): + bogus_id = "NTL-0000-0000-0000" + + with pytest.raises(MPTAPIError) as error: + template_variants_service.get(bogus_id) + + assert error.value.status_code == HTTPStatus.NOT_FOUND + + +def test_update_template_variant(template_variants_service, created_template_variant, short_uuid): + new_subject = f"e2e template variant updated {short_uuid}" + + assert_update_resource( # act + template_variants_service, created_template_variant.id, "subject", new_subject + ) + + +def test_activate_template_variant(template_variants_service, created_template_variant): + result = template_variants_service.activate(created_template_variant.id) + + assert result.status == "Active" + assert result.default + + +def test_disable_template_variant( + template_variants_service, created_template_variant, secondary_template_variant_data +): + # The default variant cannot be disabled, so the test disables a second active variant. + template_variants_service.activate(created_template_variant.id) + secondary_variant = template_variants_service.create(secondary_template_variant_data) + template_variants_service.activate(secondary_variant.id) + + result = template_variants_service.disable(secondary_variant.id) + + assert result.status == "Disabled" + + +def test_delete_template_variant(template_variants_service, created_template_variant): + template_variants_service.delete(created_template_variant.id) + + result = template_variants_service.get(created_template_variant.id) + + assert result.status == "Deleted" + + +def test_delete_template_variant_not_found(template_variants_service): + # The platform answers a delete on an unknown id with 400, not 404. + bogus_id = "NTL-0000-0000-0000" + + with pytest.raises(MPTAPIError) as error: + template_variants_service.delete(bogus_id) + + assert error.value.status_code == HTTPStatus.BAD_REQUEST + + +def test_filter_template_variants(template_variants_service, created_template_variant): + assert_service_filter_with_iterate( # act + template_variants_service, created_template_variant.id, None + ) diff --git a/tests/unit/resources/notifications/test_notifications.py b/tests/unit/resources/notifications/test_notifications.py index 994eb8f6..921d1f36 100644 --- a/tests/unit/resources/notifications/test_notifications.py +++ b/tests/unit/resources/notifications/test_notifications.py @@ -18,6 +18,10 @@ AsyncSubscribersService, SubscribersService, ) +from mpt_api_client.resources.notifications.templates import ( + AsyncTemplatesService, + TemplatesService, +) from mpt_api_client.resources.notifications.webhooks import ( AsyncWebhooksService, WebhooksService, @@ -48,6 +52,7 @@ def test_async_notifications_init(async_http_client): ("subscribers", SubscribersService), ("directories", DirectoriesService), ("footers", FootersService), + ("templates", TemplatesService), ("webhooks", WebhooksService), ], ) @@ -69,6 +74,7 @@ def test_notifications_properties(http_client, attr_name, expected): ("subscribers", AsyncSubscribersService), ("directories", AsyncDirectoriesService), ("footers", AsyncFootersService), + ("templates", AsyncTemplatesService), ("webhooks", AsyncWebhooksService), ], ) diff --git a/tests/unit/resources/notifications/test_template_variants.py b/tests/unit/resources/notifications/test_template_variants.py new file mode 100644 index 00000000..62756d2d --- /dev/null +++ b/tests/unit/resources/notifications/test_template_variants.py @@ -0,0 +1,125 @@ +import httpx +import pytest +import respx + +from mpt_api_client.resources.notifications.template_variants import ( + AsyncTemplateVariantsService, + TemplateVariantsService, +) + +TEMPLATE_ID = "NTL-1234" +VARIANTS_PATH = f"/public/v1/notifications/templates/{TEMPLATE_ID}/variants" + + +@pytest.fixture +def template_variants_service(http_client): + return TemplateVariantsService( + http_client=http_client, endpoint_params={"template_id": TEMPLATE_ID} + ) + + +@pytest.fixture +def async_template_variants_service(async_http_client): + return AsyncTemplateVariantsService( + http_client=async_http_client, endpoint_params={"template_id": TEMPLATE_ID} + ) + + +@pytest.fixture +def template_variant_data(): + return { + "id": "NTV-1234", + "body": "Hello", + "default": True, + "languageCode": "en-US", + "subject": "Order confirmed", + "status": "Active", + "template": {"id": TEMPLATE_ID, "name": "Order confirmation"}, + "audit": {"created": {"at": "2024-01-01T00:00:00Z"}}, + } + + +def test_endpoint_contains_template_id(template_variants_service): + result = template_variants_service.build_path() + + assert result == VARIANTS_PATH + + +def test_async_endpoint_contains_template_id(async_template_variants_service): + result = async_template_variants_service.build_path() + + assert result == VARIANTS_PATH + + +@pytest.mark.parametrize( + "method", + ["get", "create", "update", "delete", "activate", "disable", "iterate", "fetch_page"], +) +def test_mixins_present(template_variants_service, method): + result = hasattr(template_variants_service, method) + + assert result is True + + +@pytest.mark.parametrize( + "method", + ["get", "create", "update", "delete", "activate", "disable", "iterate", "fetch_page"], +) +def test_async_mixins_present(async_template_variants_service, method): + result = hasattr(async_template_variants_service, method) + + assert result is True + + +def test_get_template_variant(template_variants_service, template_variant_data): + with respx.mock: + mock_route = respx.get(f"https://api.example.com{VARIANTS_PATH}/NTV-1234").mock( + return_value=httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + json=template_variant_data, + ) + ) + + result = template_variants_service.get("NTV-1234") + + assert mock_route.call_count == 1 + assert result.to_dict() == template_variant_data + assert result.language_code == "en-US" + assert result.template.id == TEMPLATE_ID + + +@pytest.mark.parametrize("action", ["activate", "disable"]) +def test_template_variant_state_actions(template_variants_service, action): + response_expected_data = {"id": "NTV-1234", "status": "Active"} + with respx.mock: + mock_route = respx.post(f"https://api.example.com{VARIANTS_PATH}/NTV-1234/{action}").mock( + return_value=httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = getattr(template_variants_service, action)("NTV-1234") + + assert mock_route.call_count == 1 + assert result.to_dict() == response_expected_data + + +@pytest.mark.parametrize("action", ["activate", "disable"]) +async def test_async_template_variant_state_actions(async_template_variants_service, action): + response_expected_data = {"id": "NTV-1234", "status": "Active"} + with respx.mock: + mock_route = respx.post(f"https://api.example.com{VARIANTS_PATH}/NTV-1234/{action}").mock( + return_value=httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = await getattr(async_template_variants_service, action)("NTV-1234") + + assert mock_route.call_count == 1 + assert result.to_dict() == response_expected_data diff --git a/tests/unit/resources/notifications/test_templates.py b/tests/unit/resources/notifications/test_templates.py new file mode 100644 index 00000000..eb23b476 --- /dev/null +++ b/tests/unit/resources/notifications/test_templates.py @@ -0,0 +1,150 @@ +import httpx +import pytest +import respx + +from mpt_api_client.resources.notifications.template_variants import ( + AsyncTemplateVariantsService, + TemplateVariantsService, +) +from mpt_api_client.resources.notifications.templates import ( + AsyncTemplatesService, + TemplatesService, +) + + +@pytest.fixture +def templates_service(http_client): + return TemplatesService(http_client=http_client) + + +@pytest.fixture +def async_templates_service(async_http_client): + return AsyncTemplatesService(http_client=async_http_client) + + +@pytest.fixture +def template_data(): + return { + "id": "NTL-1234", + "name": "Order confirmation", + "description": "Sent when an order is completed", + "status": "Active", + "type": "Event", + "externalId": "order-confirmation", + "lastUsed": "2024-01-01T00:00:00Z", + "category": {"id": "NCA-1234", "name": "Orders"}, + "defaultVariant": {"id": "NTV-1234", "languageCode": "en-US"}, + "owner": {"id": "ACC-1234", "name": "Account"}, + "variants": [{"id": "NTV-1234", "languageCode": "en-US"}], + "audit": {"created": {"at": "2024-01-01T00:00:00Z"}}, + } + + +def test_endpoint(templates_service): + result = templates_service.build_path() + + assert result == "/public/v1/notifications/templates" + + +def test_async_endpoint(async_templates_service): + result = async_templates_service.build_path() + + assert result == "/public/v1/notifications/templates" + + +@pytest.mark.parametrize( + "method", + ["get", "create", "update", "delete", "activate", "disable", "iterate", "fetch_page"], +) +def test_mixins_present(templates_service, method): + result = hasattr(templates_service, method) + + assert result is True + + +@pytest.mark.parametrize( + "method", + ["get", "create", "update", "delete", "activate", "disable", "iterate", "fetch_page"], +) +def test_async_mixins_present(async_templates_service, method): + result = hasattr(async_templates_service, method) + + assert result is True + + +def test_get_template(templates_service, template_data): + with respx.mock: + mock_route = respx.get( + "https://api.example.com/public/v1/notifications/templates/NTL-1234" + ).mock( + return_value=httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + json=template_data, + ) + ) + + result = templates_service.get("NTL-1234") + + assert mock_route.call_count == 1 + assert result.to_dict() == template_data + assert result.external_id == "order-confirmation" + assert result.default_variant.id == "NTV-1234" + + +@pytest.mark.parametrize("action", ["activate", "disable"]) +def test_template_state_actions(templates_service, action): + response_expected_data = {"id": "NTL-1234", "status": "Active"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/notifications/templates/NTL-1234/{action}" + ).mock( + return_value=httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = getattr(templates_service, action)("NTL-1234") + + assert mock_route.call_count == 1 + assert result.to_dict() == response_expected_data + + +@pytest.mark.parametrize("action", ["activate", "disable"]) +async def test_async_template_state_actions(async_templates_service, action): + response_expected_data = {"id": "NTL-1234", "status": "Active"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/notifications/templates/NTL-1234/{action}" + ).mock( + return_value=httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = await getattr(async_templates_service, action)("NTL-1234") + + assert mock_route.call_count == 1 + assert result.to_dict() == response_expected_data + + +def test_variants_accessor(templates_service): + result = templates_service.variants("NTL-1234") + + assert isinstance(result, TemplateVariantsService) + assert result.http_client is templates_service.http_client + assert result.endpoint_params == {"template_id": "NTL-1234"} + assert result.build_path() == "/public/v1/notifications/templates/NTL-1234/variants" + + +def test_async_variants_accessor(async_templates_service): + result = async_templates_service.variants("NTL-1234") + + assert isinstance(result, AsyncTemplateVariantsService) + assert result.http_client is async_templates_service.http_client + assert result.endpoint_params == {"template_id": "NTL-1234"} + assert result.build_path() == "/public/v1/notifications/templates/NTL-1234/variants"