diff --git a/CHANGELOG.md b/CHANGELOG.md index 46b53e8936..52080aa512 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,6 @@ The types of changes are: - Updated System's page to display a table that uses a paginated endpoint [#5084](https://github.com/ethyca/fides/pull/5084) - Messaging page now shows a notice if you have properties without any templates [#5077](https://github.com/ethyca/fides/pull/5077) - Endpoints for listing systems (GET /system) and datasets (GET /dataset) now support optional pagination [#5071](https://github.com/ethyca/fides/pull/5071) -- Moves some endpoints for property-specific messaging from OSS -> plus [#5069](https://github.com/ethyca/fides/pull/5069) - Messaging page will now show a notice about using global mode [#5090](https://github.com/ethyca/fides/pull/5090) - URL for deployment instructions when the webserver is running [#5088](https://github.com/ethyca/fides/pull/5088) diff --git a/clients/admin-ui/cypress/e2e/messaging.cy.ts b/clients/admin-ui/cypress/e2e/messaging.cy.ts index 0eeb7da3c6..a873029652 100644 --- a/clients/admin-ui/cypress/e2e/messaging.cy.ts +++ b/clients/admin-ui/cypress/e2e/messaging.cy.ts @@ -8,7 +8,7 @@ describe("Messaging", () => { cy.login(); stubPlus(true); - cy.intercept("/api/v1/plus/messaging/templates/summary?*", { + cy.intercept("/api/v1/messaging/templates/summary?*", { fixture: "messaging/summary.json", }).as("getEmailTemplatesSummary"); @@ -20,13 +20,13 @@ describe("Messaging", () => { fixture: "properties/properties.json", }).as("getProperties"); - cy.intercept("PATCH", "/api/v1/plus/messaging/templates/*", {}).as( + cy.intercept("PATCH", "/api/v1/messaging/templates/*", {}).as( "patchTemplate" ); cy.intercept( "POST", - "/api/v1/plus/messaging/templates/privacy_request_complete_access", + "/api/v1/messaging/templates/privacy_request_complete_access", {} ).as("postTemplate"); }); diff --git a/clients/admin-ui/src/features/messaging-templates/messaging-templates.slice.plus.ts b/clients/admin-ui/src/features/messaging-templates/messaging-templates.slice.plus.ts deleted file mode 100644 index 3d05cba9a6..0000000000 --- a/clients/admin-ui/src/features/messaging-templates/messaging-templates.slice.plus.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { baseApi } from "common/api.slice"; - -import { - MessagingTemplateCreate, - MessagingTemplatePatch, - MessagingTemplateResponse, - MessagingTemplateUpdate, -} from "~/features/messaging-templates/messaging-templates.slice"; -import { Page_MessagingTemplateWithPropertiesSummary_ } from "~/types/api"; - -const messagingTemplatesPlusApi = baseApi.injectEndpoints({ - endpoints: (build) => ({ - getSummaryMessagingTemplates: build.query< - Page_MessagingTemplateWithPropertiesSummary_, - any - >({ - query: (params) => ({ - method: "GET", - url: `plus/messaging/templates/summary`, - params, - }), - providesTags: () => ["Property-Specific Messaging Templates"], - }), - // Full update existing template - putMessagingTemplateById: build.mutation< - MessagingTemplateResponse, - MessagingTemplateUpdate - >({ - query: ({ templateId, template }) => ({ - url: `plus/messaging/templates/${templateId}`, - method: "PUT", - body: template, - }), - invalidatesTags: () => ["Property-Specific Messaging Templates"], - }), - // Partial update existing template, e.g. enable it - patchMessagingTemplateById: build.mutation< - MessagingTemplateResponse, - MessagingTemplatePatch - >({ - query: ({ templateId, template }) => ({ - url: `plus/messaging/templates/${templateId}`, - method: "PATCH", - body: template, - }), - invalidatesTags: () => ["Property-Specific Messaging Templates"], - }), - // endpoint for creating new messaging template- POST by type - createMessagingTemplateByType: build.mutation< - MessagingTemplateResponse, - MessagingTemplateCreate - >({ - query: ({ templateType, template }) => ({ - url: `plus/messaging/templates/${templateType}`, - method: "POST", - body: template, - }), - invalidatesTags: () => ["Property-Specific Messaging Templates"], - }), - }), -}); - -export const { - useGetSummaryMessagingTemplatesQuery, - usePutMessagingTemplateByIdMutation, - useCreateMessagingTemplateByTypeMutation, - usePatchMessagingTemplateByIdMutation, -} = messagingTemplatesPlusApi; diff --git a/clients/admin-ui/src/features/messaging-templates/messaging-templates.slice.ts b/clients/admin-ui/src/features/messaging-templates/messaging-templates.slice.ts index 722159ebe1..f437cebb31 100644 --- a/clients/admin-ui/src/features/messaging-templates/messaging-templates.slice.ts +++ b/clients/admin-ui/src/features/messaging-templates/messaging-templates.slice.ts @@ -1,5 +1,8 @@ import { baseApi } from "~/features/common/api.slice"; -import { MinimalProperty } from "~/types/api"; +import { + MinimalProperty, + Page_MessagingTemplateWithPropertiesSummary_, +} from "~/types/api"; import { BulkUpdateFailed } from "~/types/api/models/BulkUpdateFailed"; export type MessagingTemplate = { @@ -67,6 +70,17 @@ const messagingTemplatesApi = baseApi.injectEndpoints({ query: () => ({ url: `messaging/templates` }), providesTags: () => ["Messaging Templates"], }), + getSummaryMessagingTemplates: build.query< + Page_MessagingTemplateWithPropertiesSummary_, + any + >({ + query: (params) => ({ + method: "GET", + url: `messaging/templates/summary`, + params, + }), + providesTags: () => ["Property-Specific Messaging Templates"], + }), updateMessagingTemplates: build.mutation< BulkPutMessagingTemplateResponse, MessagingTemplate[] @@ -76,7 +90,7 @@ const messagingTemplatesApi = baseApi.injectEndpoints({ method: "PUT", body: templates, }), - invalidatesTags: () => ["Messaging Templates"], + invalidatesTags: () => ["Property-Specific Messaging Templates"], }), // Render data from existing template- GET by id getMessagingTemplateById: build.query({ @@ -85,6 +99,30 @@ const messagingTemplatesApi = baseApi.injectEndpoints({ }), providesTags: () => ["Property-Specific Messaging Templates"], }), + // Update existing template + putMessagingTemplateById: build.mutation< + MessagingTemplateResponse, + MessagingTemplateUpdate + >({ + query: ({ templateId, template }) => ({ + url: `/messaging/templates/${templateId}`, + method: "PUT", + body: template, + }), + invalidatesTags: () => ["Property-Specific Messaging Templates"], + }), + // Update existing template + patchMessagingTemplateById: build.mutation< + MessagingTemplateResponse, + MessagingTemplatePatch + >({ + query: ({ templateId, template }) => ({ + url: `/messaging/templates/${templateId}`, + method: "PATCH", + body: template, + }), + invalidatesTags: () => ["Property-Specific Messaging Templates"], + }), // endpoint for rendering data for default template- GET by type getMessagingTemplateDefault: build.query< MessagingTemplateDefaultResponse, @@ -94,6 +132,18 @@ const messagingTemplatesApi = baseApi.injectEndpoints({ url: `/messaging/templates/default/${templateType}`, }), }), + // endpoint for creating new messaging template- POST by type + createMessagingTemplateByType: build.mutation< + MessagingTemplateResponse, + MessagingTemplateCreate + >({ + query: ({ templateType, template }) => ({ + url: `/messaging/templates/${templateType}`, + method: "POST", + body: template, + }), + invalidatesTags: () => ["Property-Specific Messaging Templates"], + }), // delete template by id deleteMessagingTemplateById: build.mutation({ query: (templateId: string) => ({ @@ -108,7 +158,11 @@ const messagingTemplatesApi = baseApi.injectEndpoints({ export const { useGetMessagingTemplatesQuery, useUpdateMessagingTemplatesMutation, + useGetSummaryMessagingTemplatesQuery, useGetMessagingTemplateByIdQuery, + usePutMessagingTemplateByIdMutation, useGetMessagingTemplateDefaultQuery, + useCreateMessagingTemplateByTypeMutation, useDeleteMessagingTemplateByIdMutation, + usePatchMessagingTemplateByIdMutation, } = messagingTemplatesApi; diff --git a/clients/admin-ui/src/features/messaging-templates/useMessagingTemplateToggle.tsx b/clients/admin-ui/src/features/messaging-templates/useMessagingTemplateToggle.tsx index 1ce90998b3..9424d59afe 100644 --- a/clients/admin-ui/src/features/messaging-templates/useMessagingTemplateToggle.tsx +++ b/clients/admin-ui/src/features/messaging-templates/useMessagingTemplateToggle.tsx @@ -3,7 +3,7 @@ import { useToast } from "fidesui"; import { useCallback } from "react"; import { errorToastParams, successToastParams } from "~/features/common/toast"; -import { usePatchMessagingTemplateByIdMutation } from "~/features/messaging-templates/messaging-templates.slice.plus"; +import { usePatchMessagingTemplateByIdMutation } from "~/features/messaging-templates/messaging-templates.slice"; import { isErrorResult } from "~/types/errors"; const useMessagingTemplateToggle = () => { diff --git a/clients/admin-ui/src/pages/messaging/[id].tsx b/clients/admin-ui/src/pages/messaging/[id].tsx index 59eeea4d6b..7482189907 100644 --- a/clients/admin-ui/src/pages/messaging/[id].tsx +++ b/clients/admin-ui/src/pages/messaging/[id].tsx @@ -18,8 +18,8 @@ import { MessagingTemplateCreateOrUpdate, useDeleteMessagingTemplateByIdMutation, useGetMessagingTemplateByIdQuery, + usePutMessagingTemplateByIdMutation, } from "~/features/messaging-templates/messaging-templates.slice"; -import { usePutMessagingTemplateByIdMutation } from "~/features/messaging-templates/messaging-templates.slice.plus"; import PropertySpecificMessagingTemplateForm, { FormValues, } from "~/features/messaging-templates/PropertySpecificMessagingTemplateForm"; diff --git a/clients/admin-ui/src/pages/messaging/add-template.tsx b/clients/admin-ui/src/pages/messaging/add-template.tsx index c39b34d983..5e60c56ee4 100644 --- a/clients/admin-ui/src/pages/messaging/add-template.tsx +++ b/clients/admin-ui/src/pages/messaging/add-template.tsx @@ -8,9 +8,9 @@ import Layout from "~/features/common/Layout"; import { errorToastParams, successToastParams } from "~/features/common/toast"; import { MessagingTemplateCreateOrUpdate, + useCreateMessagingTemplateByTypeMutation, useGetMessagingTemplateDefaultQuery, } from "~/features/messaging-templates/messaging-templates.slice"; -import { useCreateMessagingTemplateByTypeMutation } from "~/features/messaging-templates/messaging-templates.slice.plus"; import PropertySpecificMessagingTemplateForm, { FormValues, } from "~/features/messaging-templates/PropertySpecificMessagingTemplateForm"; diff --git a/clients/admin-ui/src/pages/messaging/index.tsx b/clients/admin-ui/src/pages/messaging/index.tsx index 428976625a..ca984e9a2b 100644 --- a/clients/admin-ui/src/pages/messaging/index.tsx +++ b/clients/admin-ui/src/pages/messaging/index.tsx @@ -35,7 +35,7 @@ import { PaginationBar } from "~/features/common/table/v2/PaginationBar"; import AddMessagingTemplateModal from "~/features/messaging-templates/AddMessagingTemplateModal"; import { CustomizableMessagingTemplatesEnum } from "~/features/messaging-templates/CustomizableMessagingTemplatesEnum"; import CustomizableMessagingTemplatesLabelEnum from "~/features/messaging-templates/CustomizableMessagingTemplatesLabelEnum"; -import { useGetSummaryMessagingTemplatesQuery } from "~/features/messaging-templates/messaging-templates.slice.plus"; +import { useGetSummaryMessagingTemplatesQuery } from "~/features/messaging-templates/messaging-templates.slice"; import useMessagingTemplateToggle from "~/features/messaging-templates/useMessagingTemplateToggle"; import { useGetConfigurationSettingsQuery } from "~/features/privacy-requests"; import { useGetAllPropertiesQuery } from "~/features/properties"; diff --git a/src/fides/api/api/v1/endpoints/messaging_endpoints.py b/src/fides/api/api/v1/endpoints/messaging_endpoints.py index a31383d5e6..d721cd067e 100644 --- a/src/fides/api/api/v1/endpoints/messaging_endpoints.py +++ b/src/fides/api/api/v1/endpoints/messaging_endpoints.py @@ -31,7 +31,10 @@ get_messaging_method, get_schema_for_secrets, ) -from fides.api.models.messaging_template import DEFAULT_MESSAGING_TEMPLATES +from fides.api.models.messaging_template import ( + DEFAULT_MESSAGING_TEMPLATES, + MessagingTemplate, +) from fides.api.oauth.utils import verify_oauth_client from fides.api.schemas.api import BulkUpdateFailed from fides.api.schemas.messaging.messaging import ( @@ -47,7 +50,10 @@ MessagingMethod, MessagingServiceType, MessagingTemplateDefault, + MessagingTemplateWithPropertiesBodyParams, MessagingTemplateWithPropertiesDetail, + MessagingTemplateWithPropertiesPatchBodyParams, + MessagingTemplateWithPropertiesSummary, TestMessagingStatusMessage, UserEmailInviteStatus, ) @@ -59,13 +65,17 @@ from fides.api.service.messaging.messaging_crud_service import ( create_or_update_basic_templates, create_or_update_messaging_config, + create_property_specific_template_by_type, delete_messaging_config, delete_template_by_id, get_all_basic_messaging_templates, get_default_template_by_type, get_messaging_config_by_key, get_template_by_id, + patch_property_specific_template, + save_defaults_for_all_messaging_template_types, update_messaging_config, + update_property_specific_template, ) from fides.api.util.api_router import APIRouter from fides.api.util.logger import Pii @@ -88,6 +98,8 @@ MESSAGING_STATUS, MESSAGING_TEMPLATE_BY_ID, MESSAGING_TEMPLATE_DEFAULT_BY_TEMPLATE_TYPE, + MESSAGING_TEMPLATES_BY_TEMPLATE_TYPE, + MESSAGING_TEMPLATES_SUMMARY, MESSAGING_TEST, V1_URL_PREFIX, ) @@ -594,6 +606,29 @@ def update_basic_messaging_templates( return BulkPutBasicMessagingTemplateResponse(succeeded=succeeded, failed=failed) +@router.get( + MESSAGING_TEMPLATES_SUMMARY, + dependencies=[Security(verify_oauth_client, scopes=[MESSAGING_TEMPLATE_UPDATE])], + response_model=Page[MessagingTemplateWithPropertiesSummary], +) +def get_property_specific_messaging_templates_summary( + *, db: Session = Depends(deps.get_db), params: Params = Depends() +) -> AbstractPage[MessagingTemplate]: + """ + Returns all messaging templates, automatically saving any missing message template types to the db. + """ + # First save any missing template types to db + save_defaults_for_all_messaging_template_types(db) + ordered_templates = MessagingTemplate.query(db=db).order_by( + MessagingTemplate.created_at.desc() + ) + # Now return all templates + return paginate( + ordered_templates, + params=params, + ) + + @router.get( MESSAGING_TEMPLATE_DEFAULT_BY_TEMPLATE_TYPE, dependencies=[Security(verify_oauth_client, scopes=[MESSAGING_TEMPLATE_UPDATE])], @@ -617,6 +652,95 @@ def get_default_messaging_template( ) +@router.post( + MESSAGING_TEMPLATES_BY_TEMPLATE_TYPE, + dependencies=[Security(verify_oauth_client, scopes=[MESSAGING_TEMPLATE_UPDATE])], + response_model=Optional[MessagingTemplateWithPropertiesDetail], +) +def create_property_specific_messaging_template( + template_type: MessagingActionType, + *, + db: Session = Depends(deps.get_db), + messaging_template_create_body: MessagingTemplateWithPropertiesBodyParams, +) -> Optional[MessagingTemplate]: + """ + Creates property-specific messaging template by template type. + """ + logger.info( + "Creating new property-specific messaging template of type '{}'", template_type + ) + try: + return create_property_specific_template_by_type( + db, template_type, messaging_template_create_body + ) + except MessagingTemplateValidationException as e: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=e.message, + ) + + +@router.put( + MESSAGING_TEMPLATE_BY_ID, + dependencies=[Security(verify_oauth_client, scopes=[MESSAGING_TEMPLATE_UPDATE])], + response_model=Optional[MessagingTemplateWithPropertiesDetail], +) +def update_property_specific_messaging_template( + template_id: str, + *, + db: Session = Depends(deps.get_db), + messaging_template_update_body: MessagingTemplateWithPropertiesBodyParams, +) -> Optional[MessagingTemplate]: + """ + Updates property-specific messaging template by template id. + """ + logger.info("Updating property-specific messaging template of id '{}'", template_id) + try: + return update_property_specific_template( + db, template_id, messaging_template_update_body + ) + except EmailTemplateNotFoundException as e: + raise HTTPException( + status_code=HTTP_404_NOT_FOUND, + detail=e.message, + ) + except MessagingTemplateValidationException as e: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=e.message, + ) + + +@router.patch( + MESSAGING_TEMPLATE_BY_ID, + dependencies=[Security(verify_oauth_client, scopes=[MESSAGING_TEMPLATE_UPDATE])], + response_model=Optional[MessagingTemplateWithPropertiesDetail], +) +def patch_property_specific_messaging_template( + template_id: str, + *, + db: Session = Depends(deps.get_db), + messaging_template_update_body: MessagingTemplateWithPropertiesPatchBodyParams, +) -> Optional[MessagingTemplate]: + """ + Updates property-specific messaging template by template id. + """ + logger.info("Patching property-specific messaging template of id '{}'", template_id) + try: + data = messaging_template_update_body.dict(exclude_none=True) + return patch_property_specific_template(db, template_id, data) + except EmailTemplateNotFoundException as e: + raise HTTPException( + status_code=HTTP_404_NOT_FOUND, + detail=e.message, + ) + except MessagingTemplateValidationException as e: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=e.message, + ) + + @router.get( MESSAGING_TEMPLATE_BY_ID, dependencies=[Security(verify_oauth_client, scopes=[MESSAGING_TEMPLATE_UPDATE])], diff --git a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py index ac9b1cb713..384215b376 100644 --- a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py @@ -2055,6 +2055,91 @@ def test_put_messaging_templates_invalid_type( } +class TestGetPropertySpecificMessagingTemplateSummary: + @pytest.fixture + def url(self) -> str: + return V1_URL_PREFIX + MESSAGING_TEMPLATES_SUMMARY + + def test_get_messaging_templates_unauthorized( + self, url, api_client: TestClient, generate_auth_header + ) -> None: + auth_header = generate_auth_header(scopes=[]) + response = api_client.get(url, headers=auth_header) + assert response.status_code == 403 + + def test_get_messaging_templates_wrong_scope( + self, url, api_client: TestClient, generate_auth_header + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_READ]) + response = api_client.get(url, headers=auth_header) + assert response.status_code == 403 + + def test_get_messaging_templates_summary_no_db_templates( + self, url, api_client: TestClient, generate_auth_header + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + response = api_client.get(url, headers=auth_header) + assert response.status_code == 200 + response_body = json.loads(response.text) + assert len(response_body["items"]) == 6 + + # Validate the response conforms to the expected model + [ + MessagingTemplateWithPropertiesSummary(**item) + for item in response_body["items"] + ] + + def test_get_all_messaging_templates_summary_some_db_templates( + self, + url, + api_client: TestClient, + generate_auth_header, + messaging_template_subject_identity_verification, + messaging_template_privacy_request_receipt, + ): + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + response = api_client.get(url, headers=auth_header) + assert response.status_code == 200 + response_body = json.loads(response.text) + assert len(response_body["items"]) == 6 + + # Validate the response conforms to the expected model + [ + MessagingTemplateWithPropertiesSummary(**item) + for item in response_body["items"] + ] + + def test_get_all_messaging_templates_summary_all_db_templates( + self, db: Session, url, api_client: TestClient, generate_auth_header, property_a + ): + content = { + "subject": "Some subject", + "body": "Some body", + } + for template_type, default_template in DEFAULT_MESSAGING_TEMPLATES.items(): + data = { + "content": content, + "properties": [{"id": property_a.id, "name": property_a.name}], + "is_enabled": True, + "type": template_type, + } + MessagingTemplate.create( + db=db, + data=data, + ) + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + response = api_client.get(url, headers=auth_header) + assert response.status_code == 200 + response_body = json.loads(response.text) + assert len(response_body["items"]) == 6 + + # Validate the response conforms to the expected model + [ + MessagingTemplateWithPropertiesSummary(**item) + for item in response_body["items"] + ] + + class TestGetMessagingTemplateDefaultByTemplateType: @pytest.fixture def url(self) -> str: @@ -2099,6 +2184,367 @@ def test_get_messaging_template_default( MessagingTemplateDefault(**resp) +class TestCreateMessagingTemplateByTemplateType: + @pytest.fixture + def url(self) -> str: + return (V1_URL_PREFIX + MESSAGING_TEMPLATES_BY_TEMPLATE_TYPE).format( + template_type=MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value + ) + + @pytest.fixture + def test_create_data(self) -> Dict[str, Any]: + return { + "content": { + "subject": "Here is your code {{code}}", + "body": "Use code {{code}} to verify your identity, you have {{minutes}} minutes!", + }, + "properties": [], + "is_enabled": False, + } + + def test_create_messaging_template_unauthorized( + self, url, api_client: TestClient, generate_auth_header, test_create_data + ) -> None: + auth_header = generate_auth_header(scopes=[]) + response = api_client.post(url, json=test_create_data, headers=auth_header) + assert response.status_code == 403 + + def test_create_messaging_template_wrong_scope( + self, url, api_client: TestClient, generate_auth_header, test_create_data + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_READ]) + response = api_client.post(url, json=test_create_data, headers=auth_header) + assert response.status_code == 403 + + def test_create_messaging_template_invalid( + self, + url, + api_client: TestClient, + generate_auth_header, + messaging_template_subject_identity_verification, + test_create_data, + property_a, + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + data = {**test_create_data, "is_enabled": True, "properties": [property_a.id]} + # Cannot create messaging template with same template type and property id as existing + response = api_client.post(url, json=data, headers=auth_header) + assert response.status_code == 400 + + def test_create_messaging_template_success( + self, + url, + db: Session, + api_client: TestClient, + generate_auth_header, + test_create_data, + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + response = api_client.post(url, json=test_create_data, headers=auth_header) + assert response.status_code == 200 + + template_with_type = MessagingTemplate.get_by( + db=db, + field="type", + value=MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value, + ) + + assert template_with_type.content == test_create_data["content"] + assert template_with_type.properties == [] + assert template_with_type.is_enabled is False + + # delete created template so that property fixture can be deleted + db.delete(template_with_type) + + def test_create_messaging_template_with_properties_success( + self, + url, + db: Session, + api_client: TestClient, + generate_auth_header, + test_create_data, + property_a, + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + data = {**test_create_data, "properties": [property_a.id]} + response = api_client.post(url, json=data, headers=auth_header) + assert response.status_code == 200 + + template_with_type = MessagingTemplate.get_by( + db=db, + field="type", + value=MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value, + ) + + assert template_with_type.content == test_create_data["content"] + assert len(template_with_type.properties) == 1 + assert template_with_type.properties[0].id == property_a.id + assert template_with_type.properties[0].name == property_a.name + assert template_with_type.is_enabled is False + + # delete created template so that property fixture can be deleted + db.delete(template_with_type) + + +class TestPatchMessagingTemplateByTemplateType: + @pytest.fixture + def url(self, messaging_template_with_property_disabled) -> str: + return (V1_URL_PREFIX + MESSAGING_TEMPLATE_BY_ID).format( + template_id=messaging_template_with_property_disabled.id + ) + + @pytest.fixture + def test_patch_data_enable(self) -> Dict[str, Any]: + return { + "is_enabled": True, + } + + def test_patch_messaging_template_unauthorized( + self, url, api_client: TestClient, generate_auth_header, test_patch_data_enable + ) -> None: + auth_header = generate_auth_header(scopes=[]) + response = api_client.patch( + url, json=test_patch_data_enable, headers=auth_header + ) + assert response.status_code == 403 + + def test_patch_messaging_template_wrong_scope( + self, url, api_client: TestClient, generate_auth_header, test_patch_data_enable + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_READ]) + response = api_client.patch( + url, json=test_patch_data_enable, headers=auth_header + ) + assert response.status_code == 403 + + def test_patch_messaging_template_invalid_id( + self, api_client: TestClient, generate_auth_header, test_patch_data_enable + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + url = (V1_URL_PREFIX + MESSAGING_TEMPLATE_BY_ID).format(template_id="invalid") + response = api_client.patch( + url, json=test_patch_data_enable, headers=auth_header + ) + assert response.status_code == 404 + + def test_patch_enable_messaging_template_invalid_data( + self, + api_client: TestClient, + generate_auth_header, + messaging_template_subject_identity_verification, + messaging_template_no_property, + property_a, + test_patch_data_enable, + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + url = (V1_URL_PREFIX + MESSAGING_TEMPLATE_BY_ID).format( + template_id=messaging_template_no_property.id + ) + + # this property is already used by the subject identity verification template + data = {**test_patch_data_enable, "properties": [property_a.id]} + + response = api_client.patch(url, json=data, headers=auth_header) + assert response.status_code == 400 + + def test_patch_enable_messaging_template_success( + self, + url, + db: Session, + api_client: TestClient, + generate_auth_header, + test_patch_data_enable, + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + response = api_client.patch( + url, json=test_patch_data_enable, headers=auth_header + ) + assert response.status_code == 200 + + template_with_type = MessagingTemplate.get_by( + db=db, + field="type", + value=MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value, + ) + + assert template_with_type.content is not None + assert len(template_with_type.properties) == 1 + assert template_with_type.is_enabled is True + + db.delete(template_with_type) + + def test_patch_enable_messaging_template_with_new_properties_success( + self, + db: Session, + api_client: TestClient, + generate_auth_header, + test_patch_data_enable, + property_a, + property_b, + ) -> None: + template_type = MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value + content = { + "subject": "Here is your code {{code}}", + "body": "Use code {{code}} to verify your identity, you have {{minutes}} minutes!", + } + data = { + "content": content, + "properties": [{"id": property_a.id, "name": property_a.name}], + "is_enabled": False, + "type": template_type, + } + messaging_template = MessagingTemplate.create( + db=db, + data=data, + ) + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + url = (V1_URL_PREFIX + MESSAGING_TEMPLATE_BY_ID).format( + template_id=messaging_template.id + ) + # replace property a with property b and enable + data = { + **test_patch_data_enable, + "is_enabled": True, + "properties": [property_b.id], + } + response = api_client.patch(url, json=data, headers=auth_header) + assert response.status_code == 200 + + db.refresh(messaging_template) + + assert messaging_template.content is not None + assert len(messaging_template.properties) == 1 + assert messaging_template.properties[0].id == property_b.id + assert messaging_template.is_enabled is True + + db.delete(messaging_template) + + def test_patch_disable_messaging_template_with_properties_success( + self, + db: Session, + api_client: TestClient, + generate_auth_header, + test_patch_data_enable, + property_a, + ) -> None: + template_type = MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value + content = { + "subject": "Here is your code {{code}}", + "body": "Use code {{code}} to verify your identity, you have {{minutes}} minutes!", + } + data = { + "content": content, + "properties": [{"id": property_a.id, "name": property_a.name}], + "is_enabled": True, + "type": template_type, + } + messaging_template = MessagingTemplate.create( + db=db, + data=data, + ) + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + url = (V1_URL_PREFIX + MESSAGING_TEMPLATE_BY_ID).format( + template_id=messaging_template.id + ) + data = { + "is_enabled": False, + } + response = api_client.patch(url, json=data, headers=auth_header) + assert response.status_code == 200 + + db.refresh(messaging_template) + + assert messaging_template.content is not None + assert len(messaging_template.properties) == 1 + assert messaging_template.is_enabled is False + + db.delete(messaging_template) + + +class TestUpdateMessagingTemplateByTemplateType: + @pytest.fixture + def url(self, messaging_template_subject_identity_verification) -> str: + return (V1_URL_PREFIX + MESSAGING_TEMPLATE_BY_ID).format( + template_id=messaging_template_subject_identity_verification.id + ) + + @pytest.fixture + def test_update_data(self) -> Dict[str, Any]: + return { + "content": { + "subject": "Hello there, here is your code: {{code}}", + "body": "Use code {{code}} to verify your identity, you have {{minutes}} minutes!", + }, + "properties": [], + "is_enabled": False, + } + + def test_update_messaging_template_unauthorized( + self, url, api_client: TestClient, generate_auth_header, test_update_data + ) -> None: + auth_header = generate_auth_header(scopes=[]) + response = api_client.put(url, json=test_update_data, headers=auth_header) + assert response.status_code == 403 + + def test_update_messaging_template_wrong_scope( + self, url, api_client: TestClient, generate_auth_header, test_update_data + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_READ]) + response = api_client.put(url, json=test_update_data, headers=auth_header) + assert response.status_code == 403 + + def test_update_messaging_template_invalid_id( + self, api_client: TestClient, generate_auth_header, test_update_data + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + url = (V1_URL_PREFIX + MESSAGING_TEMPLATE_BY_ID).format(template_id="invalid") + response = api_client.put(url, json=test_update_data, headers=auth_header) + assert response.status_code == 404 + + def test_update_messaging_template_invalid_data( + self, + api_client: TestClient, + generate_auth_header, + messaging_template_subject_identity_verification, + messaging_template_no_property, + property_a, + test_update_data, + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + url = (V1_URL_PREFIX + MESSAGING_TEMPLATE_BY_ID).format( + template_id=messaging_template_no_property.id + ) + # this property is already used by the subject identity verification template + data = {**test_update_data, "is_enabled": True, "properties": [property_a.id]} + + response = api_client.put(url, json=data, headers=auth_header) + assert response.status_code == 400 + + def test_update_messaging_template_success( + self, + url, + db: Session, + api_client: TestClient, + generate_auth_header, + test_update_data, + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + response = api_client.put(url, json=test_update_data, headers=auth_header) + assert response.status_code == 200 + + template_with_type = MessagingTemplate.get_by( + db=db, + field="type", + value=MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value, + ) + + assert template_with_type.content == test_update_data["content"] + assert template_with_type.properties == [] + assert template_with_type.is_enabled is False + + db.delete(template_with_type) + + class TestGetMessagingTemplateById: @pytest.fixture def url(self, messaging_template_subject_identity_verification) -> str: