diff --git a/src/fides/api/api/v1/endpoints/messaging_endpoints.py b/src/fides/api/api/v1/endpoints/messaging_endpoints.py index 18f50c6e51..fe379d4944 100644 --- a/src/fides/api/api/v1/endpoints/messaging_endpoints.py +++ b/src/fides/api/api/v1/endpoints/messaging_endpoints.py @@ -21,6 +21,8 @@ from fides.api.common_exceptions import ( MessageDispatchException, MessagingConfigNotFoundException, + EmailTemplateNotFoundException, + MessagingTemplateValidationException, ) from fides.api.models.messaging import ( MessagingConfig, @@ -28,11 +30,14 @@ default_messaging_config_name, 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 ( - BulkPutMessagingTemplateResponse, + BulkPutBasicMessagingTemplateResponse, MessagingActionType, MessagingConfigRequest, MessagingConfigRequestBase, @@ -40,9 +45,14 @@ MessagingConfigStatus, MessagingConfigStatusMessage, MessagingServiceType, - MessagingTemplateRequest, - MessagingTemplateResponse, + BasicMessagingTemplateRequest, + BasicMessagingTemplateResponse, TestMessagingStatusMessage, + MessagingTemplateWithPropertiesSummary, + MessagingTemplateWithPropertiesDetail, + MessagingTemplateWithPropertiesBodyParams, + MessagingTemplateDefault, + MessagingTemplateWithPropertiesPatchBodyParams, ) from fides.api.schemas.messaging.messaging_secrets_docs_only import ( possible_messaging_secrets, @@ -50,12 +60,19 @@ from fides.api.schemas.redis_cache import Identity from fides.api.service.messaging.message_dispatch_service import dispatch_message from fides.api.service.messaging.messaging_crud_service import ( - create_or_update_basic_templates, create_or_update_messaging_config, delete_messaging_config, get_all_basic_messaging_templates, get_messaging_config_by_key, update_messaging_config, + create_or_update_basic_templates, + get_default_template_by_type, + create_property_specific_template_by_type, + get_template_by_id, + update_property_specific_template, + delete_template_by_id, + save_defaults_for_all_messaging_template_types, + patch_property_specific_template, ) from fides.api.util.api_router import APIRouter from fides.api.util.logger import Pii @@ -74,9 +91,13 @@ MESSAGING_DEFAULT_SECRETS, MESSAGING_SECRETS, MESSAGING_STATUS, - MESSAGING_TEMPLATES, + BASIC_MESSAGING_TEMPLATES, MESSAGING_TEST, V1_URL_PREFIX, + MESSAGING_TEMPLATES_SUMMARY, + MESSAGING_TEMPLATES_BY_TEMPLATE_TYPE, + MESSAGING_TEMPLATE_DEFAULT_BY_TEMPLATE_TYPE, + MESSAGING_TEMPLATE_BY_ID, ) from fides.config.config_proxy import ConfigProxy @@ -496,16 +517,16 @@ def send_test_message( @router.get( - MESSAGING_TEMPLATES, + BASIC_MESSAGING_TEMPLATES, dependencies=[Security(verify_oauth_client, scopes=[MESSAGING_TEMPLATE_UPDATE])], - response_model=List[MessagingTemplateResponse], + response_model=List[BasicMessagingTemplateResponse], ) def get_basic_messaging_templates( *, db: Session = Depends(deps.get_db) -) -> List[MessagingTemplateResponse]: +) -> List[BasicMessagingTemplateResponse]: """Returns the available messaging templates, augments the models with labels to be used in the UI.""" return [ - MessagingTemplateResponse( + BasicMessagingTemplateResponse( type=template.type, content=template.content, label=DEFAULT_MESSAGING_TEMPLATES.get(template.type, {}).get("label", None), @@ -515,12 +536,14 @@ def get_basic_messaging_templates( @router.put( - MESSAGING_TEMPLATES, + BASIC_MESSAGING_TEMPLATES, dependencies=[Security(verify_oauth_client, scopes=[MESSAGING_TEMPLATE_UPDATE])], ) def update_basic_messaging_templates( - templates: List[MessagingTemplateRequest], *, db: Session = Depends(deps.get_db) -) -> BulkPutMessagingTemplateResponse: + templates: List[BasicMessagingTemplateRequest], + *, + db: Session = Depends(deps.get_db), +) -> BulkPutBasicMessagingTemplateResponse: """Updates the messaging templates and reverts empty subject or body values to the default values.""" succeeded = [] @@ -535,9 +558,7 @@ def update_basic_messaging_templates( if not default_template: raise ValueError("Invalid template type.") - content["subject"] = ( - content["subject"] or default_template["content"]["subject"] - ) + content["subject"] = content["subject"] or default_template["content"]["subject"] content["body"] = content["body"] or default_template["content"]["body"] # For Basic Messaging Templates, we ignore the is_enabled flag at runtime. This is because @@ -549,7 +570,7 @@ def update_basic_messaging_templates( ) succeeded.append( - MessagingTemplateResponse( + BasicMessagingTemplateResponse( type=template_type, content=content, label=default_template.get("label"), @@ -565,4 +586,196 @@ def update_basic_messaging_templates( ) ) - return BulkPutMessagingTemplateResponse(succeeded=succeeded, failed=failed) + 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])], + response_model=MessagingTemplateDefault, +) +def get_default_messaging_template( + template_type: MessagingActionType, +) -> MessagingTemplateDefault: + """ + Retrieves default messaging template by template type. + """ + logger.info( + "Finding default messaging template of template type '{}'", template_type + ) + try: + return get_default_template_by_type(template_type) + except MessagingTemplateValidationException as e: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=e.message, + ) + + +@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])], + response_model=MessagingTemplateWithPropertiesDetail, +) +def get_messaging_template_by_id( + template_id: str, + *, + db: Session = Depends(deps.get_db), +) -> MessagingTemplateWithPropertiesDetail: + """ + Retrieves messaging template by template tid. + """ + logger.info("Finding messaging template with id '{}'", template_id) + + try: + messaging_template = get_template_by_id(db, template_id) + return MessagingTemplateWithPropertiesDetail( + id=template_id, + type=messaging_template.type, + content=messaging_template.content, + is_enabled=messaging_template.is_enabled, + properties=messaging_template.properties, + ) + except EmailTemplateNotFoundException as e: + raise HTTPException( + status_code=HTTP_404_NOT_FOUND, + detail=e.message, + ) + + +@router.delete( + MESSAGING_TEMPLATE_BY_ID, + dependencies=[Security(verify_oauth_client, scopes=[MESSAGING_TEMPLATE_UPDATE])], + status_code=HTTP_204_NO_CONTENT, + response_model=None, +) +def delete_messaging_template_by_id( + template_id: str, + *, + db: Session = Depends(deps.get_db), +) -> None: + """ + Deletes messaging template by template id. + """ + logger.info("Deleting messaging template with id '{}'", template_id) + try: + delete_template_by_id(db, template_id) + 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) diff --git a/src/fides/api/common_exceptions.py b/src/fides/api/common_exceptions.py index fdac5476c2..8c146e07a3 100644 --- a/src/fides/api/common_exceptions.py +++ b/src/fides/api/common_exceptions.py @@ -203,6 +203,14 @@ class EmailTemplateUnhandledActionType(FidesopsException): """Custom Exception - Email Template Unhandled ActionType Error""" +class EmailTemplateNotFoundException(FidesopsException): + """Custom Exception - Email Template Not Found""" + + +class MessagingTemplateValidationException(FidesopsException): + """Custom Exception - Messaging Template Could Not Be Created, Updated, or Deleted""" + + class OAuth2TokenException(FidesopsException): """Custom Exception - Unable to access or refresh OAuth2 tokens for SaaS connector""" diff --git a/src/fides/api/schemas/messaging/messaging.py b/src/fides/api/schemas/messaging/messaging.py index 552a5a92e4..501463be64 100644 --- a/src/fides/api/schemas/messaging/messaging.py +++ b/src/fides/api/schemas/messaging/messaging.py @@ -5,7 +5,7 @@ from fideslang.default_taxonomy import DEFAULT_TAXONOMY from fideslang.validation import FidesKey -from pydantic import BaseModel, Extra, root_validator +from pydantic import BaseModel, Extra, root_validator, Field from fides.api.custom_types import PhoneNumber, SafeStr from fides.api.schemas import Msg @@ -74,7 +74,7 @@ class MessagingActionType(str, Enum): MessagingActionType.PRIVACY_REQUEST_COMPLETE_ACCESS.value, MessagingActionType.PRIVACY_REQUEST_COMPLETE_DELETION.value, MessagingActionType.PRIVACY_REQUEST_REVIEW_DENY.value, - MessagingActionType.PRIVACY_REQUEST_REVIEW_APPROVE.value + MessagingActionType.PRIVACY_REQUEST_REVIEW_APPROVE.value, ) @@ -428,41 +428,89 @@ class MessagingConfigStatusMessage(BaseModel): detail: Optional[str] = None -class MessagingTemplateBase(BaseModel): +class BasicMessagingTemplateBase(BaseModel): type: str - content: Dict[str, Any] + content: Dict[str, Any] = Field( + example={ + "subject": "Message subject", + "body": "Custom message body", + } + ) -class MessagingTemplateRequest(MessagingTemplateBase): +class BasicMessagingTemplateRequest(BasicMessagingTemplateBase): pass -class MessagingTemplateResponse(MessagingTemplateBase): +class BasicMessagingTemplateResponse(BasicMessagingTemplateBase): label: str -class BulkPutMessagingTemplateResponse(BulkResponse): - succeeded: List[MessagingTemplateResponse] +class BulkPutBasicMessagingTemplateResponse(BulkResponse): + succeeded: List[BasicMessagingTemplateResponse] failed: List[BulkUpdateFailed] class MessagingTemplateWithPropertiesBase(BaseModel): - id: Optional[str] # Since summary returns db or defaults, this can be null + id: str type: str is_enabled: bool properties: Optional[List[MinimalProperty]] + class Config: + orm_mode = True + use_enum_values = True + + +class MessagingTemplateDefault(BaseModel): + type: str + is_enabled: bool + content: Dict[str, Any] = Field( + example={ + "subject": "Message subject", + "body": "Custom message body", + } + ) + class MessagingTemplateWithPropertiesSummary(MessagingTemplateWithPropertiesBase): - pass + class Config: + orm_mode = True + use_enum_values = True class MessagingTemplateWithPropertiesDetail(MessagingTemplateWithPropertiesBase): - content: Dict[str, Any] + content: Dict[str, Any] = Field( + example={ + "subject": "Message subject", + "body": "Custom message body", + } + ) + + class Config: + orm_mode = True + use_enum_values = True class MessagingTemplateWithPropertiesBodyParams(BaseModel): - content: Dict[str, Any] + content: Dict[str, Any] = Field( + example={ + "subject": "Message subject", + "body": "Custom message body", + } + ) properties: Optional[List[str]] is_enabled: bool + + +class MessagingTemplateWithPropertiesPatchBodyParams(BaseModel): + + content: Optional[Dict[str, Any]] = Field( + example={ + "subject": "Message subject", + "body": "Custom message body", + } + ) + properties: Optional[List[str]] + is_enabled: Optional[bool] diff --git a/src/fides/api/service/messaging/messaging_crud_service.py b/src/fides/api/service/messaging/messaging_crud_service.py index 4f87606ee5..56dfc55b82 100644 --- a/src/fides/api/service/messaging/messaging_crud_service.py +++ b/src/fides/api/service/messaging/messaging_crud_service.py @@ -1,4 +1,3 @@ -from collections import defaultdict from typing import Any, Dict, List, Optional from fideslang.validation import FidesKey @@ -7,7 +6,8 @@ from fides.api.common_exceptions import ( MessagingConfigNotFoundException, - MessagingConfigValidationException, + EmailTemplateNotFoundException, + MessagingTemplateValidationException, ) from fides.api.models.messaging import MessagingConfig from fides.api.models.messaging_template import ( @@ -19,8 +19,7 @@ MessagingConfigRequest, MessagingConfigResponse, MessagingTemplateWithPropertiesBodyParams, - MessagingTemplateWithPropertiesDetail, - MessagingTemplateWithPropertiesSummary, + MessagingTemplateDefault, ) @@ -281,12 +280,50 @@ def _validate_overlapping_templates( for db_template in possible_overlapping_templates: for db_property in db_template.properties: if db_property.id in new_property_ids: - raise MessagingConfigValidationException( + raise MessagingTemplateValidationException( f"There is already an enabled messaging template with template type {template_type} and property {db_property.id}" ) -def update_messaging_template( +def patch_property_specific_template( + db: Session, + template_id: str, + template_patch_data: Dict[str, Any], +) -> Optional[MessagingTemplate]: + """ + This method is only for the property-specific messaging templates feature. Not for basic messaging templates. + + Used to perform a partial update for messaging templates. E.g. template_patch_data = {"is_enabled": False} + """ + messaging_template: MessagingTemplate = get_template_by_id(db, template_id) + # use passed-in values if they exist, otherwise fall back on existing values in DB + properties = ( + template_patch_data["properties"] + if "properties" in list(template_patch_data.keys()) + else messaging_template.properties + ) + is_enabled = ( + template_patch_data["is_enabled"] + if "is_enabled" in list(template_patch_data.keys()) + else messaging_template.is_enabled + ) + _validate_overlapping_templates( + db, + messaging_template.type, + properties, + is_enabled, + template_id, + ) + + if "properties" in list(template_patch_data.keys()): + template_patch_data["properties"] = [ + {"id": property_id} for property_id in template_patch_data["properties"] + ] + + return messaging_template.update(db=db, data=template_patch_data) + + +def update_property_specific_template( db: Session, template_id: str, template_update_body: MessagingTemplateWithPropertiesBodyParams, @@ -317,7 +354,7 @@ def update_messaging_template( return messaging_template.update(db=db, data=data) -def create_messaging_template( +def create_property_specific_template_by_type( db: Session, template_type: str, template_create_body: MessagingTemplateWithPropertiesBodyParams, @@ -326,7 +363,7 @@ def create_messaging_template( This method is only for the property-specific messaging templates feature. Not for basic messaging templates. """ if template_type not in DEFAULT_MESSAGING_TEMPLATES: - raise MessagingConfigValidationException( + raise MessagingTemplateValidationException( f"Messaging template type {template_type} is not supported." ) _validate_overlapping_templates( @@ -357,7 +394,7 @@ def delete_template_by_id(db: Session, template_id: str) -> None: .all() ) if len(templates_with_type) <= 1: - raise MessagingConfigValidationException( + raise MessagingTemplateValidationException( f"Messaging template with id {template_id} cannot be deleted because it is the only template with type {messaging_template.type}" ) logger.info("Deleting messaging config with id '{}'", template_id) @@ -370,7 +407,7 @@ def get_template_by_id(db: Session, template_id: str) -> MessagingTemplate: db, object_id=template_id ) if not messaging_template: - raise MessagingConfigNotFoundException( + raise EmailTemplateNotFoundException( f"No messaging template found with id {template_id}" ) return messaging_template @@ -378,69 +415,45 @@ def get_template_by_id(db: Session, template_id: str) -> MessagingTemplate: def get_default_template_by_type( template_type: str, -) -> MessagingTemplateWithPropertiesDetail: +) -> MessagingTemplateDefault: default_template = DEFAULT_MESSAGING_TEMPLATES.get(template_type) if not default_template: - raise MessagingConfigValidationException( + raise MessagingTemplateValidationException( f"Messaging template type {template_type} is not supported." ) - template = MessagingTemplateWithPropertiesDetail( - id=None, + template = MessagingTemplateDefault( + is_enabled=False, type=template_type, content=default_template["content"], - is_enabled=False, - properties=[], ) return template -# TODO: (PROD-2058) if id is None, we know on FE that this does not exist yet in DB -def get_all_messaging_templates_summary( +def save_defaults_for_all_messaging_template_types( db: Session, -) -> Optional[List[MessagingTemplateWithPropertiesSummary]]: +) -> None: """ This method is only for the property-specific messaging templates feature. Not for basic messaging templates. + + We retrieve all templates from the db, writing the default templates that do not already exist. This way, we can + have a pure Query obj to provide to the endpoint pagination fn. """ - # Retrieve all templates from the database - db_templates: Dict[str, List[Dict[str, Any]]] = defaultdict(list) - for template in MessagingTemplate.all(db): - db_templates[template.type].append( - { - "id": template.id, - "type": template.type, - "is_enabled": template.is_enabled, - "properties": template.properties, - } - ) + logger.info( + "Saving any messaging template defaults that don't yet exist in the DB..." + ) - # Create a list of MessagingTemplate models, using defaults if a key is not found in the database - templates: List[MessagingTemplateWithPropertiesSummary] = [] for ( template_type, default_template, # pylint: disable=W0612 ) in DEFAULT_MESSAGING_TEMPLATES.items(): - # insert type key, see if there are any matches with DB, else use defaults - db_templates_with_type: Optional[List[Dict[str, Any]]] = db_templates.get( - template_type + # If the db does not have any existing templates with a given template type, write one to the DB + any_db_template_with_type = MessagingTemplate.get_by( + db=db, field="type", value=template_type ) - if db_templates_with_type: - for db_template in db_templates_with_type: - templates.append( - MessagingTemplateWithPropertiesSummary( - id=db_template["id"], - type=template_type, - is_enabled=db_template["is_enabled"], - properties=db_template["properties"], - ) - ) - else: - templates.append( - MessagingTemplateWithPropertiesSummary( - id=None, - type=template_type, - is_enabled=False, - properties=[], - ) - ) - - return templates + if not any_db_template_with_type: + data = { + "content": default_template["content"], + "is_enabled": False, + "type": template_type, + } + MessagingTemplate.create(db=db, data=data) diff --git a/src/fides/common/api/v1/urn_registry.py b/src/fides/common/api/v1/urn_registry.py index beac5c277d..a97cf17a84 100644 --- a/src/fides/common/api/v1/urn_registry.py +++ b/src/fides/common/api/v1/urn_registry.py @@ -51,7 +51,13 @@ # Email URLs -MESSAGING_TEMPLATES = "/messaging/templates" +BASIC_MESSAGING_TEMPLATES = "/messaging/templates" +MESSAGING_TEMPLATE_DEFAULT_BY_TEMPLATE_TYPE = ( + "/messaging/templates/default/{template_type}" +) +MESSAGING_TEMPLATES_SUMMARY = "/messaging/templates/summary" +MESSAGING_TEMPLATES_BY_TEMPLATE_TYPE = "/messaging/templates/{template_type}" +MESSAGING_TEMPLATE_BY_ID = "/messaging/templates/{template_id}" MESSAGING_CONFIG = "/messaging/config" MESSAGING_SECRETS = "/messaging/config/{config_key}/secret" MESSAGING_BY_KEY = "/messaging/config/{config_key}" diff --git a/tests/fixtures/application_fixtures.py b/tests/fixtures/application_fixtures.py index 15cc50ba89..60408acce4 100644 --- a/tests/fixtures/application_fixtures.py +++ b/tests/fixtures/application_fixtures.py @@ -361,6 +361,27 @@ def property_b(db: Session) -> Generator: prop_b.delete(db=db) +@pytest.fixture(scope="function") +def messaging_template_no_property_disabled(db: Session) -> Generator: + 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": [], + "is_enabled": False, + "type": template_type, + } + messaging_template = MessagingTemplate.create( + db=db, + data=data, + ) + yield messaging_template + messaging_template.delete(db) + + @pytest.fixture(scope="function") def messaging_template_no_property(db: Session) -> Generator: template_type = MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value @@ -391,14 +412,15 @@ def messaging_template_subject_identity_verification( "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=MessagingTemplateWithPropertiesDetail( - content=content, - properties=[{"id": property_a.id, "name": property_a.name}], - is_enabled=True, - type=template_type, - ).dict(), + data=data, ) yield messaging_template messaging_template.delete(db) @@ -411,14 +433,15 @@ def messaging_template_privacy_request_receipt(db: Session, property_a) -> Gener "subject": "Your request has been received.", "body": "Stay tuned!", } + 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=MessagingTemplateWithPropertiesDetail( - content=content, - properties=[{"id": property_a.id, "name": property_a.name}], - is_enabled=True, - type=template_type, - ).dict(), + data=data, ) yield messaging_template messaging_template.delete(db) diff --git a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py index 7baaed1858..a1238fc340 100644 --- a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py @@ -5,6 +5,10 @@ import pytest from fastapi_pagination import Params +from fides.api.models.messaging_template import ( + DEFAULT_MESSAGING_TEMPLATES, + MessagingTemplate, +) from sqlalchemy.orm import Session from starlette.testclient import TestClient @@ -17,7 +21,11 @@ MessagingServiceDetails, MessagingServiceSecrets, MessagingServiceType, - MessagingTemplateResponse, + BasicMessagingTemplateResponse, + MessagingTemplateWithPropertiesSummary, + MessagingTemplateWithPropertiesDetail, + MessagingActionType, + MessagingTemplateDefault, ) from fides.common.api.scope_registry import ( MESSAGING_CREATE_OR_UPDATE, @@ -34,9 +42,13 @@ MESSAGING_DEFAULT_SECRETS, MESSAGING_SECRETS, MESSAGING_STATUS, - MESSAGING_TEMPLATES, + BASIC_MESSAGING_TEMPLATES, MESSAGING_TEST, V1_URL_PREFIX, + MESSAGING_TEMPLATES_SUMMARY, + MESSAGING_TEMPLATE_DEFAULT_BY_TEMPLATE_TYPE, + MESSAGING_TEMPLATES_BY_TEMPLATE_TYPE, + MESSAGING_TEMPLATE_BY_ID, ) from fides.config import get_config @@ -1887,10 +1899,10 @@ def test_test_message_dispatch_error( assert mock_dispatch_message.called -class TestGetMessagingTemplates: +class TestGetBasicMessagingTemplates: @pytest.fixture def url(self) -> str: - return V1_URL_PREFIX + MESSAGING_TEMPLATES + return V1_URL_PREFIX + BASIC_MESSAGING_TEMPLATES def test_get_messaging_templates_unauthorized( self, url, api_client: TestClient, generate_auth_header @@ -1914,13 +1926,13 @@ def test_get_messaging_templates( assert response.status_code == 200 # Validate the response conforms to the expected model - [MessagingTemplateResponse(**item) for item in response.json()] + [BasicMessagingTemplateResponse(**item) for item in response.json()] -class TestPutMessagingTemplates: +class TestPutBasicMessagingTemplates: @pytest.fixture def url(self) -> str: - return V1_URL_PREFIX + MESSAGING_TEMPLATES + return V1_URL_PREFIX + BASIC_MESSAGING_TEMPLATES @pytest.fixture def payload(self) -> List[Dict[str, Any]]: @@ -2041,3 +2053,520 @@ 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: + return (V1_URL_PREFIX + MESSAGING_TEMPLATE_DEFAULT_BY_TEMPLATE_TYPE).format( + template_type=MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value + ) + + def test_get_messaging_template_default_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_template_default_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_template_default_unsupported( + self, api_client: TestClient, generate_auth_header + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + # This template type is not supported for having defaults + url = (V1_URL_PREFIX + MESSAGING_TEMPLATE_DEFAULT_BY_TEMPLATE_TYPE).format( + template_type=MessagingActionType.CONSENT_REQUEST_EMAIL_FULFILLMENT.value + ) + response = api_client.get(url, headers=auth_header) + assert response.status_code == 400 + + def test_get_messaging_template_default( + 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 + resp = response.json() + + # Validate the response conforms to the expected model + 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": True, + } + + 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, "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 True + + # 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 True + + # delete created template so that property fixture can be deleted + db.delete(template_with_type) + + +class TestPatchMessagingTemplateByTemplateType: + @pytest.fixture + def url(self, messaging_template_no_property_disabled) -> str: + return (V1_URL_PREFIX + MESSAGING_TEMPLATE_BY_ID).format( + template_id=messaging_template_no_property_disabled.id + ) + + @pytest.fixture + def test_patch_data(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 + ) -> None: + auth_header = generate_auth_header(scopes=[]) + response = api_client.patch(url, json=test_patch_data, 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 + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_READ]) + response = api_client.patch(url, json=test_patch_data, 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 + ) -> 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, headers=auth_header) + assert response.status_code == 404 + + def test_patch_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, + ) -> 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, "properties": [property_a.id]} + + response = api_client.patch(url, json=data, headers=auth_header) + assert response.status_code == 400 + + def test_patch_messaging_template_success( + self, + url, + db: Session, + api_client: TestClient, + generate_auth_header, + test_patch_data, + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + response = api_client.patch(url, json=test_patch_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 is not None + assert template_with_type.properties == [] + assert template_with_type.is_enabled is True + + +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": True, + } + + 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, "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 True + + +class TestGetMessagingTemplateById: + @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 + ) + + def test_get_messaging_template_by_id_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_template_by_id_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_template_by_id_invalid_id( + self, api_client: TestClient, generate_auth_header + ) -> 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.get(url, headers=auth_header) + assert response.status_code == 404 + + def test_get_messaging_template_by_id_success( + self, + url, + db: Session, + api_client: TestClient, + generate_auth_header, + property_a, + messaging_template_subject_identity_verification, + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + response = api_client.get(url, headers=auth_header) + assert response.status_code == 200 + template = response.json() + assert len(template["properties"]) == 1 + assert template["properties"][0]["id"] == property_a.id + assert template["is_enabled"] is True + + +class TestDeleteMessagingTemplateById: + @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 + ) + + def test_delete_messaging_template_by_id_unauthorized( + self, url, api_client: TestClient, generate_auth_header + ) -> None: + auth_header = generate_auth_header(scopes=[]) + response = api_client.delete(url, headers=auth_header) + assert response.status_code == 403 + + def test_delete_messaging_template_by_id_wrong_scope( + self, url, api_client: TestClient, generate_auth_header + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_READ]) + response = api_client.delete(url, headers=auth_header) + assert response.status_code == 403 + + def test_delete_messaging_template_by_id_invalid_id( + self, api_client: TestClient, generate_auth_header + ) -> 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.delete(url, headers=auth_header) + assert response.status_code == 404 + + def test_delete_messaging_template_by_id__validation_error( + self, + url, + db: Session, + api_client: TestClient, + generate_auth_header, + messaging_template_subject_identity_verification, + ) -> None: + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + response = api_client.delete(url, headers=auth_header) + assert response.status_code == 400 + + def test_delete_messaging_template_by_id_success( + self, + db: Session, + api_client: TestClient, + generate_auth_header, + messaging_template_no_property, + property_a, + ) -> None: + # Creating new config, so we don't run into issues trying to clean up a deleted fixture + 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, + ) + + url = (V1_URL_PREFIX + MESSAGING_TEMPLATE_BY_ID).format( + template_id=messaging_template.id + ) + auth_header = generate_auth_header(scopes=[MESSAGING_TEMPLATE_UPDATE]) + response = api_client.delete(url, headers=auth_header) + assert response.status_code == 204 + + db.expunge_all() + template = ( + db.query(MessagingTemplate).filter_by(id=messaging_template.id).first() + ) + assert template is None diff --git a/tests/ops/service/messaging/test_messaging_crud_service.py b/tests/ops/service/messaging/test_messaging_crud_service.py index cfe0fb19db..716a399ea1 100644 --- a/tests/ops/service/messaging/test_messaging_crud_service.py +++ b/tests/ops/service/messaging/test_messaging_crud_service.py @@ -4,8 +4,8 @@ from sqlalchemy.orm import Session from fides.api.common_exceptions import ( - MessagingConfigNotFoundException, - MessagingConfigValidationException, + EmailTemplateNotFoundException, + MessagingTemplateValidationException, ) from fides.api.models.messaging_template import ( DEFAULT_MESSAGING_TEMPLATES, @@ -15,18 +15,19 @@ from fides.api.schemas.messaging.messaging import ( MessagingActionType, MessagingTemplateWithPropertiesBodyParams, - MessagingTemplateWithPropertiesDetail, + MessagingTemplateDefault, ) from fides.api.service.messaging.messaging_crud_service import ( - create_messaging_template, + update_property_specific_template, + create_property_specific_template_by_type, create_or_update_basic_templates, delete_template_by_id, get_all_basic_messaging_templates, - get_all_messaging_templates_summary, + save_defaults_for_all_messaging_template_types, get_basic_messaging_template_by_type_or_default, get_default_template_by_type, get_template_by_id, - update_messaging_template, + patch_property_specific_template, ) @@ -152,6 +153,58 @@ def test_create_or_update_basic_templates_existing_type_multiple( # with basic templates assert len(template.properties) == 1 + def test_patch_messaging_template_to_disable( + self, + db: Session, + messaging_template_subject_identity_verification, + property_a, + property_b, + ): + update_body = { + # add new property B + "properties": [property_a.id, property_b.id], + "is_enabled": False, + } + + patch_property_specific_template( + db, + messaging_template_subject_identity_verification.id, + update_body, + ) + messaging_template: Optional[MessagingTemplate] = MessagingTemplate.get( + db, object_id=messaging_template_subject_identity_verification.id + ) + assert len(messaging_template.properties) == 2 + + # assert relationship to properties + property_a_db = Property.get(db, object_id=property_a.id) + assert len(property_a_db.messaging_templates) == 1 + property_b_db = Property.get(db, object_id=property_b.id) + assert len(property_b_db.messaging_templates) == 1 + + assert messaging_template.is_enabled is False + + def test_patch_messaging_template_to_enable( + self, + db: Session, + messaging_template_no_property_disabled, + ): + update_body = { + "is_enabled": True, + } + + patch_property_specific_template( + db, + messaging_template_no_property_disabled.id, + update_body, + ) + messaging_template: Optional[MessagingTemplate] = MessagingTemplate.get( + db, object_id=messaging_template_no_property_disabled.id + ) + assert len(messaging_template.properties) == 0 + + assert messaging_template.is_enabled is True + def test_update_messaging_template_add_property( self, db: Session, @@ -169,7 +222,7 @@ def test_update_messaging_template_add_property( "is_enabled": True, } - update_messaging_template( + update_property_specific_template( db, messaging_template_subject_identity_verification.id, MessagingTemplateWithPropertiesBodyParams(**update_body), @@ -202,7 +255,7 @@ def test_update_messaging_template_remove_all_properties( "is_enabled": True, } - update_messaging_template( + update_property_specific_template( db, messaging_template_subject_identity_verification.id, MessagingTemplateWithPropertiesBodyParams(**update_body), @@ -231,8 +284,8 @@ def test_update_messaging_template_id_not_found( "properties": [property_a.id, property_b.id], "is_enabled": True, } - with pytest.raises(MessagingConfigNotFoundException) as exc: - update_messaging_template( + with pytest.raises(EmailTemplateNotFoundException) as exc: + update_property_specific_template( db, "invalid", MessagingTemplateWithPropertiesBodyParams(**update_body) ) @@ -248,7 +301,7 @@ def test_update_messaging_template_property_not_found( "is_enabled": True, } - update_messaging_template( + update_property_specific_template( db, messaging_template_subject_identity_verification.id, MessagingTemplateWithPropertiesBodyParams(**update_body), @@ -281,7 +334,7 @@ def test_update_messaging_template_conflicting_template( "is_enabled": True, } with pytest.raises(Exception) as exc: - update_messaging_template( + update_property_specific_template( db, messaging_template_no_property.id, MessagingTemplateWithPropertiesBodyParams(**update_body), @@ -303,7 +356,7 @@ def test_update_messaging_template_conflicting_template_but_one_disabled( "properties": [property_a.id], "is_enabled": False, } - update_messaging_template( + update_property_specific_template( db, messaging_template_no_property.id, MessagingTemplateWithPropertiesBodyParams(**update_body), @@ -329,7 +382,7 @@ def test_create_messaging_template(self, db: Session, property_a): "is_enabled": True, } - created_template = create_messaging_template( + created_template = create_property_specific_template_by_type( db, template_type, MessagingTemplateWithPropertiesBodyParams(**create_body) ) messaging_template: Optional[MessagingTemplate] = MessagingTemplate.get( @@ -352,7 +405,7 @@ def test_create_messaging_template_no_properties(self, db: Session): "is_enabled": True, } - created_template = create_messaging_template( + created_template = create_property_specific_template_by_type( db, template_type, MessagingTemplateWithPropertiesBodyParams(**create_body) ) messaging_template: Optional[MessagingTemplate] = MessagingTemplate.get( @@ -370,8 +423,8 @@ def test_create_messaging_template_invalid_type(self, db: Session, property_a): "properties": [property_a.id], "is_enabled": True, } - with pytest.raises(MessagingConfigValidationException) as exc: - create_messaging_template( + with pytest.raises(MessagingTemplateValidationException) as exc: + create_property_specific_template_by_type( db, template_type, create_body, @@ -389,7 +442,7 @@ def test_create_messaging_template_property_not_found( "properties": [property_a.id, "invalid_id"], "is_enabled": True, } - template = create_messaging_template( + template = create_property_specific_template_by_type( db, template_type, MessagingTemplateWithPropertiesBodyParams(**create_body), @@ -419,7 +472,7 @@ def test_create_messaging_template_conflicting_template_but_one_disabled( "is_enabled": False, } - created_template = create_messaging_template( + created_template = create_property_specific_template_by_type( db, template_type, MessagingTemplateWithPropertiesBodyParams(**create_body) ) messaging_template: Optional[MessagingTemplate] = MessagingTemplate.get( @@ -446,8 +499,8 @@ def test_create_messaging_template_conflicting_template( "properties": [property_a.id], "is_enabled": True, } - with pytest.raises(MessagingConfigValidationException) as exc: - create_messaging_template( + with pytest.raises(MessagingTemplateValidationException) as exc: + create_property_specific_template_by_type( db, template_type, MessagingTemplateWithPropertiesBodyParams(**create_body), @@ -465,14 +518,15 @@ def test_delete_template_by_id( "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_to_delete = MessagingTemplate.create( db=db, - data=MessagingTemplateWithPropertiesDetail( - content=content, - properties=[{"id": property_a.id, "name": property_a.name}], - is_enabled=True, - type=template_type, - ).dict(), + data=data, ) # Delete message template @@ -498,7 +552,7 @@ def test_delete_template_by_id_not_found( messaging_template_no_property, messaging_template_subject_identity_verification, ): - with pytest.raises(MessagingConfigNotFoundException) as exc: + with pytest.raises(EmailTemplateNotFoundException) as exc: delete_template_by_id(db, template_id="not_there") messaging_template: Optional[MessagingTemplate] = MessagingTemplate.get( db, object_id=messaging_template_subject_identity_verification.id @@ -508,7 +562,7 @@ def test_delete_template_by_id_not_found( def test_delete_template_by_id_cannot_delete_only_type( self, db: Session, messaging_template_subject_identity_verification ): - with pytest.raises(MessagingConfigValidationException) as exc: + with pytest.raises(MessagingTemplateValidationException) as exc: delete_template_by_id( db, template_id=messaging_template_subject_identity_verification.id ) @@ -520,7 +574,7 @@ def test_delete_template_by_id_cannot_delete_only_type( def test_get_template_by_id( self, db: Session, messaging_template_subject_identity_verification ): - template = get_template_by_id( + template: MessagingTemplate = get_template_by_id( db, template_id=messaging_template_subject_identity_verification.id ) assert template.type == MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value @@ -529,40 +583,39 @@ def test_get_template_by_id( assert template.is_enabled is True def test_get_template_by_id_not_found(self, db: Session): - with pytest.raises(MessagingConfigNotFoundException) as exc: + with pytest.raises(EmailTemplateNotFoundException) as exc: get_template_by_id(db, template_id="not_there") def test_get_default_template_by_type(self, db: Session): - default: MessagingTemplateWithPropertiesDetail = get_default_template_by_type( + default: MessagingTemplateDefault = get_default_template_by_type( MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value ) assert default.is_enabled is False - assert len(default.properties) == 0 - assert default.id is None assert default.type is MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value assert default.content is not None def test_get_default_template_by_type_invalid(self, db: Session): - with pytest.raises(MessagingConfigValidationException) as exc: + with pytest.raises(MessagingTemplateValidationException) as exc: get_default_template_by_type("invalid_type") - def test_get_all_messaging_templates_summary_no_db_templates(self, db: Session): - summary = get_all_messaging_templates_summary(db) - assert len(summary) == 6 - # id should not exist for templates that do not exist in db - assert summary[0].id is None - assert summary[0].is_enabled is False + def test_save_defaults_for_all_messaging_template_types_no_db_templates( + self, db: Session + ): + save_defaults_for_all_messaging_template_types(db) + all_templates = MessagingTemplate.query(db).all() + assert len(all_templates) == 6 - def test_get_all_messaging_templates_summary_some_db_templates( + def test_save_defaults_for_all_messaging_template_types_some_db_templates( self, db: Session, messaging_template_subject_identity_verification, messaging_template_privacy_request_receipt, ): - summary = get_all_messaging_templates_summary(db) - assert len(summary) == 6 + save_defaults_for_all_messaging_template_types(db) + all_templates = MessagingTemplate.query(db).all() + assert len(all_templates) == 6 - def test_get_all_messaging_templates_summary_all_db_templates( + def test_save_defaults_for_all_messaging_template_types_all_db_templates( self, db: Session, property_a ): content = { @@ -570,16 +623,16 @@ def test_get_all_messaging_templates_summary_all_db_templates( "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=MessagingTemplateWithPropertiesDetail( - content=content, - properties=[{"id": property_a.id, "name": property_a.name}], - is_enabled=True, - type=template_type, - ).dict(), + data=data, ) - summary = get_all_messaging_templates_summary(db) - assert len(summary) == 6 - # id should exist for templates that exist in db - assert summary[0].id + save_defaults_for_all_messaging_template_types(db) + all_templates = MessagingTemplate.query(db).all() + assert len(all_templates) == 6