From 5cb88a9b92fe62f4b05b85bef24ca1f0f641059b Mon Sep 17 00:00:00 2001 From: Corey Carvalho <44616801+coreycarvalho@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:10:05 -0400 Subject: [PATCH 1/6] #1969 - Deploy from main instead of release during CI/CD (#2060) --- .github/scripts/createAndPostTag.js | 10 ++++----- .github/scripts/prData.js | 14 ++++++------- .github/workflows/cd-pipeline.yml | 32 ++--------------------------- 3 files changed, 14 insertions(+), 42 deletions(-) diff --git a/.github/scripts/createAndPostTag.js b/.github/scripts/createAndPostTag.js index 01af02bb32..5495ec02f0 100644 --- a/.github/scripts/createAndPostTag.js +++ b/.github/scripts/createAndPostTag.js @@ -59,14 +59,14 @@ async function createAndPostTag(params) { const previousVersion = await getReleaseVersionValue(github, owner, repo); // Retrieve PR data to decide the new version tag - const { releaseBranchSha, newVersion } = await prData({ + const { mainBranchSha, newVersion } = await prData({ github, context, core, }); - // Create and push the tag using the SHA from releaseBranchSha - await createTag(github, owner, repo, newVersion, releaseBranchSha); + // Create and push the tag using the SHA from mainBranchSha + await createTag(github, owner, repo, newVersion, mainBranchSha); // Update the RELEASE_VERSION repo variable await github.rest.actions.updateRepoVariable({ @@ -86,10 +86,10 @@ async function createAndPostTag(params) { // Construct the summary content const summaryContent = ` ### Successful Tag Creation! -- After merge to the release branch, a tag was created. +- After merge to the main branch, a tag was created. - Previous version was ${previousVersion} - New version is ${newVersion} -- Tag created for version ${newVersion} using the new release branch SHA: ${releaseBranchSha} +- Tag created for version ${newVersion} using the new main branch SHA: ${mainBranchSha} `; // Append the summary to the GitHub step summary file or log it diff --git a/.github/scripts/prData.js b/.github/scripts/prData.js index c2274fb543..2a72b28748 100644 --- a/.github/scripts/prData.js +++ b/.github/scripts/prData.js @@ -18,21 +18,21 @@ async function fetchPullRequests(github, owner, repo, sha) { } /** - * Retrieves the SHA of the release branch's latest commit. + * Retrieves the SHA of the main branch's latest commit. * @param {Object} github - The GitHub client instance. * @param {string} owner - The owner of the GitHub repository. * @param {string} repo - The repository name. - * @returns {Promise} - A promise resolving to the SHA of the latest commit on the release branch. + * @returns {Promise} - A promise resolving to the SHA of the latest commit on the main branch. */ -async function fetchReleaseBranchSha(github, owner, repo) { +async function fetchMainBranchSha(github, owner, repo) { const { data } = await github.rest.repos.getCommit({ owner, repo, - ref: 'heads/release', + ref: 'heads/main', }); if (data && data.sha) { - console.log('The release branch head SHA is: ' + data.sha); + console.log('The main branch head SHA is: ' + data.sha); return data.sha; } else { throw new Error('No SHA found in the response'); @@ -94,7 +94,7 @@ async function prData(params) { try { const pullRequestData = await fetchPullRequests(github, owner, repo, sha); const currentVersion = await getReleaseVersionValue(github, owner, repo); - const releaseBranchSha = await fetchReleaseBranchSha(github, owner, repo); + const mainBranchSha = await fetchMainBranchSha(github, owner, repo); const labels = pullRequestData.data[0].labels; const prNumber = pullRequestData.data[0].number; @@ -106,7 +106,7 @@ async function prData(params) { ); return { - releaseBranchSha, + mainBranchSha, currentVersion, newVersion, label, diff --git a/.github/workflows/cd-pipeline.yml b/.github/workflows/cd-pipeline.yml index e6708a74e5..4e4effad3d 100644 --- a/.github/workflows/cd-pipeline.yml +++ b/.github/workflows/cd-pipeline.yml @@ -65,35 +65,8 @@ jobs: - name: Pause for manual approval run: echo "Deployment paused for manual approval." - merge-to-release: - needs: approval-deploy-staging - # These permissions are needed to write to the release branch - permissions: - contents: write - runs-on: ubuntu-latest - steps: - - name: Checkout release branch - uses: actions/checkout@v4 - with: - ref: release - fetch-depth: 0 - # Fine-grained PAT with contents:write and workflows:write - # scopes - token: ${{ secrets.CD_PAT }} - - - name: Setup git user - # The following is taken from https://github.com/actions/checkout/issues/13 as a common work-around - run: | - git config --global user.name "$(git --no-pager log --format=format:'%an' -n 1)" - git config --global user.email "$(git --no-pager log --format=format:'%ae' -n 1)" - - - name: Merge commit SHA to release - run: | - git merge ${{ github.sha }} --no-squash -X theirs - git push - create-and-post-tag: - needs: merge-to-release + needs: approval-deploy-staging uses: ./.github/workflows/create-and-post-tag.yml secrets: inherit @@ -136,5 +109,4 @@ jobs: with: environment: prod ref: ${{ needs.create-and-post-tag.outputs.newVersion }} - lambdaDeploy: true - + lambdaDeploy: true \ No newline at end of file From a17db840b55c149cdbbd23c9c3d5c97cf8a87602 Mon Sep 17 00:00:00 2001 From: Adam King Date: Thu, 17 Oct 2024 12:27:13 -0500 Subject: [PATCH 2/6] #1406 [VA-IIR] - V3 Profile Upgrade: Feature flag/unused code cleanup (#2055) --- app/celery/contact_information_tasks.py | 54 +- ...ecipient_communication_permissions_task.py | 127 -- app/feature_flags.py | 4 - app/notifications/process_notifications.py | 18 - app/va/va_profile/va_profile_client.py | 231 +--- .../dev/vaec-api-task-definition.json | 4 - .../dev/vaec-celery-task-definition.json | 8 - .../perf/vaec-api-task-definition.json | 4 - .../perf/vaec-celery-task-definition.json | 8 - .../prod/vaec-api-task-definition.json | 4 - .../prod/vaec-celery-task-definition.json | 8 - .../staging/vaec-api-task-definition.json | 4 - .../staging/vaec-celery-task-definition.json | 8 - .../celery/test_contact_information_tasks.py | 67 +- ...ontact_information_tasks_for_profile_v3.py | 381 ------ ...ecipient_communication_permissions_task.py | 317 ----- .../test_process_notifications.py | 27 +- ...st_process_notifications_for_profile_v3.py | 1116 ----------------- .../va/va_profile/test_va_profile_client.py | 475 +++++-- .../test_va_profile_client_for_profile_v3.py | 635 ---------- 20 files changed, 460 insertions(+), 3040 deletions(-) delete mode 100644 app/celery/lookup_recipient_communication_permissions_task.py delete mode 100644 tests/app/celery/test_contact_information_tasks_for_profile_v3.py delete mode 100644 tests/app/celery/test_lookup_recipient_communication_permissions_task.py delete mode 100644 tests/app/notifications/test_process_notifications_for_profile_v3.py delete mode 100644 tests/app/va/va_profile/test_va_profile_client_for_profile_v3.py diff --git a/app/celery/contact_information_tasks.py b/app/celery/contact_information_tasks.py index ab3838ea31..6496603960 100644 --- a/app/celery/contact_information_tasks.py +++ b/app/celery/contact_information_tasks.py @@ -14,7 +14,6 @@ update_notification_status_by_id, ) from app.exceptions import NotificationTechnicalFailureException, NotificationPermanentFailureException -from app.feature_flags import FeatureFlag, is_feature_enabled from app.models import ( NOTIFICATION_PERMANENT_FAILURE, NOTIFICATION_PREFERENCES_DECLINED, @@ -49,8 +48,7 @@ def lookup_contact_info( ): """ Celery task to look up contact information (email/phone number) for a given notification. - If the feature flag, VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, is enabled, - also check for related communication permissions. + Also check for related communication permissions. Args: self (Task): The Celery task instance. @@ -71,50 +69,16 @@ def lookup_contact_info( recipient_identifier = notification.recipient_identifiers[IdentifierType.VA_PROFILE_ID.value] try: - if is_feature_enabled(FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP): - result = get_profile_result(notification, recipient_identifier) - notification.to = result.recipient - if not result.communication_allowed: - handle_communication_not_allowed(notification, recipient_identifier, result.permission_message) - # Otherwise, this communication is allowed. We will update the notification below and continue the chain. - else: - notification.to = get_recipient( - notification.notification_type, - notification_id, - recipient_identifier, - ) + result = get_profile_result(notification, recipient_identifier) + notification.to = result.recipient + if not result.communication_allowed: + handle_communication_not_allowed(notification, recipient_identifier, result.permission_message) + # Otherwise, this communication is allowed. We will update the notification below and continue the chain. dao_update_notification(notification) except Exception as e: handle_lookup_contact_info_exception(self, notification, recipient_identifier, e) -def get_recipient( - notification_type: str, - notification_id: str, - recipient_identifier: RecipientIdentifier, -) -> str: - """ - Retrieve the recipient email or phone number. - - Args: - notification_type (str): The type of recipient info requested. - notification_id (str): The notification ID associated with this request. - recipient_identifier (RecipientIdentifier): The VA profile ID to retrieve the profile for. - - Returns: - str: The recipient email or phone number. - """ - if notification_type == EMAIL_TYPE: - return va_profile_client.get_email(recipient_identifier) - elif notification_type == SMS_TYPE: - return va_profile_client.get_telephone(recipient_identifier) - else: - raise NotImplementedError( - f'The task lookup_contact_info failed for notification {notification_id}. ' - f'{notification_type} is not supported' - ) - - def get_profile_result( notification: Notification, recipient_identifier: RecipientIdentifier, @@ -130,9 +94,9 @@ def get_profile_result( VAProfileResult: The contact info result from VA Profile. """ if notification.notification_type == EMAIL_TYPE: - return va_profile_client.get_email_with_permission(recipient_identifier, notification) + return va_profile_client.get_email(recipient_identifier, notification) elif notification.notification_type == SMS_TYPE: - return va_profile_client.get_telephone_with_permission(recipient_identifier, notification) + return va_profile_client.get_telephone(recipient_identifier, notification) else: raise NotImplementedError( f'The task lookup_contact_info failed for notification {notification.id}. ' @@ -208,6 +172,8 @@ def handle_lookup_contact_info_exception( else: # Means the default_send is True and this does not require an explicit opt-in return None + elif isinstance(e, NotificationPermanentFailureException): + raise e else: current_app.logger.exception(f'Unhandled exception for notification {notification.id}: {e}') raise e diff --git a/app/celery/lookup_recipient_communication_permissions_task.py b/app/celery/lookup_recipient_communication_permissions_task.py deleted file mode 100644 index c53a276950..0000000000 --- a/app/celery/lookup_recipient_communication_permissions_task.py +++ /dev/null @@ -1,127 +0,0 @@ -from typing import Optional -from sqlalchemy.orm.exc import NoResultFound -from app.va.va_profile.exceptions import VAProfileIdNotFoundException -from flask import current_app -from notifications_utils.statsd_decorators import statsd - -from app import notify_celery, va_profile_client -from app.celery.common import can_retry, handle_max_retries_exceeded -from app.celery.exceptions import AutoRetryException -from app.celery.service_callback_tasks import check_and_queue_callback_task -from app.dao.communication_item_dao import get_communication_item -from app.dao.notifications_dao import get_notification_by_id, update_notification_status_by_id -from app.exceptions import NotificationTechnicalFailureException, NotificationPermanentFailureException -from app.models import RecipientIdentifier, NOTIFICATION_PREFERENCES_DECLINED -from app.va.va_profile import VAProfileRetryableException -from app.va.va_profile.exceptions import CommunicationItemNotFoundException -from app.va.identifier import IdentifierType - - -@notify_celery.task( - bind=True, - name='lookup-recipient-communication-permissions', - throws=(AutoRetryException,), - autoretry_for=(AutoRetryException,), - max_retries=2886, - retry_backoff=True, - retry_backoff_max=60, -) -@statsd(namespace='tasks') -def lookup_recipient_communication_permissions( - self, - notification_id: str, -) -> None: - current_app.logger.info('Looking up communication preferences for notification_id: %s', notification_id) - - notification = get_notification_by_id(notification_id) - - try: - va_profile_recipient_identifier = notification.recipient_identifiers[IdentifierType.VA_PROFILE_ID.value] - except KeyError as e: - current_app.logger.info('No VA Profile ID for notification %s.', notification_id) - raise VAProfileIdNotFoundException(f'No VA Profile ID for notification {notification_id}.') from e - - va_profile_id = va_profile_recipient_identifier.id_value - communication_item_id = notification.template.communication_item_id - notification_type = notification.notification_type - - try: - status_reason = recipient_has_given_permission( - self, - IdentifierType.VA_PROFILE_ID.value, - va_profile_id, - notification_id, - notification_type, - communication_item_id, - ) - except NotificationTechnicalFailureException: - check_and_queue_callback_task(notification) - raise - - if status_reason is not None: - # The recipient doesn't grant permission. a.k.a. preferences-declined - - update_notification_status_by_id( - notification_id, NOTIFICATION_PREFERENCES_DECLINED, status_reason=status_reason - ) - message = f'The recipient for notification {notification_id} has declined permission to receive notifications.' - current_app.logger.info(message) - check_and_queue_callback_task(notification) - raise NotificationPermanentFailureException(message) - - -def recipient_has_given_permission( - task, id_type: str, id_value: str, notification_id: str, notification_type: str, communication_item_id: str -) -> Optional[str]: - """ - Return None if the recipient has the permissions. Otherwise, return a string to explain the lack of permission. - """ - - default_send_flag = True - communication_item = None - identifier = RecipientIdentifier(id_type=id_type, id_value=id_value) - - try: - communication_item = get_communication_item(communication_item_id) - except NoResultFound: - current_app.logger.info('No communication item found for notification %s', notification_id) - - if communication_item is None: - # Calling va_profile without a communication item won't return anything. - # Perform default behavior of sending the notification. - return None - - # get default send flag when available - default_send_flag = communication_item.default_send_indicator - - try: - is_allowed = va_profile_client.get_is_communication_allowed( - identifier, communication_item.va_profile_item_id, notification_id, notification_type, default_send_flag - ) - except VAProfileRetryableException as e: - if can_retry(task.request.retries, task.max_retries, notification_id): - current_app.logger.warning( - 'Unable to look up recipient communication permissions for notification: %s', notification_id - ) - raise AutoRetryException('Found VAProfileRetryableException, autoretrying...', e, e.args) - else: - msg = handle_max_retries_exceeded(notification_id, 'lookup_recipient_communication_permissions') - raise NotificationTechnicalFailureException(msg) - except CommunicationItemNotFoundException: - current_app.logger.info( - 'Communication item for recipient %s not found on notification %s', id_value, notification_id - ) - - # return status reason message if message should not be sent - return None if default_send_flag else 'No recipient opt-in found for explicit preference' - - current_app.logger.info( - 'Value of permission for item %s for recipient %s for notification %s: %s', - communication_item.va_profile_item_id, - id_value, - notification_id, - is_allowed, - ) - - # return status reason message if message should not be sent - return None if is_allowed else 'Contact preferences set to false' diff --git a/app/feature_flags.py b/app/feature_flags.py index 844e030bda..626d8cc391 100644 --- a/app/feature_flags.py +++ b/app/feature_flags.py @@ -22,10 +22,6 @@ class FeatureFlag(Enum): V3_ENABLED = 'V3_ENABLED' COMP_AND_PEN_MESSAGES_ENABLED = 'COMP_AND_PEN_MESSAGES_ENABLED' VA_PROFILE_EMAIL_STATUS_ENABLED = 'VA_PROFILE_EMAIL_STATUS_ENABLED' - VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP = ( - 'VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP' - ) - VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS = 'VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS' def accept_recipient_identifiers_enabled(): diff --git a/app/notifications/process_notifications.py b/app/notifications/process_notifications.py index b7bd8d9f5c..19fb08e271 100644 --- a/app/notifications/process_notifications.py +++ b/app/notifications/process_notifications.py @@ -14,7 +14,6 @@ from app import redis_store from app.celery import provider_tasks -from app.celery.lookup_recipient_communication_permissions_task import lookup_recipient_communication_permissions from app.celery.contact_information_tasks import lookup_contact_info from app.celery.lookup_va_profile_id_task import lookup_va_profile_id from app.celery.onsite_notification_tasks import send_va_onsite_notification_task @@ -174,12 +173,6 @@ def send_notification_to_queue( if recipient_id_type != IdentifierType.VA_PROFILE_ID.value: tasks.append(lookup_va_profile_id.si(notification.id).set(queue=QueueNames.LOOKUP_VA_PROFILE_ID)) - tasks.append( - lookup_recipient_communication_permissions.si(str(notification.id)).set( - queue=QueueNames.COMMUNICATION_ITEM_PERMISSIONS - ) - ) - # Including sms_sender_id is necessary so the correct sender can be chosen. # https://docs.celeryq.dev/en/v4.4.7/userguide/canvas.html#immutability deliver_task, queue = _get_delivery_task(notification, research_mode, queue, sms_sender_id) @@ -276,17 +269,6 @@ def send_to_queue_for_recipient_info_based_on_recipient_identifier( ] tasks.append(lookup_contact_info.si(notification.id).set(queue=QueueNames.LOOKUP_CONTACT_INFO)) - - if ( - not is_feature_enabled(FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP) - and communication_item_id - ): - tasks.append( - lookup_recipient_communication_permissions.si(notification.id).set( - queue=QueueNames.COMMUNICATION_ITEM_PERMISSIONS - ), - ) - deliver_task, deliver_queue = _get_delivery_task(notification) tasks.append(deliver_task.si(notification.id).set(queue=deliver_queue)) diff --git a/app/va/va_profile/va_profile_client.py b/app/va/va_profile/va_profile_client.py index e0aa936b26..ff45ad7be3 100644 --- a/app/va/va_profile/va_profile_client.py +++ b/app/va/va_profile/va_profile_client.py @@ -9,7 +9,6 @@ import iso8601 import requests -from app.feature_flags import FeatureFlag, is_feature_enabled from app.va.identifier import OIDS, IdentifierType, transform_to_fhir_format from app.va.va_profile import ( NoContactInfoException, @@ -115,136 +114,51 @@ def get_profile(self, va_profile_id: RecipientIdentifier) -> Profile: response_json: dict = response.json() return response_json.get('profile', {}) - def get_telephone(self, va_profile_id: RecipientIdentifier) -> str: + def get_mobile_telephone_from_contact_info(self, contact_info: ContactInformation) -> Optional[str]: """ - Retrieve the telephone number from the profile information for a given VA profile ID. + Find the most recently created mobile phone number from a veteran's Vet360 contact information Args: - va_profile_id (RecipientIdentifier): The VA profile ID to retrieve the telephone number for. + contact_info (ContactInformation): Contact Information object retrieved from Vet360 API endpoint Returns: - str: The telephone number retrieved from the VA Profile service. + string representation of the most recently created mobile phone number, or None """ - contact_info: ContactInformation = self.get_profile(va_profile_id).get('contactInformation', {}) - self.logger.debug('V3 Profile - Retrieved ContactInformation: %s', contact_info) - telephones: list[Telephone] = contact_info.get(self.PHONE_BIO_TYPE, []) + sorted_telephones = sorted( - [phone for phone in telephones if phone['phoneType'] == PhoneNumberType.MOBILE.value], + [phone for phone in telephones if phone['phoneType'].lower() == PhoneNumberType.MOBILE.value.lower()], key=lambda phone: iso8601.parse_date(phone['createDate']), reverse=True, ) - if sorted_telephones: - if ( - sorted_telephones[0].get('countryCode') - and sorted_telephones[0].get('areaCode') - and sorted_telephones[0].get('phoneNumber') - ): - self.statsd_client.incr('clients.va-profile.get-telephone.success') - # https://en.wikipedia.org/wiki/E.164 format - return f"+{sorted_telephones[0]['countryCode']}{sorted_telephones[0]['areaCode']}{sorted_telephones[0]['phoneNumber']}" - - self.statsd_client.incr('clients.va-profile.get-telephone.failure') - self._raise_no_contact_info_exception(self.PHONE_BIO_TYPE, va_profile_id, contact_info.get(self.TX_AUDIT_ID)) - - def get_email(self, va_profile_id: RecipientIdentifier) -> str: - """ - Retrieve the email address from the profile information for a given VA profile ID. - - Args: - va_profile_id (RecipientIdentifier): The VA profile ID to retrieve the email address for. - - Returns: - str: The email address retrieved from the VA Profile service. - """ - contact_info: ContactInformation = self.get_profile(va_profile_id).get('contactInformation', {}) - sorted_emails = sorted( - contact_info.get(self.EMAIL_BIO_TYPE, []), - key=lambda email: iso8601.parse_date(email['createDate']), - reverse=True, - ) - if sorted_emails: - self.statsd_client.incr('clients.va-profile.get-email.success') - return sorted_emails[0].get('emailAddressText') - - self.statsd_client.incr('clients.va-profile.get-email.failure') - self._raise_no_contact_info_exception(self.EMAIL_BIO_TYPE, va_profile_id, contact_info.get(self.TX_AUDIT_ID)) - - def has_valid_mobile_telephone_classification(self, telephone: Telephone, contact_info: ContactInformation) -> bool: - """ - Args: - telephone (Telephone): telephone entry from ContactInformation object retrieved from Vet360 API endpoint - - Returns: - bool - if AWS-classified telephone is a valid sms recipient (if nonexistent, return True) + if not sorted_telephones: + self.statsd_client.incr('clients.va-profile.get-telephone.failure') + self.statsd_client.incr(f'clients.va-profile.get-{self.PHONE_BIO_TYPE}.no-{self.PHONE_BIO_TYPE}') + raise NoContactInfoException( + f'No {self.PHONE_BIO_TYPE} in response for VA Profile ID {contact_info.get("vaProfileId")}' + f'with AuditId {contact_info.get(self.TX_AUDIT_ID)}' + ) - Raises: - InvalidPhoneNumberException - if AWS-classified telephone is not a valid sms recipient - """ + telephone = sorted_telephones[0] classification = telephone.get('classification', {}) classification_code = classification.get('classificationCode', None) - if classification_code is None: - # fall back, if no phone number classification is present - self.logger.debug( - 'V3 Profile -- No telephone classification present, assuming the number is a valid SMS recipient (VA Profile ID: %s)', - telephone['vaProfileId'], - ) - return True - - if classification_code not in VALID_PHONE_TYPES_FOR_SMS_DELIVERY: + if classification_code is not None and classification_code not in VALID_PHONE_TYPES_FOR_SMS_DELIVERY: self.logger.debug( 'V3 Profile -- Phone classification code of %s is not a valid SMS recipient (VA Profile ID: %s)', classification_code, telephone['vaProfileId'], ) - self._raise_invalid_phone_number_exception(contact_info) - - self.logger.debug( - 'V3 Profile -- Phone classification code of %s is a valid SMS recipient (VA Profile ID: %s)', - classification_code, - telephone['vaProfileId'], - ) - return True - - def get_mobile_telephone_from_contact_info(self, contact_info: ContactInformation) -> Optional[str]: - """ - Find the most recently created mobile phone number from a veteran's Vet360 contact information - - Args: - contact_info (ContactInformation): Contact Information object retrieved from Vet360 API endpoint - - Returns: - string representation of the most recently created mobile phone number, or None - """ - telephones: list[Telephone] = contact_info.get(self.PHONE_BIO_TYPE, []) - - sorted_telephones = sorted( - [phone for phone in telephones if phone['phoneType'] == PhoneNumberType.MOBILE.value], - key=lambda phone: iso8601.parse_date(phone['createDate']), - reverse=True, - ) + self.statsd_client.incr(f'clients.va-profile.get-{self.PHONE_BIO_TYPE}.no-{self.PHONE_BIO_TYPE}') + raise InvalidPhoneNumberException( + f'No valid {self.PHONE_BIO_TYPE} in response for VA Profile ID {contact_info.get("vaProfileId")} ' + f'with AuditId {contact_info.get("txAuditId")}' + ) - if sorted_telephones: - is_mobile = True - if is_feature_enabled(FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS): - self.logger.debug( - 'V3 Profile -- VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS enabled. Checking telephone classification info.' - ) - is_mobile = self.has_valid_mobile_telephone_classification(sorted_telephones[0], contact_info) - else: - self.logger.debug( - 'V3 Profile -- VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS is not enabled. Will not check classification info.' - ) - if ( - is_mobile - and sorted_telephones[0].get('countryCode') - and sorted_telephones[0].get('areaCode') - and sorted_telephones[0].get('phoneNumber') - ): - self.statsd_client.incr('clients.va-profile.get-telephone.success') - return f"+{sorted_telephones[0]['countryCode']}{sorted_telephones[0]['areaCode']}{sorted_telephones[0]['phoneNumber']}" + if telephone.get('countryCode') and telephone.get('areaCode') and telephone.get('phoneNumber'): + self.statsd_client.incr('clients.va-profile.get-telephone.success') + return f"+{telephone['countryCode']}{telephone['areaCode']}{telephone['phoneNumber']}" - def get_telephone_with_permission( + def get_telephone( self, va_profile_id: RecipientIdentifier, notification: Notification, @@ -276,15 +190,9 @@ def get_telephone_with_permission( contact_info: ContactInformation = profile.get('contactInformation', {}) telephone = self.get_mobile_telephone_from_contact_info(contact_info) - if not telephone: - self.statsd_client.incr('clients.va-profile.get-telephone.failure') - self._raise_no_contact_info_exception( - self.PHONE_BIO_TYPE, va_profile_id, contact_info.get(self.TX_AUDIT_ID) - ) - return VAProfileResult(telephone, communication_allowed, permission_message) - def get_email_with_permission( + def get_email( self, va_profile_id: RecipientIdentifier, notification: Notification, @@ -320,68 +228,16 @@ def get_email_with_permission( key=lambda email: iso8601.parse_date(email['createDate']), reverse=True, ) - if sorted_emails: - self.statsd_client.incr('clients.va-profile.get-email.success') - email_result = sorted_emails[0].get('emailAddressText') - return VAProfileResult(email_result, communication_allowed, permission_message) - - self.statsd_client.incr('clients.va-profile.get-email.failure') - self._raise_no_contact_info_exception(self.EMAIL_BIO_TYPE, va_profile_id, contact_info.get(self.TX_AUDIT_ID)) - - def get_is_communication_allowed( - self, - recipient_id: RecipientIdentifier, - communication_item_id: str, - notification_id: str, - notification_type: str, - default_send: bool, - ) -> bool: - """ - Determine if communication is allowed for a given recipient, communication item, and notification type. - - Args: - recipient_id (RecipientIdentifier): The recipient's VA profile ID. - communication_item_id (str): The ID of the communication item. - notification_id (str): The ID of the notification. - notification_type (str): The type of the notification. - - Returns: - bool: True if communication is allowed, False otherwise. - - Raises: - CommunicationItemNotFoundException: If no communication permissions are found for the given parameters. - """ - - communication_permissions: CommunicationPermissions = self.get_profile(recipient_id).get( - 'communicationPermissions', {} - ) - communication_channel = CommunicationChannel(VA_NOTIFY_TO_VA_PROFILE_NOTIFICATION_TYPES[notification_type]) - for perm in communication_permissions: - if ( - perm['communicationChannelId'] == communication_channel.id - and perm['communicationItemId'] == communication_item_id - ): - self.statsd_client.incr('clients.va-profile.get-communication-item-permission.success') - # if default send is true and allowed is false, return false - # if default send is true and allowed is true, return true - # if default send is false, default to what it finds - permission: bool | None = perm['allowed'] - if permission is not None: - return perm['allowed'] - else: - return default_send - - self.logger.debug( - 'V3 Profile -- Recipient %s did not have permission for communication item %s and channel %s for notification %s', - recipient_id, - communication_item_id, - notification_type, - notification_id, - ) + if not sorted_emails: + self.statsd_client.incr('clients.va-profile.get-email.failure') + self.statsd_client.incr(f'clients.va-profile.get-{self.EMAIL_BIO_TYPE}.no-{self.EMAIL_BIO_TYPE}') + raise NoContactInfoException( + f'No {self.EMAIL_BIO_TYPE} in response for VA Profile ID {va_profile_id} ' + f'with AuditId {contact_info.get(self.TX_AUDIT_ID)}' + ) - # TODO 893 - use default communication item settings when that has been implemented - self.statsd_client.incr('clients.va-profile.get-communication-item-permission.no-permissions') - raise CommunicationItemNotFoundException + self.statsd_client.incr('clients.va-profile.get-email.success') + return VAProfileResult(sorted_emails[0].get('emailAddressText'), communication_allowed, permission_message) def get_is_communication_allowed_from_profile( self, @@ -482,24 +338,6 @@ def _handle_exceptions(self, va_profile_id_value: str, error: Exception): raise exception from error - def _raise_no_contact_info_exception( - self, - bio_type: str, - va_profile_id: str, - tx_audit_id: str, - ): - self.statsd_client.incr(f'clients.va-profile.get-{bio_type}.no-{bio_type}') - raise NoContactInfoException( - f'No {bio_type} in response for VA Profile ID {va_profile_id} ' f'with AuditId {tx_audit_id}' - ) - - def _raise_invalid_phone_number_exception(self, contact_info: ContactInformation): - self.statsd_client.incr(f'clients.va-profile.get-{self.PHONE_BIO_TYPE}.no-{self.PHONE_BIO_TYPE}') - raise InvalidPhoneNumberException( - f'No valid {self.PHONE_BIO_TYPE} in response for VA Profile ID {contact_info.get("vaProfileId")} ' - f'with AuditId {contact_info.get("txAuditId")}' - ) - def send_va_profile_email_status(self, notification_data: dict) -> None: """ This method sends notification status data to VA Profile. This is part of our integration to help VA Profile @@ -511,7 +349,6 @@ def send_va_profile_email_status(self, notification_data: dict) -> None: requests.Timeout: if the request to VA Profile times out RequestException: if something unexpected happens when sending the request """ - headers = {'Authorization': f'Bearer {self.va_profile_token}'} url = f'{self.va_profile_url}/contact-information-vanotify/notify/status' diff --git a/cd/application-deployment/dev/vaec-api-task-definition.json b/cd/application-deployment/dev/vaec-api-task-definition.json index 339ed9f996..b92796e730 100644 --- a/cd/application-deployment/dev/vaec-api-task-definition.json +++ b/cd/application-deployment/dev/vaec-api-task-definition.json @@ -222,10 +222,6 @@ { "name": "COMP_AND_PEN_DYNAMODB_NAME", "value": "dev-bip-payment-notification-table" - }, - { - "name": "VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP", - "value": "True" } ], "secrets": [ diff --git a/cd/application-deployment/dev/vaec-celery-task-definition.json b/cd/application-deployment/dev/vaec-celery-task-definition.json index acf60b3389..0fe25d1772 100644 --- a/cd/application-deployment/dev/vaec-celery-task-definition.json +++ b/cd/application-deployment/dev/vaec-celery-task-definition.json @@ -228,14 +228,6 @@ { "name": "COMP_AND_PEN_DYNAMODB_NAME", "value": "dev-bip-payment-notification-table" - }, - { - "name": "VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP", - "value": "True" - }, - { - "name": "VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS", - "value": "True" } ], "secrets": [ diff --git a/cd/application-deployment/perf/vaec-api-task-definition.json b/cd/application-deployment/perf/vaec-api-task-definition.json index 3067e231f6..74ab387a3d 100644 --- a/cd/application-deployment/perf/vaec-api-task-definition.json +++ b/cd/application-deployment/perf/vaec-api-task-definition.json @@ -198,10 +198,6 @@ { "name": "COMP_AND_PEN_DYNAMODB_NAME", "value": "perf-bip-payment-notification-table" - }, - { - "name": "VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP", - "value": "True" } ], "secrets": [ diff --git a/cd/application-deployment/perf/vaec-celery-task-definition.json b/cd/application-deployment/perf/vaec-celery-task-definition.json index 61c3c24e88..da3857fc52 100644 --- a/cd/application-deployment/perf/vaec-celery-task-definition.json +++ b/cd/application-deployment/perf/vaec-celery-task-definition.json @@ -188,14 +188,6 @@ { "name": "COMP_AND_PEN_PERF_TO_NUMBER", "value": "+14254147755" - }, - { - "name": "VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP", - "value": "True" - }, - { - "name": "VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS", - "value": "True" } ], "secrets": [ diff --git a/cd/application-deployment/prod/vaec-api-task-definition.json b/cd/application-deployment/prod/vaec-api-task-definition.json index b679776ef8..0898d56594 100644 --- a/cd/application-deployment/prod/vaec-api-task-definition.json +++ b/cd/application-deployment/prod/vaec-api-task-definition.json @@ -202,10 +202,6 @@ { "name": "COMP_AND_PEN_DYNAMODB_NAME", "value": "prod-bip-payment-notification-table" - }, - { - "name": "VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP", - "value": "True" } ], "secrets": [ diff --git a/cd/application-deployment/prod/vaec-celery-task-definition.json b/cd/application-deployment/prod/vaec-celery-task-definition.json index 5c5f221e66..69ded18322 100644 --- a/cd/application-deployment/prod/vaec-celery-task-definition.json +++ b/cd/application-deployment/prod/vaec-celery-task-definition.json @@ -192,14 +192,6 @@ { "name": "COMP_AND_PEN_DYNAMODB_NAME", "value": "prod-bip-payment-notification-table" - }, - { - "name": "VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP", - "value": "True" - }, - { - "name": "VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS", - "value": "True" } ], "secrets": [ diff --git a/cd/application-deployment/staging/vaec-api-task-definition.json b/cd/application-deployment/staging/vaec-api-task-definition.json index 7587e7042a..c8c8508bea 100644 --- a/cd/application-deployment/staging/vaec-api-task-definition.json +++ b/cd/application-deployment/staging/vaec-api-task-definition.json @@ -218,10 +218,6 @@ { "name": "COMP_AND_PEN_DYNAMODB_NAME", "value": "staging-bip-payment-notification-table" - }, - { - "name": "VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP", - "value": "True" } ], "secrets": [ diff --git a/cd/application-deployment/staging/vaec-celery-task-definition.json b/cd/application-deployment/staging/vaec-celery-task-definition.json index 2cc2bd3bd2..1722bc517c 100644 --- a/cd/application-deployment/staging/vaec-celery-task-definition.json +++ b/cd/application-deployment/staging/vaec-celery-task-definition.json @@ -208,14 +208,6 @@ { "name": "COMP_AND_PEN_DYNAMODB_NAME", "value": "staging-bip-payment-notification-table" - }, - { - "name": "VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP", - "value": "True" - }, - { - "name": "VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS", - "value": "True" } ], "secrets": [ diff --git a/tests/app/celery/test_contact_information_tasks.py b/tests/app/celery/test_contact_information_tasks.py index f19f2ae42c..7437206908 100644 --- a/tests/app/celery/test_contact_information_tasks.py +++ b/tests/app/celery/test_contact_information_tasks.py @@ -5,7 +5,6 @@ from app.celery.common import RETRIES_EXCEEDED from app.celery.contact_information_tasks import lookup_contact_info -from app.celery.lookup_recipient_communication_permissions_task import lookup_recipient_communication_permissions from app.celery.exceptions import AutoRetryException from app.exceptions import NotificationTechnicalFailureException, NotificationPermanentFailureException from app.models import ( @@ -22,8 +21,7 @@ VAProfileNonRetryableException, VAProfileRetryableException, ) -from app.va.va_profile.va_profile_client import CommunicationChannel - +from app.va.va_profile.va_profile_client import VAProfileResult, CommunicationChannel EXAMPLE_VA_PROFILE_ID = '135' notification_id = str(uuid.uuid4()) @@ -39,9 +37,10 @@ def test_should_get_email_address_and_update_notification(client, mocker, sample mocked_get_notification_by_id = mocker.patch( 'app.celery.contact_information_tasks.get_notification_by_id', return_value=notification ) + va_profile_result = VAProfileResult(recipient='test@test.org', communication_allowed=True, permission_message=None) mocked_va_profile_client = mocker.Mock(VAProfileClient) - mocked_va_profile_client.get_email = mocker.Mock(return_value='test@test.org') + mocked_va_profile_client.get_email = mocker.Mock(return_value=va_profile_result) mocker.patch('app.celery.contact_information_tasks.va_profile_client', new=mocked_va_profile_client) mocked_update_notification = mocker.patch('app.celery.contact_information_tasks.dao_update_notification') @@ -49,7 +48,7 @@ def test_should_get_email_address_and_update_notification(client, mocker, sample lookup_contact_info(notification.id) mocked_get_notification_by_id.assert_called() - mocked_va_profile_client.get_email.assert_called_with(mocker.ANY) + mocked_va_profile_client.get_email.assert_called_with(mocker.ANY, notification) recipient_identifier = mocked_va_profile_client.get_email.call_args[0][0] assert isinstance(recipient_identifier, RecipientIdentifier) assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID @@ -67,7 +66,8 @@ def test_should_get_phone_number_and_update_notification(client, mocker, sample_ ) mocked_va_profile_client = mocker.Mock(VAProfileClient) - mocked_va_profile_client.get_telephone = mocker.Mock(return_value='+15555555555') + va_profile_result = VAProfileResult(recipient='+15555555555', communication_allowed=True, permission_message=None) + mocked_va_profile_client.get_telephone = mocker.Mock(return_value=va_profile_result) mocker.patch('app.celery.contact_information_tasks.va_profile_client', new=mocked_va_profile_client) mocked_update_notification = mocker.patch('app.celery.contact_information_tasks.dao_update_notification') @@ -75,7 +75,36 @@ def test_should_get_phone_number_and_update_notification(client, mocker, sample_ lookup_contact_info(notification.id) mocked_get_notification_by_id.assert_called() - mocked_va_profile_client.get_telephone.assert_called_with(mocker.ANY) + mocked_va_profile_client.get_telephone.assert_called_with(mocker.ANY, notification) + recipient_identifier = mocked_va_profile_client.get_telephone.call_args[0][0] + assert isinstance(recipient_identifier, RecipientIdentifier) + assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID + mocked_update_notification.assert_called_with(notification) + assert notification.to == '+15555555555' + + +def test_should_get_phone_number_and_update_notification_with_no_communication_item( + client, mocker, sample_notification +): + notification = sample_notification( + recipient_identifiers=[{'id_type': IdentifierType.VA_PROFILE_ID.value, 'id_value': EXAMPLE_VA_PROFILE_ID}] + ) + notification.template.communication_item_id = None + assert notification.notification_type == SMS_TYPE + mocked_get_notification_by_id = mocker.patch( + 'app.celery.contact_information_tasks.get_notification_by_id', return_value=notification + ) + + mocked_va_profile_client = mocker.Mock(VAProfileClient) + mocked_va_profile_client.get_telephone = mocker.Mock(return_value=VAProfileResult('+15555555555', True, None)) + mocker.patch('app.celery.contact_information_tasks.va_profile_client', new=mocked_va_profile_client) + + mocked_update_notification = mocker.patch('app.celery.contact_information_tasks.dao_update_notification') + + lookup_contact_info(notification.id) + + mocked_get_notification_by_id.assert_called() + mocked_va_profile_client.get_telephone.assert_called_with(mocker.ANY, notification) recipient_identifier = mocked_va_profile_client.get_telephone.call_args[0][0] assert isinstance(recipient_identifier, RecipientIdentifier) assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID @@ -109,7 +138,7 @@ def test_should_not_retry_on_non_retryable_exception(client, mocker, sample_temp with pytest.raises(NotificationPermanentFailureException): lookup_contact_info(notification.id) - mocked_va_profile_client.get_email.assert_called_with(mocker.ANY) + mocked_va_profile_client.get_email.assert_called_with(mocker.ANY, notification) recipient_identifier = mocked_va_profile_client.get_email.call_args[0][0] assert isinstance(recipient_identifier, RecipientIdentifier) assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID @@ -136,7 +165,7 @@ def test_should_retry_on_retryable_exception(client, mocker, sample_template, sa with pytest.raises(AutoRetryException): lookup_contact_info(notification.id) - mocked_va_profile_client.get_email.assert_called_with(mocker.ANY) + mocked_va_profile_client.get_email.assert_called_with(mocker.ANY, notification) recipient_identifier = mocked_va_profile_client.get_email.call_args[0][0] assert isinstance(recipient_identifier, RecipientIdentifier) assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID @@ -171,10 +200,10 @@ def test_lookup_contact_info_should_retry_on_timeout( assert str(exc_info.value.args[1]) == 'Request timed out' if notification_type == SMS_TYPE: - mocked_va_profile_client.get_telephone.assert_called_with(mocker.ANY) + mocked_va_profile_client.get_telephone.assert_called_with(mocker.ANY, notification) recipient_identifier = mocked_va_profile_client.get_telephone.call_args[0][0] else: - mocked_va_profile_client.get_email.assert_called_with(mocker.ANY) + mocked_va_profile_client.get_email.assert_called_with(mocker.ANY, notification) recipient_identifier = mocked_va_profile_client.get_email.call_args[0][0] assert isinstance(recipient_identifier, RecipientIdentifier) @@ -202,7 +231,7 @@ def test_should_update_notification_to_technical_failure_on_max_retries( with pytest.raises(NotificationTechnicalFailureException): lookup_contact_info(notification.id) - mocked_va_profile_client.get_email.assert_called_with(mocker.ANY) + mocked_va_profile_client.get_email.assert_called_with(mocker.ANY, notification) recipient_identifier = mocked_va_profile_client.get_email.call_args[0][0] assert isinstance(recipient_identifier, RecipientIdentifier) assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID @@ -236,7 +265,7 @@ def test_should_update_notification_to_permanent_failure_on_no_contact_info_exce with pytest.raises(NotificationPermanentFailureException): lookup_contact_info(notification.id) - mocked_va_profile_client.get_email.assert_called_with(mocker.ANY) + mocked_va_profile_client.get_email.assert_called_with(mocker.ANY, notification) recipient_identifier = mocked_va_profile_client.get_email.call_args[0][0] assert isinstance(recipient_identifier, RecipientIdentifier) assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID @@ -367,17 +396,17 @@ def test_get_email_or_sms_with_permission_utilizes_default_send( if default_send: # Leaving this logic so it's easier to understand if user_set or user_set is None: - # Implicit + user has not opted out - assert lookup_contact_info(notification.id) is None + # Implicit + user has not opted out - this command will execute and not raise an exception + lookup_contact_info(notification.id) else: # Implicit + user has opted out with pytest.raises(NotificationPermanentFailureException): - lookup_recipient_communication_permissions(notification.id) + lookup_contact_info(notification.id) else: if user_set: - # Explicit + User has opted in - assert lookup_recipient_communication_permissions(notification.id) is None + # Explicit + User has opted in - this command will execute and not raise an exception + lookup_contact_info(notification.id) else: # Explicit + User has not defined opted in with pytest.raises(NotificationPermanentFailureException): - lookup_recipient_communication_permissions(notification.id) + lookup_contact_info(notification.id) diff --git a/tests/app/celery/test_contact_information_tasks_for_profile_v3.py b/tests/app/celery/test_contact_information_tasks_for_profile_v3.py deleted file mode 100644 index c2c282813f..0000000000 --- a/tests/app/celery/test_contact_information_tasks_for_profile_v3.py +++ /dev/null @@ -1,381 +0,0 @@ -import pytest -import uuid -from app.celery.common import RETRIES_EXCEEDED -from app.celery.contact_information_tasks import lookup_contact_info -from app.celery.exceptions import AutoRetryException -from app.exceptions import NotificationTechnicalFailureException, NotificationPermanentFailureException -from app.feature_flags import FeatureFlag -from app.models import ( - EMAIL_TYPE, - NOTIFICATION_TECHNICAL_FAILURE, - NOTIFICATION_PERMANENT_FAILURE, - RecipientIdentifier, - SMS_TYPE, -) -from app.va.identifier import IdentifierType -from app.va.va_profile import ( - NoContactInfoException, - VAProfileClient, - VAProfileNonRetryableException, - VAProfileRetryableException, - VAProfileResult, -) -from requests import Timeout - -from tests.app.factories.feature_flag import mock_feature_flag - -EXAMPLE_VA_PROFILE_ID = '135' -notification_id = str(uuid.uuid4()) - - -def test_should_get_email_address_and_update_notification(client, mocker, sample_template, sample_notification): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template(template_type=EMAIL_TYPE) - notification = sample_notification( - template=template, - recipient_identifiers=[{'id_type': IdentifierType.VA_PROFILE_ID.value, 'id_value': EXAMPLE_VA_PROFILE_ID}], - ) - - mocked_get_notification_by_id = mocker.patch( - 'app.celery.contact_information_tasks.get_notification_by_id', return_value=notification - ) - - mocked_va_profile_client = mocker.Mock(VAProfileClient) - mocked_va_profile_client.get_email_with_permission = mocker.Mock( - return_value=VAProfileResult('test@test.org', True, None) - ) - mocker.patch('app.celery.contact_information_tasks.va_profile_client', new=mocked_va_profile_client) - - mocked_update_notification = mocker.patch('app.celery.contact_information_tasks.dao_update_notification') - - lookup_contact_info(notification.id) - - mocked_get_notification_by_id.assert_called() - mocked_va_profile_client.get_email_with_permission.assert_called_with(mocker.ANY, notification) - recipient_identifier = mocked_va_profile_client.get_email_with_permission.call_args[0][0] - assert isinstance(recipient_identifier, RecipientIdentifier) - assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID - mocked_update_notification.assert_called_with(notification) - assert notification.to == 'test@test.org' - - -def test_should_get_phone_number_and_update_notification(client, mocker, sample_notification): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - notification = sample_notification( - recipient_identifiers=[{'id_type': IdentifierType.VA_PROFILE_ID.value, 'id_value': EXAMPLE_VA_PROFILE_ID}] - ) - assert notification.notification_type == SMS_TYPE - mocked_get_notification_by_id = mocker.patch( - 'app.celery.contact_information_tasks.get_notification_by_id', return_value=notification - ) - - mocked_va_profile_client = mocker.Mock(VAProfileClient) - mocked_va_profile_client.get_telephone_with_permission = mocker.Mock( - return_value=VAProfileResult('+15555555555', True, None) - ) - mocker.patch('app.celery.contact_information_tasks.va_profile_client', new=mocked_va_profile_client) - - mocked_update_notification = mocker.patch('app.celery.contact_information_tasks.dao_update_notification') - - lookup_contact_info(notification.id) - - mocked_get_notification_by_id.assert_called() - mocked_va_profile_client.get_telephone_with_permission.assert_called_with(mocker.ANY, notification) - recipient_identifier = mocked_va_profile_client.get_telephone_with_permission.call_args[0][0] - assert isinstance(recipient_identifier, RecipientIdentifier) - assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID - mocked_update_notification.assert_called_with(notification) - assert notification.to == '+15555555555' - - -def test_should_get_phone_number_and_update_notification_with_no_communication_item( - client, mocker, sample_notification -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - notification = sample_notification( - recipient_identifiers=[{'id_type': IdentifierType.VA_PROFILE_ID.value, 'id_value': EXAMPLE_VA_PROFILE_ID}] - ) - notification.template.communication_item_id = None - assert notification.notification_type == SMS_TYPE - mocked_get_notification_by_id = mocker.patch( - 'app.celery.contact_information_tasks.get_notification_by_id', return_value=notification - ) - - mocked_va_profile_client = mocker.Mock(VAProfileClient) - mocked_va_profile_client.get_telephone_with_permission = mocker.Mock( - return_value=VAProfileResult('+15555555555', True, None) - ) - mocker.patch('app.celery.contact_information_tasks.va_profile_client', new=mocked_va_profile_client) - - mocked_update_notification = mocker.patch('app.celery.contact_information_tasks.dao_update_notification') - - lookup_contact_info(notification.id) - - mocked_get_notification_by_id.assert_called() - mocked_va_profile_client.get_telephone_with_permission.assert_called_with(mocker.ANY, notification) - recipient_identifier = mocked_va_profile_client.get_telephone_with_permission.call_args[0][0] - assert isinstance(recipient_identifier, RecipientIdentifier) - assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID - mocked_update_notification.assert_called_with(notification) - assert notification.to == '+15555555555' - - -def test_should_not_retry_on_non_retryable_exception(client, mocker, sample_template, sample_notification): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template(template_type=EMAIL_TYPE) - notification = sample_notification( - template=template, - recipient_identifiers=[{'id_type': IdentifierType.VA_PROFILE_ID.value, 'id_value': EXAMPLE_VA_PROFILE_ID}], - ) - - mocker.patch('app.celery.contact_information_tasks.get_notification_by_id', return_value=notification) - - mocked_check_and_queue_callback_task = mocker.patch( - 'app.celery.contact_information_tasks.check_and_queue_callback_task', - ) - - mocked_va_profile_client = mocker.Mock(VAProfileClient) - - exception = VAProfileNonRetryableException - mocked_va_profile_client.get_email_with_permission = mocker.Mock(side_effect=exception) - mocker.patch('app.celery.contact_information_tasks.va_profile_client', new=mocked_va_profile_client) - - mocked_update_notification_status_by_id = mocker.patch( - 'app.celery.contact_information_tasks.update_notification_status_by_id' - ) - - with pytest.raises(NotificationPermanentFailureException): - lookup_contact_info(notification.id) - - mocked_va_profile_client.get_email_with_permission.assert_called_with(mocker.ANY, notification) - recipient_identifier = mocked_va_profile_client.get_email_with_permission.call_args[0][0] - assert isinstance(recipient_identifier, RecipientIdentifier) - assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID - - mocked_update_notification_status_by_id.assert_called_with( - notification.id, NOTIFICATION_PERMANENT_FAILURE, status_reason=exception.failure_reason - ) - mocked_check_and_queue_callback_task.assert_called_once_with(notification) - - -@pytest.mark.parametrize('exception_type', (Timeout, VAProfileRetryableException)) -def test_should_retry_on_retryable_exception(client, mocker, sample_template, sample_notification, exception_type): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template(template_type=EMAIL_TYPE) - notification = sample_notification( - template=template, - recipient_identifiers=[{'id_type': IdentifierType.VA_PROFILE_ID.value, 'id_value': EXAMPLE_VA_PROFILE_ID}], - ) - mocker.patch('app.celery.contact_information_tasks.get_notification_by_id', return_value=notification) - - mocked_va_profile_client = mocker.Mock(VAProfileClient) - mocked_va_profile_client.get_email_with_permission = mocker.Mock(side_effect=exception_type('some error')) - mocker.patch('app.celery.contact_information_tasks.va_profile_client', new=mocked_va_profile_client) - - with pytest.raises(AutoRetryException): - lookup_contact_info(notification.id) - - mocked_va_profile_client.get_email_with_permission.assert_called_with(mocker.ANY, notification) - recipient_identifier = mocked_va_profile_client.get_email_with_permission.call_args[0][0] - assert isinstance(recipient_identifier, RecipientIdentifier) - assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID - - -@pytest.mark.parametrize('notification_type', (SMS_TYPE, EMAIL_TYPE)) -def test_lookup_contact_info_should_retry_on_timeout( - client, mocker, sample_template, sample_notification, notification_type -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template(template_type=notification_type) - notification = sample_notification( - template=template, - recipient_identifiers=[{'id_type': IdentifierType.VA_PROFILE_ID.value, 'id_value': EXAMPLE_VA_PROFILE_ID}], - ) - - mocker.patch('app.celery.contact_information_tasks.get_notification_by_id', return_value=notification) - - mocked_va_profile_client = mocker.Mock(VAProfileClient) - - if notification_type == SMS_TYPE: - mocked_va_profile_client.get_telephone_with_permission = mocker.Mock(side_effect=Timeout('Request timed out')) - else: - mocked_va_profile_client.get_email_with_permission = mocker.Mock(side_effect=Timeout('Request timed out')) - - mocker.patch('app.celery.contact_information_tasks.va_profile_client', new=mocked_va_profile_client) - - with pytest.raises(AutoRetryException) as exc_info: - lookup_contact_info(notification.id) - - assert exc_info.value.args[0] == 'Found Timeout, autoretrying...' - assert isinstance(exc_info.value.args[1], Timeout) - assert str(exc_info.value.args[1]) == 'Request timed out' - - if notification_type == SMS_TYPE: - mocked_va_profile_client.get_telephone_with_permission.assert_called_with(mocker.ANY, notification) - recipient_identifier = mocked_va_profile_client.get_telephone_with_permission.call_args[0][0] - else: - mocked_va_profile_client.get_email_with_permission.assert_called_with(mocker.ANY, notification) - recipient_identifier = mocked_va_profile_client.get_email_with_permission.call_args[0][0] - - assert isinstance(recipient_identifier, RecipientIdentifier) - assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID - - -def test_should_update_notification_to_technical_failure_on_max_retries( - client, mocker, sample_template, sample_notification -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template(template_type=EMAIL_TYPE) - notification = sample_notification( - template=template, - recipient_identifiers=[{'id_type': IdentifierType.VA_PROFILE_ID.value, 'id_value': EXAMPLE_VA_PROFILE_ID}], - ) - mocker.patch('app.celery.contact_information_tasks.get_notification_by_id', return_value=notification) - - mocked_va_profile_client = mocker.Mock(VAProfileClient) - mocked_va_profile_client.get_email_with_permission = mocker.Mock(side_effect=VAProfileRetryableException) - mocker.patch('app.celery.contact_information_tasks.va_profile_client', new=mocked_va_profile_client) - mocker.patch('app.celery.contact_information_tasks.can_retry', return_value=False) - mocked_handle_max_retries_exceeded = mocker.patch( - 'app.celery.contact_information_tasks.handle_max_retries_exceeded' - ) - - with pytest.raises(NotificationTechnicalFailureException): - lookup_contact_info(notification.id) - - mocked_va_profile_client.get_email_with_permission.assert_called_with(mocker.ANY, notification) - recipient_identifier = mocked_va_profile_client.get_email_with_permission.call_args[0][0] - assert isinstance(recipient_identifier, RecipientIdentifier) - assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID - - mocked_handle_max_retries_exceeded.assert_called_once() - - -def test_should_update_notification_to_permanent_failure_on_no_contact_info_exception( - client, mocker, sample_template, sample_notification -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template(template_type=EMAIL_TYPE) - notification = sample_notification( - template=template, - recipient_identifiers=[{'id_type': IdentifierType.VA_PROFILE_ID.value, 'id_value': EXAMPLE_VA_PROFILE_ID}], - ) - mocker.patch('app.celery.contact_information_tasks.get_notification_by_id', return_value=notification) - - mocked_va_profile_client = mocker.Mock(VAProfileClient) - exception = NoContactInfoException - mocked_va_profile_client.get_email_with_permission = mocker.Mock(side_effect=exception) - mocker.patch('app.celery.contact_information_tasks.va_profile_client', new=mocked_va_profile_client) - - mocked_check_and_queue_callback_task = mocker.patch( - 'app.celery.contact_information_tasks.check_and_queue_callback_task', - ) - - mocked_update_notification_status_by_id = mocker.patch( - 'app.celery.contact_information_tasks.update_notification_status_by_id' - ) - - with pytest.raises(NotificationPermanentFailureException): - lookup_contact_info(notification.id) - - mocked_va_profile_client.get_email_with_permission.assert_called_with(mocker.ANY, notification) - recipient_identifier = mocked_va_profile_client.get_email_with_permission.call_args[0][0] - assert isinstance(recipient_identifier, RecipientIdentifier) - assert recipient_identifier.id_value == EXAMPLE_VA_PROFILE_ID - - mocked_update_notification_status_by_id.assert_called_with( - notification.id, NOTIFICATION_PERMANENT_FAILURE, status_reason=exception.failure_reason - ) - - mocked_check_and_queue_callback_task.assert_called_once_with(notification) - - -@pytest.mark.parametrize( - 'exception, throws_additional_exception, notification_status, exception_reason', - [ - ( - VAProfileRetryableException, - NotificationTechnicalFailureException, - NOTIFICATION_TECHNICAL_FAILURE, - RETRIES_EXCEEDED, - ), - ( - NoContactInfoException, - NotificationPermanentFailureException, - NOTIFICATION_PERMANENT_FAILURE, - NoContactInfoException.failure_reason, - ), - ( - VAProfileNonRetryableException, - NotificationPermanentFailureException, - NOTIFICATION_PERMANENT_FAILURE, - VAProfileNonRetryableException.failure_reason, - ), - ], -) -def test_exception_sets_failure_reason_if_thrown( - client, - mocker, - sample_template, - sample_notification, - exception, - throws_additional_exception, - notification_status, - exception_reason, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template(template_type=EMAIL_TYPE) - notification = sample_notification( - template=template, - recipient_identifiers=[{'id_type': IdentifierType.VA_PROFILE_ID.value, 'id_value': EXAMPLE_VA_PROFILE_ID}], - ) - mocker.patch('app.celery.contact_information_tasks.get_notification_by_id', return_value=notification) - - mocked_va_profile_client = mocker.Mock(VAProfileClient) - mocked_va_profile_client.get_email_with_permission = mocker.Mock(side_effect=exception) - mocker.patch('app.celery.contact_information_tasks.va_profile_client', new=mocked_va_profile_client) - mocker.patch('app.celery.contact_information_tasks.can_retry', return_value=False) - - mocked_check_and_queue_callback_task = mocker.patch( - 'app.celery.contact_information_tasks.check_and_queue_callback_task', - ) - - if exception_reason == RETRIES_EXCEEDED: - mocker_handle_max_retries_exceeded = mocker.patch( - 'app.celery.contact_information_tasks.handle_max_retries_exceeded' - ) - with pytest.raises(throws_additional_exception): - lookup_contact_info(notification.id) - mocker_handle_max_retries_exceeded.assert_called_once() - else: - mocked_update_notification_status_by_id = mocker.patch( - 'app.celery.contact_information_tasks.update_notification_status_by_id' - ) - if throws_additional_exception: - with pytest.raises(throws_additional_exception): - lookup_contact_info(notification.id) - else: - lookup_contact_info(notification.id) - mocked_update_notification_status_by_id.assert_called_once_with( - notification.id, notification_status, status_reason=exception_reason - ) - - mocked_check_and_queue_callback_task.assert_called_once_with(notification) diff --git a/tests/app/celery/test_lookup_recipient_communication_permissions_task.py b/tests/app/celery/test_lookup_recipient_communication_permissions_task.py deleted file mode 100644 index 2f0a36b584..0000000000 --- a/tests/app/celery/test_lookup_recipient_communication_permissions_task.py +++ /dev/null @@ -1,317 +0,0 @@ -import pytest -from collections import namedtuple -import uuid - -from app.celery.lookup_recipient_communication_permissions_task import ( - lookup_recipient_communication_permissions, - recipient_has_given_permission, -) -from app.exceptions import NotificationTechnicalFailureException, NotificationPermanentFailureException -from app.models import ( - CommunicationItem, - EMAIL_TYPE, - Notification, - NOTIFICATION_PREFERENCES_DECLINED, - RecipientIdentifier, - SMS_TYPE, -) -from app.va.va_profile import VAProfileRetryableException -from app.va.va_profile.exceptions import CommunicationItemNotFoundException -from app.va.va_profile.va_profile_client import VAProfileClient -from app.va.identifier import IdentifierType -from app.feature_flags import FeatureFlag -from tests.app.factories.feature_flag import mock_feature_flag - - -@pytest.fixture -def mock_communication_item(mocker): - mock_communication_item = mocker.Mock() - mock_communication_item.va_profile_item_id = 5 - mock_communication_item.default_send_indicator = True - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.get_communication_item', - return_value=mock_communication_item, - ) - - -def mock_notification_with_vaprofile_id(mocker, notification_type=SMS_TYPE) -> Notification: - id = uuid.uuid4() - Notification = namedtuple('Notification', ['id', 'notification_type', 'template', 'recipient_identifiers']) - MockTemplate = namedtuple('MockTemplate', ['communication_item_id']) - template = MockTemplate(communication_item_id=1) - return Notification( - id=id, - notification_type=notification_type, - template=template, - recipient_identifiers={ - f'{IdentifierType.VA_PROFILE_ID.value}': RecipientIdentifier( - notification_id=id, id_type=IdentifierType.VA_PROFILE_ID.value, id_value='va-profile-id' - ), - }, - ) - - -def mock_notification_without_vaprofile_id(mocker, notification_type=SMS_TYPE) -> Notification: - id = uuid.uuid4() - Notification = namedtuple('Notification', ['id', 'notification_type', 'template', 'recipient_identifiers']) - MockTemplate = namedtuple('MockTemplate', ['communication_item_id']) - template = MockTemplate(communication_item_id=1) - return Notification( - id=id, - notification_type=notification_type, - template=template, - recipient_identifiers={ - f'{IdentifierType.PID.value}': RecipientIdentifier( - notification_id=id, id_type=IdentifierType.PID.value, id_value='pid-id' - ), - }, - ) - - -def test_lookup_recipient_communication_permissions_should_not_update_notification_status_if_recipient_has_permissions( - client, mocker -): - """ - This is the happy path for which no exceptions are raised, the notification has not reached any final state, and - the next Celery task in the chain should execute. - """ - - notification = mock_notification_with_vaprofile_id(mocker) - - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.recipient_has_given_permission', return_value=None - ) - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.get_notification_by_id', return_value=notification - ) - - update_notification = mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.update_notification_status_by_id' - ) - - mocked_check_and_queue_callback_task = mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.check_and_queue_callback_task', - ) - - lookup_recipient_communication_permissions(notification.id) - - update_notification.assert_not_called() - mocked_check_and_queue_callback_task.assert_not_called() - - -def test_lookup_recipient_communication_permissions_updates_notification_status_if_recipient_does_not_have_permissions( - client, mocker -): - notification = mock_notification_with_vaprofile_id(mocker) - - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.recipient_has_given_permission', - return_value='Contact preferences set to false', - ) - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.get_notification_by_id', return_value=notification - ) - - update_notification = mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.update_notification_status_by_id' - ) - - mocked_check_and_queue_callback_task = mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.check_and_queue_callback_task', - ) - - with pytest.raises(NotificationPermanentFailureException): - lookup_recipient_communication_permissions(str(notification.id)) - - update_notification.assert_called_with( - str(notification.id), NOTIFICATION_PREFERENCES_DECLINED, status_reason='Contact preferences set to false' - ) - mocked_check_and_queue_callback_task.assert_called_once() - - -def test_recipient_has_given_permission_should_return_status_message_if_user_denies_permissions( - client, mocker, mock_communication_item -): - mocked_va_profile_client = mocker.Mock(VAProfileClient) - mocked_va_profile_client.get_is_communication_allowed = mocker.Mock(return_value=False) - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.va_profile_client', new=mocked_va_profile_client - ) - - mock_task = mocker.Mock() - permission_message = recipient_has_given_permission( - mock_task, 'VAPROFILEID', '1', 'some-notification-id', SMS_TYPE, 'some-communication-id' - ) - assert permission_message == 'Contact preferences set to false' - - -def test_recipient_has_given_permission_should_return_none_if_user_grants_permissions( - client, mocker, mock_communication_item -): - mocked_va_profile_client = mocker.Mock(VAProfileClient) - mocked_va_profile_client.get_is_communication_allowed = mocker.Mock(return_value=True) - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.va_profile_client', new=mocked_va_profile_client - ) - - mock_task = mocker.Mock() - permission_message = recipient_has_given_permission( - mock_task, 'VAPROFILEID', '1', 'some-notification-id', SMS_TYPE, 'some-communication-id' - ) - assert permission_message is None - - -def test_recipient_has_given_permission_should_return_none_if_user_permissions_not_set_and_no_com_item( - client, mocker, fake_uuid -): - mocked_va_profile_client = mocker.Mock(VAProfileClient) - mocked_va_profile_client.get_is_communication_allowed = mocker.Mock(side_effect=CommunicationItemNotFoundException) - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.va_profile_client', new=mocked_va_profile_client - ) - - mocker.patch('app.celery.lookup_recipient_communication_permissions_task.get_communication_item', return_value=None) - - mock_task = mocker.Mock() - permission_message = recipient_has_given_permission( - mock_task, 'VAPROFILEID', '1', 'some-notification-id', SMS_TYPE, fake_uuid - ) - assert permission_message is None - - -@pytest.mark.parametrize( - 'send_indicator', (True, False), ids=('default_send_indicator is True', 'default_send_indicator is False') -) -def test_recipient_has_given_permission_with_default_send_indicator_and_no_preference_set( - client, mocker, send_indicator: bool -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mocked_va_profile_client = mocker.Mock(VAProfileClient) - mocked_va_profile_client.get_is_communication_allowed = mocker.Mock(side_effect=CommunicationItemNotFoundException) - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.va_profile_client', new=mocked_va_profile_client - ) - - test_communication_item = CommunicationItem( - id=uuid.uuid4(), va_profile_item_id=1, name='name', default_send_indicator=send_indicator - ) - - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.get_communication_item', - return_value=test_communication_item, - ) - - mock_task = mocker.Mock() - permission_message = recipient_has_given_permission( - mock_task, 'VAPROFILEID', '1', 'some-notification-id', SMS_TYPE, 'some-communication-id' - ) - - if send_indicator: - assert permission_message is None - else: - assert permission_message == 'No recipient opt-in found for explicit preference' - - -@pytest.mark.parametrize( - 'send_indicator', (True, False), ids=('default_send_indicator is True', 'default_send_indicator is False') -) -def test_recipient_has_given_permission_max_retries_exceeded(client, mocker, fake_uuid, send_indicator: bool): - test_communication_item = CommunicationItem( - id=uuid.uuid4(), va_profile_item_id=1, name='name', default_send_indicator=send_indicator - ) - - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.get_communication_item', - return_value=test_communication_item, - ) - - mocked_va_profile_client = mocker.Mock(VAProfileClient) - mocked_va_profile_client.get_is_communication_allowed = mocker.Mock(side_effect=VAProfileRetryableException) - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.va_profile_client', new=mocked_va_profile_client - ) - - mocker.patch('app.celery.lookup_recipient_communication_permissions_task.can_retry', return_value=False) - - mocked_max_retries = mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.handle_max_retries_exceeded' - ) - - mock_task = mocker.Mock() - - with pytest.raises(NotificationTechnicalFailureException): - recipient_has_given_permission(mock_task, 'VAPROFILEID', '1', 'some-notification-id', SMS_TYPE, fake_uuid) - - mocked_max_retries.assert_called() - - -def test_lookup_recipient_communication_permissions_max_retries_exceeded(client, mocker): - notification = mock_notification_with_vaprofile_id(mocker) - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.get_notification_by_id', return_value=notification - ) - - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.recipient_has_given_permission', - side_effect=NotificationTechnicalFailureException, - ) - - mocked_check_and_queue_callback_task = mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.check_and_queue_callback_task', - ) - - with pytest.raises(NotificationTechnicalFailureException): - lookup_recipient_communication_permissions(notification.id) - - mocked_check_and_queue_callback_task.assert_called_once() - - -@pytest.mark.parametrize('notification_type', (SMS_TYPE, EMAIL_TYPE)) -def test_recipient_has_given_permission_is_called_with_va_profile_id(client, mocker, notification_type): - notification = mock_notification_with_vaprofile_id(mocker, notification_type) - - mock = mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.recipient_has_given_permission', return_value=None - ) - mocker.patch('app.celery.lookup_recipient_communication_permissions_task.update_notification_status_by_id') - - non_va_profile_id_type = IdentifierType.PID.value - non_va_profile_id_value = 'pid-id' - - notification.recipient_identifiers[f'{non_va_profile_id_type}'] = RecipientIdentifier( - notification_id=notification.id, id_type=non_va_profile_id_type, id_value=non_va_profile_id_value - ) - - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.get_notification_by_id', return_value=notification - ) - - mocked_check_and_queue_callback_task = mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.check_and_queue_callback_task', - ) - - lookup_recipient_communication_permissions(str(notification.id)) - - id_value = notification.recipient_identifiers[IdentifierType.VA_PROFILE_ID.value].id_value - - mock.assert_called_once_with( - lookup_recipient_communication_permissions, - IdentifierType.VA_PROFILE_ID.value, - id_value, - str(notification.id), - notification_type, - notification.template.communication_item_id, - ) - mocked_check_and_queue_callback_task.assert_not_called() - - -def test_lookup_recipient_communication_permissions_raises_exception_with_non_va_profile_id(client, mocker): - notification = mock_notification_without_vaprofile_id(mocker) - - mocker.patch( - 'app.celery.lookup_recipient_communication_permissions_task.get_notification_by_id', return_value=notification - ) - - with pytest.raises(Exception): - lookup_recipient_communication_permissions(str(notification.id)) diff --git a/tests/app/notifications/test_process_notifications.py b/tests/app/notifications/test_process_notifications.py index bedb014179..7b73d259a7 100644 --- a/tests/app/notifications/test_process_notifications.py +++ b/tests/app/notifications/test_process_notifications.py @@ -8,7 +8,6 @@ from collections import namedtuple from sqlalchemy import delete, select -from app.celery.lookup_recipient_communication_permissions_task import lookup_recipient_communication_permissions from app.celery.contact_information_tasks import lookup_contact_info from app.celery.lookup_va_profile_id_task import lookup_va_profile_id from app.celery.onsite_notification_tasks import send_va_onsite_notification_task @@ -395,7 +394,7 @@ def test_send_notification_to_queue_with_no_recipient_identifiers( 'notify-internal-tasks', IdentifierType.VA_PROFILE_ID.value, 'some va profile id', - [lookup_recipient_communication_permissions, deliver_sms], + [deliver_sms], ), ( True, @@ -405,7 +404,7 @@ def test_send_notification_to_queue_with_no_recipient_identifiers( 'notify-internal-tasks', IdentifierType.PID.value, 'some pid', - [lookup_va_profile_id, lookup_recipient_communication_permissions, deliver_email], + [lookup_va_profile_id, deliver_email], ), ( True, @@ -415,7 +414,7 @@ def test_send_notification_to_queue_with_no_recipient_identifiers( 'notify-internal-tasks', IdentifierType.ICN.value, 'some icn', - [lookup_va_profile_id, lookup_recipient_communication_permissions, deliver_email], + [lookup_va_profile_id, deliver_email], ), ( True, @@ -425,7 +424,7 @@ def test_send_notification_to_queue_with_no_recipient_identifiers( 'notify-internal-tasks', IdentifierType.VA_PROFILE_ID.value, 'some va profile id', - [lookup_recipient_communication_permissions, deliver_email], + [deliver_email], ), ( False, @@ -435,7 +434,7 @@ def test_send_notification_to_queue_with_no_recipient_identifiers( 'send-sms-tasks', IdentifierType.PID.value, 'some pid', - [lookup_va_profile_id, lookup_recipient_communication_permissions, deliver_sms], + [lookup_va_profile_id, deliver_sms], ), ( False, @@ -445,7 +444,7 @@ def test_send_notification_to_queue_with_no_recipient_identifiers( 'send-email-tasks', IdentifierType.ICN.value, 'some icn', - [lookup_va_profile_id, lookup_recipient_communication_permissions, deliver_email], + [lookup_va_profile_id, deliver_email], ), ( False, @@ -455,7 +454,7 @@ def test_send_notification_to_queue_with_no_recipient_identifiers( 'send-sms-tasks', IdentifierType.VA_PROFILE_ID.value, 'some va profile id', - [lookup_recipient_communication_permissions, deliver_sms], + [deliver_sms], ), ( False, @@ -465,7 +464,7 @@ def test_send_notification_to_queue_with_no_recipient_identifiers( 'notify-internal-tasks', IdentifierType.PID.value, 'some pid', - [lookup_va_profile_id, lookup_recipient_communication_permissions, deliver_sms], + [lookup_va_profile_id, deliver_sms], ), ( False, @@ -475,7 +474,7 @@ def test_send_notification_to_queue_with_no_recipient_identifiers( 'notify-internal-tasks', IdentifierType.ICN.value, 'some icn', - [lookup_va_profile_id, lookup_recipient_communication_permissions, deliver_sms], + [lookup_va_profile_id, deliver_sms], ), ( False, @@ -485,7 +484,7 @@ def test_send_notification_to_queue_with_no_recipient_identifiers( 'notify-internal-tasks', IdentifierType.VA_PROFILE_ID.value, 'some va profile id', - [lookup_recipient_communication_permissions, deliver_email], + [deliver_email], ), ( False, @@ -495,7 +494,7 @@ def test_send_notification_to_queue_with_no_recipient_identifiers( 'notify-internal-tasks', IdentifierType.PID.value, 'some pid', - [lookup_va_profile_id, lookup_recipient_communication_permissions, deliver_sms], + [lookup_va_profile_id, deliver_sms], ), ], ) @@ -937,7 +936,6 @@ def test_persist_notification_should_not_persist_recipient_identifier_if_none_pr [ send_va_onsite_notification_task, lookup_contact_info, - lookup_recipient_communication_permissions, deliver_email, ], ), @@ -947,7 +945,6 @@ def test_persist_notification_should_not_persist_recipient_identifier_if_none_pr [ send_va_onsite_notification_task, lookup_contact_info, - lookup_recipient_communication_permissions, deliver_sms, ], ), @@ -958,7 +955,6 @@ def test_persist_notification_should_not_persist_recipient_identifier_if_none_pr lookup_va_profile_id, send_va_onsite_notification_task, lookup_contact_info, - lookup_recipient_communication_permissions, deliver_email, ], ), @@ -969,7 +965,6 @@ def test_persist_notification_should_not_persist_recipient_identifier_if_none_pr lookup_va_profile_id, send_va_onsite_notification_task, lookup_contact_info, - lookup_recipient_communication_permissions, deliver_sms, ], ), diff --git a/tests/app/notifications/test_process_notifications_for_profile_v3.py b/tests/app/notifications/test_process_notifications_for_profile_v3.py deleted file mode 100644 index 4e1f9f4f76..0000000000 --- a/tests/app/notifications/test_process_notifications_for_profile_v3.py +++ /dev/null @@ -1,1116 +0,0 @@ -import datetime -import uuid -from collections import namedtuple - -import pytest -from boto3.exceptions import Boto3Error -from freezegun import freeze_time -from sqlalchemy import delete, select -from sqlalchemy.exc import SQLAlchemyError - -from app.celery.contact_information_tasks import lookup_contact_info -from app.celery.lookup_recipient_communication_permissions_task import lookup_recipient_communication_permissions -from app.celery.lookup_va_profile_id_task import lookup_va_profile_id -from app.celery.onsite_notification_tasks import send_va_onsite_notification_task -from app.celery.provider_tasks import deliver_email, deliver_sms -from app.feature_flags import FeatureFlag -from app.models import ( - EMAIL_TYPE, - KEY_TYPE_TEST, - LETTER_TYPE, - NOTIFICATION_CREATED, - Notification, - RecipientIdentifier, - ScheduledNotification, - SMS_TYPE, - Template, -) -from app.notifications.process_notifications import ( - create_content_for_notification, - persist_notification, - persist_scheduled_notification, - send_notification_to_queue, - send_to_queue_for_recipient_info_based_on_recipient_identifier, - simulated_recipient, -) -from app.va.identifier import IdentifierType -from app.v2.errors import BadRequestError -from notifications_utils.recipients import validate_and_format_email_address, validate_and_format_phone_number - -from tests.app.factories.feature_flag import mock_feature_flag - - -def test_create_content_for_notification_passes(notify_db_session, sample_template, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template(template_type=EMAIL_TYPE) - db_template = notify_db_session.session.get(Template, template.id) - - content = create_content_for_notification(db_template, None) - assert str(content) == template.content - - -def test_create_content_for_notification_with_placeholders_passes(notify_db_session, sample_template, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template(content='Hello ((name))') - db_template = notify_db_session.session.get(Template, template.id) - - content = create_content_for_notification(db_template, {'name': 'Bobby'}) - assert content.content == template.content - assert 'Bobby' in str(content) - - -def test_create_content_for_notification_fails_with_missing_personalisation(notify_db_session, sample_template, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template(content='Hello ((name))\n((Additional placeholder))') - db_template = notify_db_session.session.get(Template, template.id) - - with pytest.raises(BadRequestError): - create_content_for_notification(db_template, None) - - -def test_create_content_for_notification_allows_additional_personalisation(notify_db_session, sample_template, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template(content='Hello ((name))\n((Additional placeholder))') - db_template = notify_db_session.session.get(Template, template.id) - - create_content_for_notification(db_template, {'name': 'Bobby', 'Additional placeholder': 'Data'}) - - -@pytest.mark.serial -@freeze_time('2016-01-01 11:09:00.061258') -def test_persist_notification_creates_and_save_to_db( - notify_db_session, - sample_api_key, - sample_template, - mocker, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - mocked_redis = mocker.patch('app.notifications.process_notifications.redis_store.get') - - template = sample_template() - api_key = sample_api_key(template.service) - - data = { - 'template_id': template.id, - 'notification_id': uuid.uuid4(), - 'created_at': datetime.datetime.utcnow(), - 'reference': str(uuid.uuid4()), - 'billing_code': str(uuid.uuid4()), - 'recipient': '+16502532222', - 'notification_type': SMS_TYPE, - 'api_key_id': api_key.id, - 'key_type': api_key.key_type, - 'reply_to_text': template.service.get_default_sms_sender(), - 'service_id': template.service.id, - 'template_version': template.version, - 'personalisation': {}, - } - - # Intermittently makes the status 'technical-failure' - # Cleaned by the template cleanup - persist_notification(**data) - - db_notification = notify_db_session.session.get(Notification, data['notification_id']) - - assert db_notification.id == data['notification_id'] - assert db_notification.template_id == data['template_id'] - assert db_notification.template_version == data['template_version'] - assert db_notification.api_key_id == data['api_key_id'] - assert db_notification.key_type == data['key_type'] - assert db_notification.notification_type == data['notification_type'] - assert db_notification.created_at == data['created_at'] - assert db_notification.reference == data['reference'] - assert db_notification.reply_to_text == data['reply_to_text'] - assert db_notification.billing_code == data['billing_code'] - assert db_notification.status == NOTIFICATION_CREATED - assert db_notification.billable_units == 0 - assert db_notification.updated_at is None - assert db_notification.created_by_id is None - assert db_notification.client_reference is None - assert not db_notification.sent_at - - mocked_redis.assert_called_once_with(str(template.service_id) + '-2016-01-01-count') - - -def test_persist_notification_throws_exception_when_missing_template(sample_api_key, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - api_key = sample_api_key() - notification = None - - with pytest.raises(SQLAlchemyError): - notification = persist_notification( - template_id=None, - template_version=None, - recipient='+16502532222', - service_id=api_key.service.id, - personalisation=None, - notification_type=SMS_TYPE, - api_key_id=api_key.id, - key_type=api_key.key_type, - ) - - assert notification is None - - -def test_cache_is_not_incremented_on_failure_to_persist_notification( - sample_api_key, - mocker, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - api_key = sample_api_key() - mocked_redis = mocker.patch('app.redis_store.get') - mock_service_template_cache = mocker.patch('app.redis_store.get_all_from_hash') - with pytest.raises(SQLAlchemyError): - persist_notification( - template_id=None, - template_version=None, - recipient='+16502532222', - service_id=api_key.service.id, - personalisation=None, - notification_type=SMS_TYPE, - api_key_id=api_key.id, - key_type=api_key.key_type, - ) - mocked_redis.assert_not_called() - mock_service_template_cache.assert_not_called() - - -def test_persist_notification_does_not_increment_cache_if_test_key( - notify_db_session, - sample_api_key, - sample_template, - mocker, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template() - api_key = sample_api_key(service=template.service, key_type=KEY_TYPE_TEST) - - mocker.patch('app.notifications.process_notifications.redis_store.get', return_value='cache') - mocker.patch('app.notifications.process_notifications.redis_store.get_all_from_hash', return_value='cache') - daily_limit_cache = mocker.patch('app.notifications.process_notifications.redis_store.incr') - template_usage_cache = mocker.patch('app.notifications.process_notifications.redis_store.increment_hash_value') - - notification_id = uuid.uuid4() - - # Cleaned by the template cleanup - persist_notification( - template_id=template.id, - template_version=template.version, - recipient='+16502532222', - service_id=template.service.id, - personalisation={}, - notification_type=SMS_TYPE, - api_key_id=api_key.id, - key_type=api_key.key_type, - reference=str(uuid.uuid4()), - notification_id=notification_id, - ) - - assert notify_db_session.session.get(Notification, notification_id) - assert not daily_limit_cache.called - assert not template_usage_cache.called - - -@freeze_time('2016-01-01 11:09:00.061258') -def test_persist_notification_with_optionals( - notify_db_session, - sample_api_key, - sample_template, - mocker, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - api_key = sample_api_key() - template = sample_template(service=api_key.service) - service = api_key.service - mocked_redis = mocker.patch('app.notifications.process_notifications.redis_store.get') - notification_id = uuid.uuid4() - created_at = datetime.datetime(2016, 11, 11, 16, 8, 18) - - # Cleaned by the template cleanup - persist_notification( - template_id=template.id, - template_version=template.version, - recipient='+16502532222', - service_id=service.id, - personalisation=None, - notification_type=SMS_TYPE, - api_key_id=api_key.id, - key_type=api_key.key_type, - created_at=created_at, - client_reference='ref from client', - notification_id=notification_id, - created_by_id=api_key.created_by_id, - ) - - persisted_notification = notify_db_session.session.get(Notification, notification_id) - - assert persisted_notification.id == notification_id - assert persisted_notification.created_at == created_at - mocked_redis.assert_called_once_with(str(service.id) + '-2016-01-01-count') - assert persisted_notification.client_reference == 'ref from client' - assert persisted_notification.reference is None - assert persisted_notification.international is False - assert persisted_notification.phone_prefix == '1' - assert persisted_notification.rate_multiplier == 1 - assert persisted_notification.created_by_id == api_key.created_by_id - assert not persisted_notification.reply_to_text - - -@freeze_time('2016-01-01 11:09:00.061258') -def test_persist_notification_doesnt_touch_cache_for_old_keys_that_dont_exist( - sample_api_key, - sample_template, - mocker, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - api_key = sample_api_key() - template = sample_template(service=api_key.service) - mock_incr = mocker.patch('app.notifications.process_notifications.redis_store.incr') - mocker.patch('app.notifications.process_notifications.redis_store.get', return_value=None) - mocker.patch('app.notifications.process_notifications.redis_store.get_all_from_hash', return_value=None) - - # Cleaned by the template cleanup - persist_notification( - template_id=template.id, - template_version=template.version, - recipient='+16502532222', - service_id=api_key.service.id, - personalisation={}, - notification_type=SMS_TYPE, - api_key_id=api_key.id, - key_type=api_key.key_type, - reference='ref', - ) - - mock_incr.assert_not_called() - - -@freeze_time('2016-01-01 11:09:00.061258') -def test_persist_notification_increments_cache_if_key_exists( - sample_api_key, - sample_template, - mocker, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - api_key = sample_api_key() - template = sample_template(service=api_key.service) - service = template.service - mock_incr = mocker.patch('app.notifications.process_notifications.redis_store.incr') - mocker.patch('app.notifications.process_notifications.redis_store.get', return_value=1) - mocker.patch('app.notifications.process_notifications.redis_store.get_all_from_hash', return_value={template.id, 1}) - - # Cleaned by the template cleanup - persist_notification( - template_id=template.id, - template_version=template.version, - recipient='+16502532222', - service_id=service.id, - personalisation={}, - notification_type=SMS_TYPE, - api_key_id=api_key.id, - key_type=api_key.key_type, - reference='ref2', - ) - - mock_incr.assert_called_once_with(str(service.id) + '-2016-01-01-count') - - -@pytest.mark.parametrize( - 'research_mode, requested_queue, notification_type, key_type, expected_queue, expected_tasks', - [ - (True, None, SMS_TYPE, 'normal', 'research-mode-tasks', [deliver_sms]), - (True, None, EMAIL_TYPE, 'normal', 'research-mode-tasks', [deliver_email]), - (True, None, EMAIL_TYPE, 'team', 'research-mode-tasks', [deliver_email]), - (False, None, SMS_TYPE, 'normal', 'send-sms-tasks', [deliver_sms]), - (False, None, EMAIL_TYPE, 'normal', 'send-email-tasks', [deliver_email]), - (False, None, SMS_TYPE, 'team', 'send-sms-tasks', [deliver_sms]), - (False, None, SMS_TYPE, 'test', 'research-mode-tasks', [deliver_sms]), - (True, 'notify-internal-tasks', EMAIL_TYPE, 'normal', 'research-mode-tasks', [deliver_email]), - (False, 'notify-internal-tasks', SMS_TYPE, 'normal', 'notify-internal-tasks', [deliver_sms]), - (False, 'notify-internal-tasks', EMAIL_TYPE, 'normal', 'notify-internal-tasks', [deliver_email]), - (False, 'notify-internal-tasks', SMS_TYPE, 'test', 'research-mode-tasks', [deliver_sms]), - ], -) -def test_send_notification_to_queue_with_no_recipient_identifiers( - research_mode, - requested_queue, - notification_type, - key_type, - expected_queue, - expected_tasks, - mocker, - sample_template, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - mocked_chain = mocker.patch('app.notifications.process_notifications.chain') - template = sample_template(template_type=notification_type) - MockService = namedtuple('Service', ['id']) - service = MockService(id=uuid.uuid4()) - - MockSmsSender = namedtuple('ServiceSmsSender', ['service_id', 'sms_sender', 'rate_limit']) - sms_sender = MockSmsSender(service_id=service.id, sms_sender='+18888888888', rate_limit=1) - - NotificationTuple = namedtuple( - 'Notification', ['id', 'key_type', 'notification_type', 'created_at', 'template', 'service_id', 'reply_to_text'] - ) - - mocker.patch( - 'app.notifications.process_notifications.dao_get_service_sms_sender_by_service_id_and_number', return_value=None - ) - - MockSmsSender = namedtuple('ServiceSmsSender', ['service_id', 'sms_sender', 'rate_limit']) - sms_sender = MockSmsSender(service_id=service.id, sms_sender='+18888888888', rate_limit=None) - - notification = NotificationTuple( - id=uuid.uuid4(), - key_type=key_type, - notification_type=notification_type, - created_at=datetime.datetime(2016, 11, 11, 16, 8, 18), - template=template, - service_id=service.id, - reply_to_text=sms_sender.sms_sender, - ) - - send_notification_to_queue(notification=notification, research_mode=research_mode, queue=requested_queue) - - args, _ = mocked_chain.call_args - for called_task, expected_task in zip(args, expected_tasks): - assert called_task.name == expected_task.name - called_task_notification_arg = args[0].args[0] - assert called_task_notification_arg == str(notification.id) - - -@pytest.mark.parametrize( - 'research_mode, requested_queue, notification_type, key_type, expected_queue, ' - 'request_recipient_id_type, request_recipient_id_value, expected_tasks', - [ - ( - True, - None, - SMS_TYPE, - 'normal', - 'research-mode-tasks', - IdentifierType.VA_PROFILE_ID.value, - 'some va profile id', - [lookup_recipient_communication_permissions, deliver_sms], - ), - ( - True, - None, - EMAIL_TYPE, - 'normal', - 'research-mode-tasks', - IdentifierType.PID.value, - 'some pid', - [lookup_va_profile_id, lookup_recipient_communication_permissions, deliver_email], - ), - ( - True, - None, - EMAIL_TYPE, - 'team', - 'research-mode-tasks', - IdentifierType.ICN.value, - 'some icn', - [lookup_va_profile_id, lookup_recipient_communication_permissions, deliver_email], - ), - ( - True, - 'notify-internal-tasks', - EMAIL_TYPE, - 'normal', - 'research-mode-tasks', - IdentifierType.VA_PROFILE_ID.value, - 'some va profile id', - [lookup_recipient_communication_permissions, deliver_email], - ), - ( - False, - None, - SMS_TYPE, - 'normal', - 'send-sms-tasks', - IdentifierType.PID.value, - 'some pid', - [lookup_va_profile_id, lookup_recipient_communication_permissions, deliver_sms], - ), - ( - False, - None, - EMAIL_TYPE, - 'normal', - 'send-email-tasks', - IdentifierType.ICN.value, - 'some icn', - [lookup_va_profile_id, lookup_recipient_communication_permissions, deliver_email], - ), - ( - False, - None, - SMS_TYPE, - 'team', - 'send-sms-tasks', - IdentifierType.VA_PROFILE_ID.value, - 'some va profile id', - [lookup_recipient_communication_permissions, deliver_sms], - ), - ( - False, - None, - SMS_TYPE, - 'test', - 'research-mode-tasks', - IdentifierType.PID.value, - 'some pid', - [lookup_va_profile_id, lookup_recipient_communication_permissions, deliver_sms], - ), - ( - False, - 'notify-internal-tasks', - SMS_TYPE, - 'normal', - 'notify-internal-tasks', - IdentifierType.ICN.value, - 'some icn', - [lookup_va_profile_id, lookup_recipient_communication_permissions, deliver_sms], - ), - ( - False, - 'notify-internal-tasks', - EMAIL_TYPE, - 'normal', - 'notify-internal-tasks', - IdentifierType.VA_PROFILE_ID.value, - 'some va profile id', - [lookup_recipient_communication_permissions, deliver_email], - ), - ( - False, - 'notify-internal-tasks', - SMS_TYPE, - 'test', - 'research-mode-tasks', - IdentifierType.PID.value, - 'some pid', - [lookup_va_profile_id, lookup_recipient_communication_permissions, deliver_sms], - ), - ], -) -def test_send_notification_to_queue_with_recipient_identifiers( - research_mode, - requested_queue, - notification_type, - key_type, - expected_queue, - request_recipient_id_type, - request_recipient_id_value, - expected_tasks, - mocker, - sample_communication_item, - sample_template, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - mock_feature_flag(mocker, FeatureFlag.SMS_SENDER_RATE_LIMIT_ENABLED, 'True') - mocked_chain = mocker.patch('app.notifications.process_notifications.chain') - template = sample_template( - template_type=notification_type, - content='Hello (( Name))\nHere is some HTML & entities' if notification_type == SMS_TYPE else None, - ) - template.service.prefix_sms = notification_type == SMS_TYPE # True only for SMS_TYPE - - MockService = namedtuple('Service', ['id']) - service = MockService(id=uuid.uuid4()) - MockSmsSender = namedtuple('ServiceSmsSender', ['service_id', 'sms_sender', 'rate_limit']) - sms_sender = MockSmsSender(service_id=service.id, sms_sender='+18888888888', rate_limit=None) - - mocker.patch( - 'app.notifications.process_notifications.dao_get_service_sms_sender_by_service_id_and_number', - return_value=sms_sender, - ) - - TestNotification = namedtuple( - 'Notification', - [ - 'id', - 'key_type', - 'notification_type', - 'created_at', - 'template', - 'recipient_identifiers', - 'service_id', - 'reply_to_text', - 'sms_sender', - ], - ) - notification_id = uuid.uuid4() - notification = TestNotification( - id=notification_id, - key_type=key_type, - notification_type=notification_type, - created_at=datetime.datetime(2016, 11, 11, 16, 8, 18), - template=template, - recipient_identifiers={ - f'{request_recipient_id_type}': RecipientIdentifier( - notification_id=notification_id, id_type=request_recipient_id_type, id_value=request_recipient_id_value - ) - }, - service_id=service.id, - reply_to_text=sms_sender.sms_sender, - sms_sender=sms_sender, - ) - - send_notification_to_queue( - notification=notification, - research_mode=research_mode, - queue=requested_queue, - recipient_id_type=request_recipient_id_type, - ) - - args, _ = mocked_chain.call_args - for called_task, expected_task in zip(args, expected_tasks): - assert called_task.name == expected_task.name - - -def test_send_notification_to_queue_throws_exception_deletes_notification( - sample_api_key, - sample_notification, - mocker, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - notification = sample_notification(api_key=sample_api_key()) - mock_feature_flag(mocker, FeatureFlag.SMS_SENDER_RATE_LIMIT_ENABLED, 'False') - mocked_chain = mocker.patch('app.notifications.process_notifications.chain', side_effect=Boto3Error('EXPECTED')) - mocker.patch('app.notifications.process_notifications.dao_get_service_sms_sender_by_service_id_and_number') - with pytest.raises(Boto3Error): - send_notification_to_queue(notification, False) - - args, _ = mocked_chain.call_args - for called_task, expected_task in zip(args, ['send-sms-tasks']): - assert called_task.args[0] == str(notification.id) - assert called_task.options['queue'] == expected_task - - -@pytest.mark.parametrize( - 'to_address, notification_type, expected', - [ - ('+16132532222', 'sms', True), - ('+16132532223', 'sms', True), - ('6132532222', 'sms', True), - ('simulate-delivered@notifications.va.gov', 'email', True), - ('simulate-delivered-2@notifications.va.gov', 'email', True), - ('simulate-delivered-3@notifications.va.gov', 'email', True), - ('6132532225', 'sms', False), - ('valid_email@test.com', 'email', False), - ], -) -def test_simulated_recipient(to_address, notification_type, expected, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - """ - The values where the expected = 'research-mode' are listed in the config['SIMULATED_EMAIL_ADDRESSES'] - and config['SIMULATED_SMS_NUMBERS']. These values should result in using the research mode queue. - SIMULATED_EMAIL_ADDRESSES = ( - 'simulate-delivered@notifications.va.gov', - 'simulate-delivered-2@notifications.va.gov', - 'simulate-delivered-2@notifications.va.gov' - ) - SIMULATED_SMS_NUMBERS = ('6132532222', '+16132532222', '+16132532223') - """ - formatted_address = None - - if notification_type == EMAIL_TYPE: - formatted_address = validate_and_format_email_address(to_address) - else: - formatted_address = validate_and_format_phone_number(to_address) - - is_simulated_address = simulated_recipient(formatted_address, notification_type) - - assert is_simulated_address == expected - - -@pytest.mark.parametrize( - 'recipient, expected_international, expected_prefix, expected_units', - [ - ('6502532222', False, '1', 1), # NA - ('+16502532222', False, '1', 1), # NA - ('+79587714230', True, '7', 1), # Russia - ('+360623400400', True, '36', 3), # Hungary - ], -) -def test_persist_notification_with_international_info_stores_correct_info( - notify_db_session, - sample_api_key, - sample_template, - mocker, - recipient, - expected_international, - expected_prefix, - expected_units, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template() - api_key = sample_api_key(service=template.service) - - notification_id = uuid.uuid4() - - # Cleaned by the template cleanup - persist_notification( - template_id=template.id, - template_version=template.version, - recipient=recipient, - service_id=template.service.id, - personalisation=None, - notification_type=SMS_TYPE, - api_key_id=api_key.id, - key_type=api_key.key_type, - client_reference='ref from client', - notification_id=notification_id, - ) - - persisted_notification = notify_db_session.session.get(Notification, notification_id) - - assert persisted_notification.international is expected_international - assert persisted_notification.phone_prefix == expected_prefix - assert persisted_notification.rate_multiplier == expected_units - - -def test_persist_notification_with_international_info_does_not_store_for_email( - notify_db_session, - sample_api_key, - sample_template, - mocker, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template() - api_key = sample_api_key(service=template.service) - - notification_id = uuid.uuid4() - - # Cleaned by the template cleanup - persist_notification( - template_id=template.id, - template_version=template.version, - recipient='foo@bar.com', - service_id=api_key.service.id, - personalisation=None, - notification_type=EMAIL_TYPE, - api_key_id=api_key.id, - key_type=api_key.key_type, - client_reference='ref from client', - notification_id=notification_id, - ) - - persisted_notification = notify_db_session.session.get(Notification, notification_id) - - assert persisted_notification.international is False - assert persisted_notification.phone_prefix is None - assert persisted_notification.rate_multiplier is None - - -# This test assumes the local timezone is EST -def test_persist_scheduled_notification(notify_db_session, sample_api_key, sample_notification, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - api_key = sample_api_key() - notification = sample_notification(api_key=api_key) - - # Cleaned by the template cleanup - persist_scheduled_notification(notification.id, '2017-05-12 14:15') - stmt = select(ScheduledNotification).where(ScheduledNotification.notification_id == notification.id) - scheduled_notification = notify_db_session.session.scalar(stmt) - - assert scheduled_notification.notification_id == notification.id - assert scheduled_notification.scheduled_for == datetime.datetime(2017, 5, 12, 18, 15) - - -@pytest.mark.parametrize( - 'recipient, expected_recipient_normalised', - [ - ('6502532222', '+16502532222'), - (' 6502532223', '+16502532223'), - ('6502532223', '+16502532223'), - ], -) -def test_persist_sms_notification_stores_normalised_number( - notify_db_session, - sample_api_key, - sample_template, - mocker, - recipient, - expected_recipient_normalised, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template() - api_key = sample_api_key(service=template.service) - - notification_id = uuid.uuid4() - - # Cleaned by the template cleanup - persist_notification( - template_id=template.id, - template_version=template.version, - recipient=recipient, - service_id=api_key.service.id, - personalisation=None, - notification_type=SMS_TYPE, - api_key_id=api_key.id, - key_type=api_key.key_type, - notification_id=notification_id, - ) - - persisted_notification = notify_db_session.session.get(Notification, notification_id) - - assert persisted_notification.to == recipient - assert persisted_notification.normalised_to == expected_recipient_normalised - - -@pytest.mark.parametrize( - 'recipient, expected_recipient_normalised', [('FOO@bar.com', 'foo@bar.com'), ('BAR@foo.com', 'bar@foo.com')] -) -def test_persist_email_notification_stores_normalised_email( - notify_db_session, - sample_api_key, - sample_template, - mocker, - recipient, - expected_recipient_normalised, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - template = sample_template() - api_key = sample_api_key(service=template.service) - - notification_id = uuid.uuid4() - - # Cleaned by the template cleanup - persist_notification( - template_id=template.id, - template_version=template.version, - recipient=recipient, - service_id=api_key.service.id, - personalisation=None, - notification_type=EMAIL_TYPE, - api_key_id=api_key.id, - key_type=api_key.key_type, - notification_id=notification_id, - ) - persisted_notification = notify_db_session.session.get(Notification, notification_id) - - assert persisted_notification.to == recipient - assert persisted_notification.normalised_to == expected_recipient_normalised - - -@pytest.mark.skip(reason='Mislabelled for route removal, fails when unskipped') -def test_persist_notification_with_billable_units_stores_correct_info( - notify_db_session, - mocker, - sample_service, - sample_template, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - service = sample_service(service_permissions=[LETTER_TYPE]) - template = sample_template(service=service, template_type=LETTER_TYPE) - mocker.patch('app.dao.templates_dao.dao_get_template_by_id', return_value=template) - - # Cleaned by the template cleanup - persist_notification( - template_id=template.id, - template_version=template.version, - recipient='123 Main Street', - service_id=template.service.id, - personalisation=None, - notification_type=template.template_type, - api_key_id=None, - key_type='normal', - billable_units=3, - template_postage=template.postage, - ) - - stmt = select(Notification) - persisted_notification = notify_db_session.session.scalars(stmt).all()[0] - - assert persisted_notification.billable_units == 3 - - -@pytest.mark.parametrize( - 'notification_type', - [ - EMAIL_TYPE, - SMS_TYPE, - ], -) -@pytest.mark.parametrize( - 'id_type, id_value', - [ - (IdentifierType.VA_PROFILE_ID.value, 'some va profile id'), - (IdentifierType.PID.value, 'some pid'), - (IdentifierType.ICN.value, 'some icn'), - ], -) -def test_persist_notification_persists_recipient_identifiers( - notify_db_session, - notification_type, - id_type, - id_value, - sample_api_key, - sample_template, - mocker, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - mocker.patch('app.notifications.process_notifications.accept_recipient_identifiers_enabled', return_value=True) - template = sample_template(template_type=notification_type) - api_key = sample_api_key() - recipient_identifier = {'id_type': id_type, 'id_value': id_value} - - notification_id = uuid.uuid4() - # Cleaned by the template cleanup - persist_notification( - template_id=template.id, - template_version=template.version, - service_id=api_key.service.id, - personalisation=None, - notification_type=notification_type, - api_key_id=api_key.id, - key_type=api_key.key_type, - recipient_identifier=recipient_identifier, - notification_id=notification_id, - ) - - recipient_identifier = notify_db_session.session.get(RecipientIdentifier, (notification_id, id_type, id_value)) - - try: - # Persisted correctly - assert recipient_identifier.notification_id == notification_id - assert recipient_identifier.id_type == id_type - assert recipient_identifier.id_value == id_value - finally: - # Teardown - stmt = delete(RecipientIdentifier).where(RecipientIdentifier.notification_id == notification_id) - notify_db_session.session.execute(stmt) - notify_db_session.session.commit() - - -@pytest.mark.parametrize( - 'recipient_identifiers_enabled, recipient_identifier', - [(True, None), (False, {'id_type': IdentifierType.VA_PROFILE_ID.value, 'id_value': 'foo'}), (False, None)], -) -def test_persist_notification_should_not_persist_recipient_identifier_if_none_present_or_toggle_off( - notify_db_session, - recipient_identifiers_enabled, - recipient_identifier, - sample_api_key, - sample_template, - mocker, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - mocker.patch( - 'app.notifications.process_notifications.accept_recipient_identifiers_enabled', - return_value=recipient_identifiers_enabled, - ) - - template = sample_template() - api_key = sample_api_key(template.service) - - # Cleaned by the template cleanup - notification = persist_notification( - template_id=template.id, - template_version=template.version, - service_id=api_key.service.id, - personalisation=None, - notification_type=EMAIL_TYPE, - api_key_id=api_key.id, - key_type=api_key.key_type, - recipient_identifier=recipient_identifier, - ) - - # Persisted correctly - assert notification.recipient_identifiers == {} - - # DB stored correctly - stmt = select(RecipientIdentifier).where(RecipientIdentifier.notification_id == notification.id) - assert notify_db_session.session.scalar(stmt) is None - - -@pytest.mark.parametrize( - 'id_type, notification_type, expected_tasks', - [ - ( - IdentifierType.VA_PROFILE_ID.value, - EMAIL_TYPE, - [ - send_va_onsite_notification_task, - lookup_contact_info, - deliver_email, - ], - ), - ( - IdentifierType.VA_PROFILE_ID.value, - SMS_TYPE, - [ - send_va_onsite_notification_task, - lookup_contact_info, - deliver_sms, - ], - ), - ( - IdentifierType.ICN.value, - EMAIL_TYPE, - [ - lookup_va_profile_id, - send_va_onsite_notification_task, - lookup_contact_info, - deliver_email, - ], - ), - ( - IdentifierType.ICN.value, - SMS_TYPE, - [ - lookup_va_profile_id, - send_va_onsite_notification_task, - lookup_contact_info, - deliver_sms, - ], - ), - ], -) -def test_send_notification_to_correct_queue_to_lookup_contact_info( - client, - mocker, - notification_type, - id_type, - expected_tasks, - sample_template, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - mock_feature_flag(mocker, FeatureFlag.SMS_SENDER_RATE_LIMIT_ENABLED, 'True') - mocked_chain = mocker.patch('app.notifications.process_notifications.chain') - - template = sample_template(template_type=notification_type) - notification_id = str(uuid.uuid4()) - notification = Notification(id=notification_id, notification_type=notification_type, template=template) - mock_template_id = uuid.uuid4() - - send_to_queue_for_recipient_info_based_on_recipient_identifier( - notification, id_type, 'some_id_value', mock_template_id - ) - - args, _ = mocked_chain.call_args - for called_task, expected_task in zip(args, expected_tasks): - assert called_task.name == expected_task.name - - -def test_send_notification_with_sms_sender_rate_limit_uses_rate_limit_delivery_task(client, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - mock_feature_flag(mocker, FeatureFlag.SMS_SENDER_RATE_LIMIT_ENABLED, 'True') - mocked_chain = mocker.patch('app.notifications.process_notifications.chain') - - MockService = namedtuple('Service', ['id']) - service = MockService(id='some service id') - - MockSmsSender = namedtuple('ServiceSmsSender', ['service_id', 'sms_sender', 'rate_limit']) - sms_sender = MockSmsSender(service_id=service.id, sms_sender='+18888888888', rate_limit=2) - - MockTemplate = namedtuple('MockTemplate', ['communication_item_id']) - template = MockTemplate(communication_item_id=1) - - mocker.patch( - 'app.notifications.process_notifications.dao_get_service_sms_sender_by_service_id_and_number', - return_value=sms_sender, - ) - - notification = Notification( - id=str(uuid.uuid4()), - notification_type=SMS_TYPE, - reply_to_text=sms_sender.sms_sender, - service_id=service.id, - template=template, - ) - - send_notification_to_queue(notification, False) - - assert mocked_chain.call_args[0][0].task == 'deliver_sms_with_rate_limiting' - - -def test_send_notification_without_sms_sender_rate_limit_uses_regular_delivery_task(client, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - mocked_chain = mocker.patch('app.notifications.process_notifications.chain') - deliver_sms_with_rate_limiting = mocker.patch( - 'app.celery.provider_tasks.deliver_sms_with_rate_limiting.apply_async' - ) - - MockService = namedtuple('Service', ['id']) - service = MockService(id='some service id') - - MockTemplate = namedtuple('MockTemplate', ['communication_item_id']) - template = MockTemplate(communication_item_id=1) - - MockSmsSender = namedtuple('ServiceSmsSender', ['service_id', 'sms_sender', 'rate_limit']) - sms_sender = MockSmsSender(service_id=service.id, sms_sender='+18888888888', rate_limit=None) - - mocker.patch( - 'app.notifications.process_notifications.dao_get_service_sms_sender_by_service_id_and_number', - return_value=sms_sender, - ) - - notification = Notification( - id=str(uuid.uuid4()), - notification_type=SMS_TYPE, - reply_to_text=sms_sender.sms_sender, - service_id=service.id, - template=template, - ) - - send_notification_to_queue(notification, False) - - assert mocked_chain.call_args[0][0].task == 'deliver_sms' - deliver_sms_with_rate_limiting.assert_not_called() diff --git a/tests/app/va/va_profile/test_va_profile_client.py b/tests/app/va/va_profile/test_va_profile_client.py index 9eb7278576..749caa4324 100644 --- a/tests/app/va/va_profile/test_va_profile_client.py +++ b/tests/app/va/va_profile/test_va_profile_client.py @@ -1,21 +1,52 @@ -import pytest import json +import random +from urllib import parse +from datetime import datetime, timedelta + +import pytest import requests import requests_mock -from urllib import parse -from app.models import EMAIL_TYPE, RecipientIdentifier -from app.va.identifier import IdentifierType, transform_to_fhir_format, OIDS +from app.celery.contact_information_tasks import lookup_contact_info +from app.exceptions import NotificationPermanentFailureException +from app.models import EMAIL_TYPE, SMS_TYPE, RecipientIdentifier +from app.va.identifier import IdentifierType, OIDS, transform_to_fhir_format from app.va.va_profile.exceptions import ( - CommunicationItemNotFoundException, NoContactInfoException, + InvalidPhoneNumberException, VAProfileIDNotFoundException, VAProfileNonRetryableException, VAProfileRetryableException, ) - - -MOCK_VA_PROFILE_URL = 'http://mock.vaprofile.va.gov' +from app.va.va_profile.va_profile_client import CommunicationChannel, VALID_PHONE_TYPES_FOR_SMS_DELIVERY +from app.va.va_profile.va_profile_types import Telephone + +from tests.app.conftest import MOCK_VA_PROFILE_URL + + +def telephone_entry( + create_date=datetime.today(), phone_type='MOBILE', classification_code='0', name_for_debugging='test mobile phone' +): + area_code = random.randint(100, 999) + phone_number = random.randint(100000, 9999999) + return { + 'createDate': f'{create_date}', + 'updateDate': f'{create_date}', + 'txAuditId': '6706b496-d727-401f-8df7-d6fc9adef0e7', + 'sourceSystem': name_for_debugging, + 'sourceDate': '2022-06-09T15:11:58Z', + 'originatingSourceSystem': 'VETSGOV', + 'sourceSystemUser': '1012833438V267437', + 'effectiveStartDate': '2022-06-09T15:11:58Z', + 'vaProfileId': 1550370, + 'telephoneId': 293410, + 'internationalIndicator': False, + 'phoneType': phone_type, + 'countryCode': '1', + 'areaCode': f'{area_code}', + 'phoneNumber': f'{phone_number}', + 'classification': {'classificationCode': classification_code}, + } @pytest.fixture(scope='function') @@ -45,31 +76,61 @@ def url(oid, id_with_aaid): class TestVAProfileClient: - def test_ut_get_email_calls_endpoint_and_returns_email_address( - self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url + def test_get_email_calls_endpoint_and_returns_email_address( + self, + rmock, + mock_va_profile_client, + mock_response, + recipient_identifier, + url, + mocker, + sample_notification, ): rmock.post(url, json=mock_response, status_code=200) - email = mock_va_profile_client.get_email(recipient_identifier) + result = mock_va_profile_client.get_email( + recipient_identifier, + sample_notification(gen_type=EMAIL_TYPE), + ) + email = result.recipient assert email == mock_response['profile']['contactInformation']['emails'][0]['emailAddressText'] assert rmock.called - def test_ut_get_email_raises_NoContactInfoException_if_no_emails_exist( - self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url + def test_get_email_raises_NoContactInfoException_if_no_emails_exist( + self, + rmock, + mock_va_profile_client, + mock_response, + recipient_identifier, + url, + mocker, + sample_notification, ): mock_response['profile']['contactInformation']['emails'] = [] rmock.post(url, json=mock_response, status_code=200) with pytest.raises(NoContactInfoException): - mock_va_profile_client.get_email(recipient_identifier) - - def test_ut_get_profile_calls_correct_url( - self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url, id_with_aaid, oid + mock_va_profile_client.get_email( + recipient_identifier, + sample_notification(gen_type=EMAIL_TYPE), + ) + + def test_get_profile_calls_correct_url( + self, + rmock, + mock_va_profile_client, + mock_response, + recipient_identifier, + url, + id_with_aaid, + oid, + mocker, + sample_notification, ): rmock.post(url, json=mock_response, status_code=200) - mock_va_profile_client.get_email(recipient_identifier) + mock_va_profile_client.get_email(recipient_identifier, sample_notification()) assert rmock.called @@ -78,8 +139,14 @@ def test_ut_get_profile_calls_correct_url( assert rmock.request_history[0].url == expected_url - def test_ut_get_email_raises_exception_when_failed_request( - self, rmock, mock_va_profile_client, recipient_identifier, url + def test_get_email_raises_exception_when_failed_request( + self, + rmock, + mock_va_profile_client, + recipient_identifier, + url, + mocker, + sample_notification, ): response = { 'messages': [ @@ -96,31 +163,85 @@ def test_ut_get_email_raises_exception_when_failed_request( rmock.post(url, json=response, status_code=200) with pytest.raises(VAProfileNonRetryableException): - mock_va_profile_client.get_email(recipient_identifier) - - def test_ut_get_telephone_calls_endpoint_and_returns_phone_number( - self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url + mock_va_profile_client.get_email(recipient_identifier, sample_notification(gen_type=EMAIL_TYPE)) + + def test_get_telephone_calls_endpoint_and_returns_phone_number( + self, + rmock, + mock_va_profile_client, + mock_response, + recipient_identifier, + url, + mocker, + sample_notification, ): rmock.post(url, json=mock_response, status_code=200) - telephone = mock_va_profile_client.get_telephone(recipient_identifier) + result = mock_va_profile_client.get_telephone(recipient_identifier, sample_notification()) + telephone = result.recipient assert telephone is not None assert rmock.called + @pytest.mark.parametrize( + 'classification_code, expected', + [ + *[(code, True) for code in VALID_PHONE_TYPES_FOR_SMS_DELIVERY], + (None, True), # if no classification code exists, fall back to True + (1, False), # LANDLINE + (3, False), # INVALID + (4, False), # OTHER + ], + ) + def test_has_valid_telephone_classification(self, mock_va_profile_client, classification_code, expected): + telephone_instance: Telephone = { + 'createDate': '2023-10-01', + 'updateDate': '2023-10-02', + 'txAuditId': 'TX123456', + 'sourceSystem': 'SystemA', + 'sourceDate': '2023-10-01', + 'originatingSourceSystem': 'SystemB', + 'sourceSystemUser': 'User123', + 'effectiveStartDate': '2023-10-01', + 'vaProfileId': 12345, + 'telephoneId': 67890, + 'internationalIndicator': False, + 'phoneType': 'MOBILE', + 'countryCode': '1', + 'areaCode': '123', + 'phoneNumber': '4567890', + 'classification': {'classificationCode': classification_code, 'classificationName': 'SOME NAME'}, + } + if classification_code is None: + telephone_instance.pop('classification') + + mock_contact_info = {'vaProfileId': 'test', 'txAuditId': '1234', 'telephones': [telephone_instance]} + if expected: + mock_va_profile_client.get_mobile_telephone_from_contact_info(mock_contact_info) + else: + with pytest.raises(InvalidPhoneNumberException): + mock_va_profile_client.get_mobile_telephone_from_contact_info(mock_contact_info) + class TestVAProfileClientExceptionHandling: - def test_ut_get_telephone_raises_NoContactInfoException_if_no_telephones_exist( - self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url + def test_get_telephone_raises_NoContactInfoException_if_no_telephones_exist( + self, + rmock, + mock_va_profile_client, + mock_response, + recipient_identifier, + url, + mocker, + sample_notification, ): mock_response['profile']['contactInformation']['telephones'] = [] rmock.post(url, json=mock_response, status_code=200) with pytest.raises(NoContactInfoException): - mock_va_profile_client.get_telephone(recipient_identifier) + mock_va_profile_client.get_telephone(recipient_identifier, sample_notification()) - def test_ut_get_telephone_raises_NoContactInfoException_if_no_mobile_telephones_exist( - self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url + def test_get_telephone_raises_NoContactInfoException_if_no_mobile_telephones_exist( + self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url, mocker, sample_notification ): telephones = mock_response['profile']['contactInformation']['telephones'] mock_response['profile']['contactInformation']['telephones'] = [ @@ -129,14 +250,56 @@ def test_ut_get_telephone_raises_NoContactInfoException_if_no_mobile_telephones_ rmock.post(url, json=mock_response, status_code=200) with pytest.raises(NoContactInfoException): - mock_va_profile_client.get_telephone(recipient_identifier) + mock_va_profile_client.get_telephone(recipient_identifier, sample_notification()) - def test_ut_handle_exceptions_retryable_exception(self, mock_va_profile_client): + def test_get_telephone_raises_InvalidPhoneNumberException_if_number_classified_as_not_mobile( + self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url, mocker, sample_notification + ): + telephones = mock_response['profile']['contactInformation']['telephones'] + for telephone in telephones: + telephone['classification'] = {'classificationCode': 1} # LANDLINE classification + mock_response['profile']['contactInformation']['telephones'] = telephones + rmock.post(url, json=mock_response, status_code=200) + + with pytest.raises(InvalidPhoneNumberException): + mock_va_profile_client.get_telephone(recipient_identifier, sample_notification()) + + def test_get_telephone_prefers_user_specified_mobile_phone( + self, rmock, mock_va_profile_client, mock_response, mocker, url, recipient_identifier, sample_notification + ): + # A veteran has configured a mobile telephone to receive notifications. They add an additional mobile + # phone, and save it as their "home" phone. Even though it is technically a mobile device and is newer, + # we should send notifications to the device specified by the user + today = datetime.today() + yesterday_morning = (datetime.today() - timedelta(days=1)).replace(hour=6) + yesterday_evening = yesterday_morning.replace(hour=20) + home_phone_created_today = telephone_entry(today, 'HOME', 1, 'home phone created today') + mobile_phone_created_yesterday_morning = telephone_entry( + yesterday_morning, 'MOBILE', 0, 'mobile phone created yesterday morning' + ) + home_phone_with_mobile_classification_created_yesterday_evening = telephone_entry( + yesterday_evening, 'HOME', 0, 'home phone with mobile classification created yesterday evening' + ) + contact_info = mock_response['profile']['contactInformation'] + contact_info['telephones'] = [ + home_phone_created_today, + mobile_phone_created_yesterday_morning, + home_phone_with_mobile_classification_created_yesterday_evening, + ] + + rmock.post(url, json=mock_response, status_code=200) + result = mock_va_profile_client.get_telephone(recipient_identifier, sample_notification()) + assert ( + result.recipient + == f"+{mobile_phone_created_yesterday_morning['countryCode']}{mobile_phone_created_yesterday_morning['areaCode']}{mobile_phone_created_yesterday_morning['phoneNumber']}" + ) + + def test_handle_exceptions_retryable_exception(self, mock_va_profile_client, mocker): # This test checks if VAProfileRetryableException is raised for a RequestException with pytest.raises(VAProfileRetryableException): mock_va_profile_client._handle_exceptions('some_va_profile_id', requests.RequestException()) - def test_ut_handle_exceptions_id_not_found_exception(self, mock_va_profile_client): + def test_handle_exceptions_id_not_found_exception(self, mock_va_profile_client, mocker): # Simulate a 404 HTTP error error = requests.HTTPError(response=requests.Response()) error.response.status_code = 404 @@ -144,15 +307,7 @@ def test_ut_handle_exceptions_id_not_found_exception(self, mock_va_profile_clien with pytest.raises(VAProfileIDNotFoundException): mock_va_profile_client._handle_exceptions('some_va_profile_id', error) - def test_ut_handle_exceptions_non_retryable_exception(self, mock_va_profile_client): - # Simulate a 400 HTTP error - error = requests.HTTPError(response=requests.Response()) - error.response.status_code = 400 - # This test checks if VAProfileNonRetryableException is raised for a 400 error - with pytest.raises(VAProfileNonRetryableException): - mock_va_profile_client._handle_exceptions('some_va_profile_id', error) - - def test_ut_handle_exceptions_timeout_exception(self, mock_va_profile_client): + def test_handle_exceptions_non_retryable_exception(self, mock_va_profile_client, mocker): # This test checks if VAProfileRetryableExcception is raised for a Timeout exception # Timeout inherits from requests.RequestException, so all exceptions of type RequestException should # raise a VAProfileRetryableException @@ -160,111 +315,135 @@ def test_ut_handle_exceptions_timeout_exception(self, mock_va_profile_client): mock_va_profile_client._handle_exceptions('some_va_profile_id', requests.Timeout()) @pytest.mark.parametrize('status', [429, 500]) - @pytest.mark.parametrize( - 'fn, args', - [ - ('get_email', ['recipient_identifier']), - ('get_telephone', ['recipient_identifier']), - ('get_is_communication_allowed', ['recipient_identifier', 1, 2, 'foo', True]), - ], - ) - def test_ut_client_raises_retryable_exception( - self, rmock, mock_va_profile_client, recipient_identifier, status, fn, args + def test_client_raises_retryable_exception( + self, + rmock, + mock_va_profile_client, + recipient_identifier, + status, + mocker, + sample_notification, ): rmock.post(requests_mock.ANY, status_code=status) with pytest.raises(VAProfileRetryableException): - func = getattr(mock_va_profile_client, fn) - # allow us to call `get_is_communication_allowed` though it has a different method signature - prepared_args = [recipient_identifier if arg == 'recipient_identifier' else arg for arg in args] - func(*prepared_args) - - @pytest.mark.parametrize('status', [400, 403, 404]) - @pytest.mark.parametrize( - 'fn, args', - [ - ('get_email', ['recipient_identifier']), - ('get_telephone', ['recipient_identifier']), - ('get_is_communication_allowed', ['recipient_identifier', 1, 2, 'foo', True]), - ], - ) - def test_ut_client_raises_nonretryable_exception( - self, rmock, mock_va_profile_client, recipient_identifier, status, fn, args - ): - rmock.post(requests_mock.ANY, status_code=status) + mock_va_profile_client.get_email(recipient_identifier, sample_notification(gen_type=EMAIL_TYPE)) - with pytest.raises(VAProfileNonRetryableException): - func = getattr(mock_va_profile_client, fn) - # allow us to call `get_is_communication_allowed` though it has a different method signature - prepared_args = [recipient_identifier if arg == 'recipient_identifier' else arg for arg in args] - func(*prepared_args) - - @pytest.mark.parametrize( - 'fn, args', - [ - ('get_email', ['recipient_identifier']), - ('get_telephone', ['recipient_identifier']), - ('get_is_communication_allowed', ['recipient_identifier', 1, 2, 'foo', True]), - ], - ) - def test_ut_client_raises_retryable_exception_when_request_exception_is_thrown( - self, mock_va_profile_client, recipient_identifier, fn, args + with pytest.raises(VAProfileRetryableException): + mock_va_profile_client.get_email(recipient_identifier, sample_notification()) + + def test_client_raises_retryable_exception_when_request_exception_is_thrown( + self, + mock_va_profile_client, + recipient_identifier, + mocker, + sample_notification, ): with requests_mock.Mocker(real_http=True) as rmock: rmock.post(requests_mock.ANY, exc=requests.RequestException) with pytest.raises(VAProfileRetryableException): - func = getattr(mock_va_profile_client, fn) - # allow us to call `get_is_communication_allowed` though it has a different method signature - prepared_args = [recipient_identifier if arg == 'recipient_identifier' else arg for arg in args] - func(*prepared_args) + mock_va_profile_client.get_email(recipient_identifier, sample_notification(gen_type=EMAIL_TYPE)) + + with pytest.raises(VAProfileRetryableException): + mock_va_profile_client.get_email(recipient_identifier, sample_notification()) class TestCommunicationPermissions: @pytest.mark.parametrize('expected', [True, False]) - def test_ut_get_is_communication_allowed_returns_whether_permissions_granted_for_sms_communication( - self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url, expected + def test_get_is_communication_allowed_returns_whether_permissions_granted_for_sms_communication( + self, + rmock, + mock_va_profile_client, + mock_response, + url, + expected, + mocker, + sample_notification, ): + notification = sample_notification() mock_response['profile']['communicationPermissions'][0]['allowed'] = expected - rmock.post(url, json=mock_response, status_code=200) + mock_response['profile']['communicationPermissions'][0]['communicationItemId'] = notification.va_profile_item_id - perm = mock_response['profile']['communicationPermissions'][0] - allowed = mock_va_profile_client.get_is_communication_allowed( - recipient_identifier, perm['communicationItemId'], 'bar', 'sms', expected + allowed = mock_va_profile_client.get_is_communication_allowed_from_profile( + mock_response['profile'], notification, CommunicationChannel.TEXT ) assert allowed is expected - assert rmock.called @pytest.mark.parametrize('expected', [True, False]) - def test_ut_get_is_communication_allowed_returns_whether_permissions_granted_for_email_communication( - self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url, expected + def test_get_is_communication_allowed_returns_whether_permissions_granted_for_email_communication( + self, + rmock, + mock_va_profile_client, + mock_response, + url, + expected, + mocker, + sample_notification, ): + notification = sample_notification(gen_type=EMAIL_TYPE) mock_response['profile']['communicationPermissions'][1]['allowed'] = expected - rmock.post(url, json=mock_response, status_code=200) + mock_response['profile']['communicationPermissions'][1]['communicationItemId'] = notification.va_profile_item_id - perm = mock_response['profile']['communicationPermissions'][1] - allowed = mock_va_profile_client.get_is_communication_allowed( - recipient_identifier, - perm['communicationItemId'], - 'bar', - 'email', - expected, + allowed = mock_va_profile_client.get_is_communication_allowed_from_profile( + mock_response['profile'], notification, CommunicationChannel.EMAIL ) assert allowed is expected - assert rmock.called - def test_ut_get_is_communication_allowed_raises_exception_if_communication_item_id_not_present( - self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url + @pytest.mark.parametrize( + 'default_send, user_set, expected', + [ + # If the user has set a preference, we always go with that and override default_send + [True, True, True], + [True, False, False], + [False, True, True], + [False, False, False], + # If the user has not set a preference, go with the default_send + [True, None, True], + [False, None, False], + ], + ) + @pytest.mark.parametrize('notification_type', [CommunicationChannel.EMAIL, CommunicationChannel.TEXT]) + def test_get_email_or_and_get_telephone_utilizes_default_send( + self, + mock_va_profile_client, + mock_response, + recipient_identifier, + sample_communication_item, + sample_notification, + sample_template, + default_send, + user_set, + expected, + notification_type, + mocker, ): - rmock.post(url, json=mock_response, status_code=200) + profile = mock_response['profile'] + communication_item = sample_communication_item(default_send) + template = sample_template(communication_item_id=communication_item.id) - # no entry exists in the response which has a communicationItemId of 999 - with pytest.raises(CommunicationItemNotFoundException): - mock_va_profile_client.get_is_communication_allowed(recipient_identifier, 999, 'bar', 'email', True) + notification = sample_notification( + template=template, gen_type=EMAIL_TYPE if notification_type == CommunicationChannel.EMAIL else SMS_TYPE + ) - assert rmock.called + if user_set is not None: + profile['communicationPermissions'][0]['allowed'] = user_set + profile['communicationPermissions'][0]['communicationItemId'] = notification.va_profile_item_id + profile['communicationPermissions'][0]['communicationChannelId'] = notification_type.id + else: + profile['communicationPermissions'] = [] + + mocker.patch.object(mock_va_profile_client, 'get_profile', return_value=profile) + + if notification_type == CommunicationChannel.EMAIL: + client_fn = mock_va_profile_client.get_email + else: + client_fn = mock_va_profile_client.get_telephone + + result = client_fn(recipient_identifier, notification) + assert result.communication_allowed == expected class TestSendEmailStatus: @@ -282,7 +461,7 @@ class TestSendEmailStatus: 'provider': 'ses', # email provider } - def test_ut_send_va_profile_email_status_sent_successfully(self, rmock, mock_va_profile_client): + def test_send_va_profile_email_status_sent_successfully(self, rmock, mock_va_profile_client, mocker): rmock.post(requests_mock.ANY, json=self.mock_response, status_code=200) mock_va_profile_client.send_va_profile_email_status(self.mock_notification_data) @@ -292,7 +471,7 @@ def test_ut_send_va_profile_email_status_sent_successfully(self, rmock, mock_va_ expected_url = f'{MOCK_VA_PROFILE_URL}/contact-information-vanotify/notify/status' assert rmock.request_history[0].url == expected_url - def test_ut_send_va_profile_email_status_timeout(self, rmock, mock_va_profile_client): + def test_send_va_profile_email_status_timeout(self, rmock, mock_va_profile_client, mocker): rmock.post(requests_mock.ANY, exc=requests.ReadTimeout) with pytest.raises(requests.Timeout): @@ -303,7 +482,7 @@ def test_ut_send_va_profile_email_status_timeout(self, rmock, mock_va_profile_cl expected_url = f'{MOCK_VA_PROFILE_URL}/contact-information-vanotify/notify/status' assert rmock.request_history[0].url == expected_url - def test_ut_send_va_profile_email_status_throws_exception(self, rmock, mock_va_profile_client): + def test_send_va_profile_email_status_throws_exception(self, rmock, mock_va_profile_client, mocker): rmock.post(requests_mock.ANY, exc=requests.RequestException) with pytest.raises(requests.RequestException): @@ -313,3 +492,63 @@ def test_ut_send_va_profile_email_status_throws_exception(self, rmock, mock_va_p expected_url = f'{MOCK_VA_PROFILE_URL}/contact-information-vanotify/notify/status' assert rmock.request_history[0].url == expected_url + + +@pytest.mark.parametrize( + 'default_send, user_set, expected', + [ + # If the user has set a preference, we always go with that and override default_send + [True, True, True], + [True, False, False], + [False, True, True], + [False, False, False], + # If the user has not set a preference, go with the default_send + [True, None, True], + [False, None, False], + ], +) +@pytest.mark.parametrize('notification_type', [CommunicationChannel.EMAIL, CommunicationChannel.TEXT]) +def test_get_email_and_get_telephone_utilizes_default_send( + mock_va_profile_response, + sample_communication_item, + sample_notification, + sample_template, + default_send, + user_set, + expected, + notification_type, + mocker, +): + # Test each combo, ensuring contact info responds with expected result + channel = EMAIL_TYPE if notification_type == CommunicationChannel.EMAIL else SMS_TYPE + profile = mock_va_profile_response['profile'] + communication_item = sample_communication_item(default_send) + template = sample_template(template_type=channel, communication_item_id=communication_item.id) + notification = sample_notification( + template=template, + gen_type=channel, + recipient_identifiers=[{'id_type': IdentifierType.VA_PROFILE_ID.value, 'id_value': '1234'}], + ) + + profile['communicationPermissions'][0]['allowed'] = user_set + profile['communicationPermissions'][0]['communicationItemId'] = notification.va_profile_item_id + profile['communicationPermissions'][0]['communicationChannelId'] = notification_type.id + + mocker.patch('app.va.va_profile.va_profile_client.VAProfileClient.get_profile', return_value=profile) + + if default_send: + if user_set or user_set is None: + # Implicit + user has not opted out + assert lookup_contact_info(notification.id) is None + else: + # Implicit + user has opted out + with pytest.raises(NotificationPermanentFailureException): + lookup_contact_info(notification.id) + else: + if user_set: + # Explicit + User has opted in + assert lookup_contact_info(notification.id) is None + else: + # Explicit + User has not defined opted in + with pytest.raises(NotificationPermanentFailureException): + lookup_contact_info(notification.id) diff --git a/tests/app/va/va_profile/test_va_profile_client_for_profile_v3.py b/tests/app/va/va_profile/test_va_profile_client_for_profile_v3.py deleted file mode 100644 index 2ed2302c37..0000000000 --- a/tests/app/va/va_profile/test_va_profile_client_for_profile_v3.py +++ /dev/null @@ -1,635 +0,0 @@ -import json -import random -from urllib import parse -from datetime import datetime, timedelta - -import pytest -import requests -import requests_mock - -from app.celery.contact_information_tasks import lookup_contact_info -from app.exceptions import NotificationPermanentFailureException -from app.feature_flags import FeatureFlag -from app.models import EMAIL_TYPE, SMS_TYPE, RecipientIdentifier -from app.va.identifier import IdentifierType, OIDS, transform_to_fhir_format -from app.va.va_profile.exceptions import ( - NoContactInfoException, - InvalidPhoneNumberException, - VAProfileIDNotFoundException, - VAProfileNonRetryableException, - VAProfileRetryableException, -) -from app.va.va_profile.va_profile_client import CommunicationChannel, VALID_PHONE_TYPES_FOR_SMS_DELIVERY -from app.va.va_profile.va_profile_types import Telephone - -from tests.app.conftest import MOCK_VA_PROFILE_URL -from tests.app.factories.feature_flag import mock_feature_flag - - -def telephone_entry( - create_date=datetime.today(), phone_type='MOBILE', classification_code='0', name_for_debugging='test mobile phone' -): - area_code = random.randint(100, 999) - phone_number = random.randint(100000, 9999999) - return { - 'createDate': f'{create_date}', - 'updateDate': f'{create_date}', - 'txAuditId': '6706b496-d727-401f-8df7-d6fc9adef0e7', - 'sourceSystem': name_for_debugging, - 'sourceDate': '2022-06-09T15:11:58Z', - 'originatingSourceSystem': 'VETSGOV', - 'sourceSystemUser': '1012833438V267437', - 'effectiveStartDate': '2022-06-09T15:11:58Z', - 'vaProfileId': 1550370, - 'telephoneId': 293410, - 'internationalIndicator': False, - 'phoneType': phone_type, - 'countryCode': '1', - 'areaCode': f'{area_code}', - 'phoneNumber': f'{phone_number}', - 'classification': {'classificationCode': classification_code}, - } - - -@pytest.fixture(scope='function') -def mock_response(): - with open('tests/app/va/va_profile/mock_response.json', 'r') as f: - return json.load(f) - - -@pytest.fixture(scope='module') -def recipient_identifier(): - return RecipientIdentifier(notification_id='123456', id_type=IdentifierType.VA_PROFILE_ID, id_value='1234') - - -@pytest.fixture(scope='module') -def id_with_aaid(recipient_identifier): - return transform_to_fhir_format(recipient_identifier) - - -@pytest.fixture(scope='module') -def oid(recipient_identifier): - return OIDS.get(recipient_identifier.id_type) - - -@pytest.fixture(scope='module') -def url(oid, id_with_aaid): - return f'{MOCK_VA_PROFILE_URL}/profile-service/profile/v3/{oid}/{id_with_aaid}' - - -class TestVAProfileClient: - def test_get_email_calls_endpoint_and_returns_email_address( - self, - rmock, - mock_va_profile_client, - mock_response, - recipient_identifier, - url, - mocker, - sample_notification, - ): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - rmock.post(url, json=mock_response, status_code=200) - - result = mock_va_profile_client.get_email_with_permission( - recipient_identifier, - sample_notification(gen_type=EMAIL_TYPE), - ) - email = result.recipient - - assert email == mock_response['profile']['contactInformation']['emails'][0]['emailAddressText'] - assert rmock.called - - def test_get_email_raises_NoContactInfoException_if_no_emails_exist( - self, - rmock, - mock_va_profile_client, - mock_response, - recipient_identifier, - url, - mocker, - sample_notification, - ): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - mock_response['profile']['contactInformation']['emails'] = [] - rmock.post(url, json=mock_response, status_code=200) - - with pytest.raises(NoContactInfoException): - mock_va_profile_client.get_email_with_permission( - recipient_identifier, - sample_notification(gen_type=EMAIL_TYPE), - ) - - def test_get_profile_calls_correct_url( - self, - rmock, - mock_va_profile_client, - mock_response, - recipient_identifier, - url, - id_with_aaid, - oid, - mocker, - sample_notification, - ): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - rmock.post(url, json=mock_response, status_code=200) - - mock_va_profile_client.get_email_with_permission(recipient_identifier, sample_notification()) - - assert rmock.called - - escaped_id = parse.quote(id_with_aaid, safe='') - expected_url = f'{MOCK_VA_PROFILE_URL}/profile-service/profile/v3/{oid}/{escaped_id}' - - assert rmock.request_history[0].url == expected_url - - def test_get_email_raises_exception_when_failed_request( - self, - rmock, - mock_va_profile_client, - recipient_identifier, - url, - mocker, - sample_notification, - ): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - response = { - 'messages': [ - { - 'code': 'CORE103', - 'key': '_CUF_NOT_FOUND', - 'text': 'The ContactInformationBio for id/criteria 103 could not be found. Please correct your requ...', - 'severity': 'INFO', - } - ], - 'txAuditId': 'dca32cae-b410-46c5-b61b-9a382567843f', - 'status': 'COMPLETED_FAILURE', - } - rmock.post(url, json=response, status_code=200) - - with pytest.raises(VAProfileNonRetryableException): - mock_va_profile_client.get_email_with_permission( - recipient_identifier, sample_notification(gen_type=EMAIL_TYPE) - ) - - def test_get_telephone_calls_endpoint_and_returns_phone_number( - self, - rmock, - mock_va_profile_client, - mock_response, - recipient_identifier, - url, - mocker, - sample_notification, - ): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - rmock.post(url, json=mock_response, status_code=200) - - result = mock_va_profile_client.get_telephone_with_permission(recipient_identifier, sample_notification()) - telephone = result.recipient - - assert telephone is not None - assert rmock.called - - @pytest.mark.parametrize( - 'classification_code, expected', - [ - *[(code, True) for code in VALID_PHONE_TYPES_FOR_SMS_DELIVERY], - (None, True), # if no classification code exists, fall back to True - (1, False), # LANDLINE - (3, False), # INVALID - (4, False), # OTHER - ], - ) - def test_has_valid_telephone_classification(self, mock_va_profile_client, classification_code, expected): - telephone_instance: Telephone = { - 'createDate': '2023-10-01', - 'updateDate': '2023-10-02', - 'txAuditId': 'TX123456', - 'sourceSystem': 'SystemA', - 'sourceDate': '2023-10-01', - 'originatingSourceSystem': 'SystemB', - 'sourceSystemUser': 'User123', - 'effectiveStartDate': '2023-10-01', - 'vaProfileId': 12345, - 'telephoneId': 67890, - 'internationalIndicator': False, - 'phoneType': 'Mobile', - 'countryCode': '1', - 'areaCode': '123', - 'phoneNumber': '4567890', - 'classification': {'classificationCode': classification_code, 'classificationName': 'SOME NAME'}, - } - if classification_code is None: - telephone_instance.pop('classification') - - mock_contact_info = {'vaProfileId': 'test', 'txAuditId': '1234'} - if expected: - assert mock_va_profile_client.has_valid_mobile_telephone_classification( - telephone_instance, mock_contact_info - ) - else: - with pytest.raises(InvalidPhoneNumberException): - mock_va_profile_client.has_valid_mobile_telephone_classification(telephone_instance, mock_contact_info) - - -class TestVAProfileClientExceptionHandling: - def test_get_telephone_raises_NoContactInfoException_if_no_telephones_exist( - self, - rmock, - mock_va_profile_client, - mock_response, - recipient_identifier, - url, - mocker, - sample_notification, - ): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - mock_response['profile']['contactInformation']['telephones'] = [] - rmock.post(url, json=mock_response, status_code=200) - - with pytest.raises(NoContactInfoException): - mock_va_profile_client.get_telephone_with_permission(recipient_identifier, sample_notification()) - - def test_get_telephone_raises_NoContactInfoException_if_no_mobile_telephones_exist( - self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url, mocker, sample_notification - ): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - telephones = mock_response['profile']['contactInformation']['telephones'] - mock_response['profile']['contactInformation']['telephones'] = [ - telephone for telephone in telephones if telephone['phoneType'] != 'MOBILE' - ] - rmock.post(url, json=mock_response, status_code=200) - - with pytest.raises(NoContactInfoException): - mock_va_profile_client.get_telephone_with_permission(recipient_identifier, sample_notification()) - - def test_get_telephone_raises_InvalidPhoneNumberException_if_number_classified_as_not_mobile( - self, rmock, mock_va_profile_client, mock_response, recipient_identifier, url, mocker, sample_notification - ): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - telephones = mock_response['profile']['contactInformation']['telephones'] - for telephone in telephones: - telephone['classification'] = {'classificationCode': 1} # LANDLINE classification - mock_response['profile']['contactInformation']['telephones'] = telephones - rmock.post(url, json=mock_response, status_code=200) - - with pytest.raises(InvalidPhoneNumberException): - mock_va_profile_client.get_telephone_with_permission(recipient_identifier, sample_notification()) - - def test_get_telephone_with_permission_prefers_user_specified_mobile_phone( - self, rmock, mock_va_profile_client, mock_response, mocker, url, recipient_identifier, sample_notification - ): - # A veteran has configured a mobile telephone to receive notifications. They add an additional mobile - # phone, and save it as their "home" phone. Even though it is technically a mobile device and is newer, - # we should send notifications to the device specified by the user - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - today = datetime.today() - yesterday_morning = (datetime.today() - timedelta(days=1)).replace(hour=6) - yesterday_evening = yesterday_morning.replace(hour=20) - home_phone_created_today = telephone_entry(today, 'HOME', 1, 'home phone created today') - mobile_phone_created_yesterday_morning = telephone_entry( - yesterday_morning, 'MOBILE', 0, 'mobile phone created yesterday morning' - ) - home_phone_with_mobile_classification_created_yesterday_evening = telephone_entry( - yesterday_evening, 'HOME', 0, 'home phone with mobile classification created yesterday evening' - ) - contact_info = mock_response['profile']['contactInformation'] - contact_info['telephones'] = [ - home_phone_created_today, - mobile_phone_created_yesterday_morning, - home_phone_with_mobile_classification_created_yesterday_evening, - ] - - rmock.post(url, json=mock_response, status_code=200) - result = mock_va_profile_client.get_telephone_with_permission(recipient_identifier, sample_notification()) - assert ( - result.recipient - == f"+{mobile_phone_created_yesterday_morning['countryCode']}{mobile_phone_created_yesterday_morning['areaCode']}{mobile_phone_created_yesterday_morning['phoneNumber']}" - ) - - def test_handle_exceptions_retryable_exception(self, mock_va_profile_client, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - # This test checks if VAProfileRetryableException is raised for a RequestException - with pytest.raises(VAProfileRetryableException): - mock_va_profile_client._handle_exceptions('some_va_profile_id', requests.RequestException()) - - def test_handle_exceptions_id_not_found_exception(self, mock_va_profile_client, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - # Simulate a 404 HTTP error - error = requests.HTTPError(response=requests.Response()) - error.response.status_code = 404 - # This test checks if VAProfileIDNotFoundException is raised for a 404 error - with pytest.raises(VAProfileIDNotFoundException): - mock_va_profile_client._handle_exceptions('some_va_profile_id', error) - - def test_handle_exceptions_non_retryable_exception(self, mock_va_profile_client, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - # Simulate a 400 HTTP error - error = requests.HTTPError(response=requests.Response()) - error.response.status_code = 400 - # This test checks if VAProfileNonRetryableException is raised for a 400 error - with pytest.raises(VAProfileNonRetryableException): - mock_va_profile_client._handle_exceptions('some_va_profile_id', error) - - def test_handle_exceptions_timeout_exception(self, mock_va_profile_client, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - # This test checks if VAProfileRetryableExcception is raised for a Timeout exception - # Timeout inherits from requests.RequestException, so all exceptions of type RequestException should - # raise a VAProfileRetryableException - with pytest.raises(VAProfileRetryableException): - mock_va_profile_client._handle_exceptions('some_va_profile_id', requests.Timeout()) - - @pytest.mark.parametrize('status', [429, 500]) - def test_client_raises_retryable_exception( - self, - rmock, - mock_va_profile_client, - recipient_identifier, - status, - mocker, - sample_notification, - ): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - rmock.post(requests_mock.ANY, status_code=status) - - with pytest.raises(VAProfileRetryableException): - mock_va_profile_client.get_email_with_permission( - recipient_identifier, sample_notification(gen_type=EMAIL_TYPE) - ) - - with pytest.raises(VAProfileRetryableException): - mock_va_profile_client.get_email_with_permission(recipient_identifier, sample_notification()) - - def test_client_raises_retryable_exception_when_request_exception_is_thrown( - self, - mock_va_profile_client, - recipient_identifier, - mocker, - sample_notification, - ): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - with requests_mock.Mocker(real_http=True) as rmock: - rmock.post(requests_mock.ANY, exc=requests.RequestException) - - with pytest.raises(VAProfileRetryableException): - mock_va_profile_client.get_email_with_permission( - recipient_identifier, sample_notification(gen_type=EMAIL_TYPE) - ) - - with pytest.raises(VAProfileRetryableException): - mock_va_profile_client.get_email_with_permission(recipient_identifier, sample_notification()) - - -class TestCommunicationPermissions: - @pytest.mark.parametrize('expected', [True, False]) - def test_get_is_communication_allowed_returns_whether_permissions_granted_for_sms_communication( - self, - rmock, - mock_va_profile_client, - mock_response, - url, - expected, - mocker, - sample_notification, - ): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - notification = sample_notification() - mock_response['profile']['communicationPermissions'][0]['allowed'] = expected - mock_response['profile']['communicationPermissions'][0]['communicationItemId'] = notification.va_profile_item_id - - allowed = mock_va_profile_client.get_is_communication_allowed_from_profile( - mock_response['profile'], notification, CommunicationChannel.TEXT - ) - - assert allowed is expected - - @pytest.mark.parametrize('expected', [True, False]) - def test_get_is_communication_allowed_returns_whether_permissions_granted_for_email_communication( - self, - rmock, - mock_va_profile_client, - mock_response, - url, - expected, - mocker, - sample_notification, - ): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - notification = sample_notification(gen_type=EMAIL_TYPE) - mock_response['profile']['communicationPermissions'][1]['allowed'] = expected - mock_response['profile']['communicationPermissions'][1]['communicationItemId'] = notification.va_profile_item_id - - allowed = mock_va_profile_client.get_is_communication_allowed_from_profile( - mock_response['profile'], notification, CommunicationChannel.EMAIL - ) - - assert allowed is expected - - @pytest.mark.parametrize( - 'default_send, user_set, expected', - [ - # If the user has set a preference, we always go with that and override default_send - [True, True, True], - [True, False, False], - [False, True, True], - [False, False, False], - # If the user has not set a preference, go with the default_send - [True, None, True], - [False, None, False], - ], - ) - @pytest.mark.parametrize('notification_type', [CommunicationChannel.EMAIL, CommunicationChannel.TEXT]) - def test_get_email_or_sms_with_permission_utilizes_default_send( - self, - mock_va_profile_client, - mock_response, - recipient_identifier, - sample_communication_item, - sample_notification, - sample_template, - default_send, - user_set, - expected, - notification_type, - mocker, - ): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - profile = mock_response['profile'] - communication_item = sample_communication_item(default_send) - template = sample_template(communication_item_id=communication_item.id) - - notification = sample_notification( - template=template, gen_type=EMAIL_TYPE if notification_type == CommunicationChannel.EMAIL else SMS_TYPE - ) - - if user_set is not None: - profile['communicationPermissions'][0]['allowed'] = user_set - profile['communicationPermissions'][0]['communicationItemId'] = notification.va_profile_item_id - profile['communicationPermissions'][0]['communicationChannelId'] = notification_type.id - else: - profile['communicationPermissions'] = [] - - mocker.patch.object(mock_va_profile_client, 'get_profile', return_value=profile) - - if notification_type == CommunicationChannel.EMAIL: - client_fn = mock_va_profile_client.get_email_with_permission - else: - client_fn = mock_va_profile_client.get_telephone_with_permission - - result = client_fn(recipient_identifier, notification) - assert result.communication_allowed == expected - - -class TestSendEmailStatus: - mock_response = {} - mock_notification_data = { - 'id': '2e9e6920-4f6f-4cd5-9e16-fc306fe23867', # this is the notification id - 'reference': None, - 'to': 'test@email.com', # this is the recipient's contact info (email) - 'status': 'delivered', # this will specify the delivery status of the notification - 'status_reason': '', # populated if there's additional context on the delivery status - 'created_at': '2024-07-25T10:00:00.0', - 'completed_at': '2024-07-25T11:00:00.0', - 'sent_at': '2024-07-25T11:00:00.0', - 'notification_type': EMAIL_TYPE, # this is the channel/type of notification (email) - 'provider': 'ses', # email provider - } - - def test_send_va_profile_email_status_sent_successfully(self, rmock, mock_va_profile_client, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - rmock.post(requests_mock.ANY, json=self.mock_response, status_code=200) - - mock_va_profile_client.send_va_profile_email_status(self.mock_notification_data) - - assert rmock.called - - expected_url = f'{MOCK_VA_PROFILE_URL}/contact-information-vanotify/notify/status' - assert rmock.request_history[0].url == expected_url - - def test_send_va_profile_email_status_timeout(self, rmock, mock_va_profile_client, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - rmock.post(requests_mock.ANY, exc=requests.ReadTimeout) - - with pytest.raises(requests.Timeout): - mock_va_profile_client.send_va_profile_email_status(self.mock_notification_data) - - assert rmock.called - - expected_url = f'{MOCK_VA_PROFILE_URL}/contact-information-vanotify/notify/status' - assert rmock.request_history[0].url == expected_url - - def test_send_va_profile_email_status_throws_exception(self, rmock, mock_va_profile_client, mocker): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - - rmock.post(requests_mock.ANY, exc=requests.RequestException) - - with pytest.raises(requests.RequestException): - mock_va_profile_client.send_va_profile_email_status(self.mock_notification_data) - - assert rmock.called - - expected_url = f'{MOCK_VA_PROFILE_URL}/contact-information-vanotify/notify/status' - assert rmock.request_history[0].url == expected_url - - -@pytest.mark.parametrize( - 'default_send, user_set, expected', - [ - # If the user has set a preference, we always go with that and override default_send - [True, True, True], - [True, False, False], - [False, True, True], - [False, False, False], - # If the user has not set a preference, go with the default_send - [True, None, True], - [False, None, False], - ], -) -@pytest.mark.parametrize('notification_type', [CommunicationChannel.EMAIL, CommunicationChannel.TEXT]) -def test_get_email_or_sms_with_permission_utilizes_default_send( - mock_va_profile_response, - sample_communication_item, - sample_notification, - sample_template, - default_send, - user_set, - expected, - notification_type, - mocker, -): - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_COMBINE_CONTACT_INFO_AND_PERMISSIONS_LOOKUP, 'True') - mock_feature_flag(mocker, FeatureFlag.VA_PROFILE_V3_IDENTIFY_MOBILE_TELEPHONE_NUMBERS, 'True') - # Test each combo, ensuring contact info responds with expected result - channel = EMAIL_TYPE if notification_type == CommunicationChannel.EMAIL else SMS_TYPE - profile = mock_va_profile_response['profile'] - communication_item = sample_communication_item(default_send) - template = sample_template(template_type=channel, communication_item_id=communication_item.id) - notification = sample_notification( - template=template, - gen_type=channel, - recipient_identifiers=[{'id_type': IdentifierType.VA_PROFILE_ID.value, 'id_value': '1234'}], - ) - - profile['communicationPermissions'][0]['allowed'] = user_set - profile['communicationPermissions'][0]['communicationItemId'] = notification.va_profile_item_id - profile['communicationPermissions'][0]['communicationChannelId'] = notification_type.id - - mocker.patch('app.va.va_profile.va_profile_client.VAProfileClient.get_profile', return_value=profile) - - if default_send: - if user_set or user_set is None: - # Implicit + user has not opted out - assert lookup_contact_info(notification.id) is None - else: - # Implicit + user has opted out - with pytest.raises(NotificationPermanentFailureException): - lookup_contact_info(notification.id) - else: - if user_set: - # Explicit + User has opted in - assert lookup_contact_info(notification.id) is None - else: - # Explicit + User has not defined opted in - with pytest.raises(NotificationPermanentFailureException): - lookup_contact_info(notification.id) From ffc9ac353aa067ccb1b1c8e550cbee333c3351cf Mon Sep 17 00:00:00 2001 From: Evan Parish <104009494+EvanParish@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:29:31 -0500 Subject: [PATCH 3/6] HOTFIX - Fix UnboundLocalError in Twilio Client --- app/clients/sms/twilio.py | 11 +++++------ app/va/va_profile/va_profile_client.py | 2 ++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/clients/sms/twilio.py b/app/clients/sms/twilio.py index a1c876bdeb..96cfbc0f7f 100644 --- a/app/clients/sms/twilio.py +++ b/app/clients/sms/twilio.py @@ -240,15 +240,14 @@ def translate_delivery_status( parsed_dict = parse_qs(decoded_msg) message_sid = parsed_dict['MessageSid'][0] twilio_delivery_status = parsed_dict['MessageStatus'][0] + error_code_data = parsed_dict.get('ErrorCode', None) if twilio_delivery_status not in self.twilio_notify_status_map: value_error = f'Invalid Twilio delivery status: {twilio_delivery_status}' raise ValueError(value_error) - if 'ErrorCode' in parsed_dict and ( - twilio_delivery_status == 'failed' or twilio_delivery_status == 'undelivered' - ): - error_code = parsed_dict['ErrorCode'][0] + if error_code_data and (twilio_delivery_status == 'failed' or twilio_delivery_status == 'undelivered'): + error_code = error_code_data[0] if error_code in self.twilio_error_code_map: notify_delivery_status: TwilioStatus = self.twilio_error_code_map[error_code] @@ -259,10 +258,10 @@ def translate_delivery_status( notify_delivery_status: TwilioStatus = self.twilio_notify_status_map[twilio_delivery_status] else: # Logic not being changed, just want to log this for now - if 'ErrorCode' in parsed_dict: + if error_code_data: self.logger.warning( 'Error code: %s existed but status for message: %s was not failed nor undelivered', - error_code, + error_code_data[0], message_sid, ) notify_delivery_status: TwilioStatus = self.twilio_notify_status_map[twilio_delivery_status] diff --git a/app/va/va_profile/va_profile_client.py b/app/va/va_profile/va_profile_client.py index ff45ad7be3..d4405d3a0d 100644 --- a/app/va/va_profile/va_profile_client.py +++ b/app/va/va_profile/va_profile_client.py @@ -31,6 +31,8 @@ 'sms': 'Text', } +# source for valid phone types for SMS delivery: +# https://docs.aws.amazon.com/pinpoint/latest/developerguide/validate-phone-numbers.html#validate-phone-numbers-example-responses VALID_PHONE_TYPES_FOR_SMS_DELIVERY = [ 0, # "MOBILE" 2, # "VOIP" From fc1145ce69275af573bc740827957eaefd768104 Mon Sep 17 00:00:00 2001 From: Evan Parish <104009494+EvanParish@users.noreply.github.com> Date: Fri, 18 Oct 2024 11:23:26 -0500 Subject: [PATCH 4/6] #1945 - Create shared environment file for ECS tasks --- .github/workflows/build.yml | 4 + .talismanrc | 8 + cd/application-deployment/dev/dev.env | 55 +++++ .../dev/vaec-api-task-definition.json | 198 +---------------- .../dev/vaec-celery-beat-task-definition.json | 174 +-------------- .../dev/vaec-celery-task-definition.json | 210 +----------------- cd/application-deployment/perf/perf.env | 51 +++++ .../perf/vaec-api-task-definition.json | 174 +-------------- .../vaec-celery-beat-task-definition.json | 6 + .../perf/vaec-celery-task-definition.json | 170 +------------- cd/application-deployment/prod/prod.env | 51 +++++ .../prod/vaec-api-task-definition.json | 178 +-------------- .../vaec-celery-beat-task-definition.json | 154 +------------ .../prod/vaec-celery-task-definition.json | 174 +-------------- cd/application-deployment/staging/staging.env | 55 +++++ .../staging/vaec-api-task-definition.json | 194 +--------------- .../vaec-celery-beat-task-definition.json | 170 +------------- .../staging/vaec-celery-task-definition.json | 190 +--------------- 18 files changed, 296 insertions(+), 1920 deletions(-) create mode 100644 cd/application-deployment/dev/dev.env create mode 100644 cd/application-deployment/perf/perf.env create mode 100644 cd/application-deployment/prod/prod.env create mode 100644 cd/application-deployment/staging/staging.env diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b23053dc6..c204c5fb90 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,6 +29,10 @@ jobs: role-skip-session-tagging: true role-duration-seconds: 900 + - name: Upload Env File to S3 + run: | + aws s3 cp cd/application-deployment/${{ inputs.environment }}/${{ inputs.environment }}.env s3://vanotify-environment-variables-${{ inputs.environment }}/ + - name: Login to VAEC ECR id: login-ecr-vaec uses: aws-actions/amazon-ecr-login@v2 diff --git a/.talismanrc b/.talismanrc index f74b3fd21f..ca4f14fd41 100644 --- a/.talismanrc +++ b/.talismanrc @@ -15,8 +15,16 @@ fileignoreconfig: checksum: b42aefd1ae0e6ea76e75db4cf14d425facd0941943b17f7ba2e41f850ad1ec23 - filename: app/template/rest.py checksum: 1e5bdac8bc694d50f8f656dec127dd036b7b1b5b6156e3282d3411956c71ba0b +- filename: cd/application-deployment/dev/dev.env + checksum: da33c215ecd18a2b3d44aa0d35c70a77b94cd5b2015d958a8103e96e7c707e23 - filename: cd/application-deployment/dev/vaec-api-task-definition.json checksum: f328ff821339b802eb1d82559e624d5b719857c813d427da5aaa39b240331ddd +- filename: cd/application-deployment/perf/perf.env + checksum: ea3f1cf6feea351e2ad414e7edd55ea98b5c31d93383d890b6ec6dfeb08023c0 +- filename: cd/application-deployment/prod/prod.env + checksum: a5bc436f5567767174804947dace7b58063e863dc9a2a047362ee797ffe7226b +- filename: cd/application-deployment/staging/staging.env + checksum: 786d9297860520e4ac71e491623ac12b98430d6b73e4c33cede67a6afdeaec07 - filename: ci/docker-compose-test.yml checksum: e3efec2749e8c19e60f5bfc68eafabe24eba647530a482ceccfc4e0e62cff424 - filename: lambda_functions/pinpoint_callback/pinpoint_callback_lambda.py diff --git a/cd/application-deployment/dev/dev.env b/cd/application-deployment/dev/dev.env new file mode 100644 index 0000000000..9a6147788b --- /dev/null +++ b/cd/application-deployment/dev/dev.env @@ -0,0 +1,55 @@ +ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED=True +API_HOST_NAME=https://dev.api.notifications.va.gov +API_MESSAGE_LIMIT_ENABLED=True +API_RATE_LIMIT_ENABLED=True +ATTACHMENTS_BUCKET=dev-notifications-va-gov-attachments +AWS_PINPOINT_APP_ID=df55c01206b742d2946ef226410af94f +AWS_SES_EMAIL_FROM_USER=dev-do-not-reply +CHECK_GITHUB_SCOPE_ENABLED=True +CHECK_TEMPLATE_NAME_EXISTS_ENABLED=True +COMP_AND_PEN_DYNAMODB_NAME=dev-bip-payment-notification-table +COMP_AND_PEN_MESSAGES_ENABLED=True +DD_ENV=dev +DD_PROFILING_ENABLED=True +DD_PROFILING_ENABLE_CODE_PROVENANCE=True +DD_SERVICE=celery-beat +DD_SITE=ddog-gov.com +EMAIL_ATTACHMENTS_ENABLED=True +EMAIL_PASSWORD_LOGIN_ENABLED=True +EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL=LOAD_BALANCING +FLASK_APP=run_celery_beat.py +GA4_URL=https://www.google-analytics.com/mp/collect +GITHUB_LOGIN_ENABLED=True +GOOGLE_ANALYTICS_ENABLED=True +GRANICUS_URL=https://stage-tms.govdelivery.com +MPI_URL=https://int.services.eauth.va.gov:9303/int +NIGHTLY_NOTIF_CSV_ENABLED=True +NOTIFICATION_FAILURE_REASON_ENABLED=True +NOTIFICATION_QUEUE_PREFIX=dev-notification- +NOTIFY_EMAIL_FROM_USER=stage-notifications +NOTIFY_ENVIRONMENT=development +PINPOINT_INBOUND_SMS_ENABLED=True +PINPOINT_RECEIPTS_ENABLED=True +PLATFORM_STATS_ENABLED=True +PROVIDER_STRATEGIES_ENABLED=True +PUSH_NOTIFICATIONS_ENABLED=True +REDIS_ENABLED=True +SESSION_COOKIE_SECURE=True +SMS_PROVIDER_SELECTION_STRATEGY_LABEL=HIGHEST_PRIORITY +SMS_SENDER_RATE_LIMIT_ENABLED=True +STATSD_HOST=localhost +TEMPLATE_SERVICE_PROVIDERS_ENABLED=True +TWILIO_ACCOUNT_SID=fake +TWILIO_AUTH_TOKEN=fake +UI_HOST_NAME=https://dev.notifications.va.gov +VANOTIFY_SSL_CERT_PATH=/app/certs/vanotify_ssl_cert.pem +VANOTIFY_SSL_KEY_PATH=/app/certs/vanotify_ssl_key.pem +VA_FLAGSHIP_APP_SID=A20623E2321D4053A6C34C9307C6C221 +VA_ONSITE_URL=https://staging-api.va.gov +VA_PROFILE_EMAIL_STATUS_ENABLED=True +VA_PROFILE_URL=https://int.vaprofile.va.gov +VA_SSO_ACCESS_TOKEN_URL=https://int.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/token +VA_SSO_AUTHORIZE_URL=https://int.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/authorize +VA_SSO_ENABLED=True +VA_SSO_SERVER_METADATA_URL=https://int.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/metadata/ISAMOP/.well-known/oauth-authorization-server +VETEXT_SID=C9BEC63F53CE4C1D992CE73E8D1D8D94 diff --git a/cd/application-deployment/dev/vaec-api-task-definition.json b/cd/application-deployment/dev/vaec-api-task-definition.json index b92796e730..1176918c29 100644 --- a/cd/application-deployment/dev/vaec-api-task-definition.json +++ b/cd/application-deployment/dev/vaec-api-task-definition.json @@ -22,206 +22,20 @@ "hostPort": 6011 } ], - "environment": [ - { - "name": "ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED", - "value": "True" - }, - { - "name": "API_HOST_NAME", - "value": "https://dev.api.notifications.va.gov" - }, - { - "name": "DD_ENV", - "value": "dev" - }, - { - "name": "DD_SITE", - "value": "ddog-gov.com" - }, + "environmentFiles": [ { - "name": "DD_PROFILING_ENABLED", - "value": "True" - }, + "type": "s3", + "value": "arn:aws-us-gov:s3:::vanotify-environment-variables-dev/dev.env" + } + ], + "environment": [ { "name": "DD_SERVICE", "value": "notification-api" }, - { - "name": "DD_PROFILING_ENABLE_CODE_PROVENANCE", - "value": "True" - }, - { - "name": "API_MESSAGE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "API_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "ATTACHMENTS_BUCKET", - "value": "dev-notifications-va-gov-attachments" - }, - { - "name": "AWS_PINPOINT_APP_ID", - "value": "df55c01206b742d2946ef226410af94f" - }, - { - "name": "AWS_SES_EMAIL_FROM_USER", - "value": "dev-do-not-reply" - }, - { - "name": "CHECK_GITHUB_SCOPE_ENABLED", - "value": "True" - }, - { - "name": "CHECK_TEMPLATE_NAME_EXISTS_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_ATTACHMENTS_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_PASSWORD_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "LOAD_BALANCING" - }, { "name": "FLASK_APP", "value": "application.py" - }, - { - "name": "GITHUB_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "GOOGLE_ANALYTICS_ENABLED", - "value": "True" - }, - { - "name": "GRANICUS_URL", - "value": "https://stage-tms.govdelivery.com" - }, - { - "name": "MPI_URL", - "value": "https://int.services.eauth.va.gov:9303/int" - }, - { - "name": "NOTIFICATION_FAILURE_REASON_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_QUEUE_PREFIX", - "value": "dev-notification-" - }, - { - "name": "NOTIFY_EMAIL_FROM_USER", - "value": "stage-notifications" - }, - { - "name": "NOTIFY_ENVIRONMENT", - "value": "development" - }, - { - "name": "PINPOINT_INBOUND_SMS_ENABLED", - "value": "True" - }, - { - "name": "PINPOINT_RECEIPTS_ENABLED", - "value": "True" - }, - { - "name": "PROVIDER_STRATEGIES_ENABLED", - "value": "True" - }, - { - "name": "REDIS_ENABLED", - "value": "True" - }, - { - "name": "NIGHTLY_NOTIF_CSV_ENABLED", - "value": "True" - }, - { - "name": "SESSION_COOKIE_SECURE", - "value": "True" - }, - { - "name": "SMS_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "HIGHEST_PRIORITY" - }, - { - "name": "SMS_SENDER_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "STATSD_HOST", - "value": "localhost" - }, - { - "name": "TEMPLATE_SERVICE_PROVIDERS_ENABLED", - "value": "True" - }, - { - "name": "UI_HOST_NAME", - "value": "https://dev.notifications.va.gov" - }, - { - "name": "VA_ONSITE_URL", - "value": "https://staging-api.va.gov" - }, - { - "name": "VA_PROFILE_URL", - "value": "https://int.vaprofile.va.gov" - }, - { - "name": "VA_FLAGSHIP_APP_SID", - "value": "A20623E2321D4053A6C34C9307C6C221" - }, - { - "name": "VETEXT_SID", - "value": "C9BEC63F53CE4C1D992CE73E8D1D8D94" - }, - { - "name": "PUSH_NOTIFICATIONS_ENABLED", - "value": "True" - }, - { - "name": "PLATFORM_STATS_ENABLED", - "value": "True" - }, - { - "name": "VA_SSO_ENABLED", - "value": "True" - }, - { - "name": "V3_ENABLED", - "value": "True" - }, - { - "name": "VA_SSO_SERVER_METADATA_URL", - "value": "https://int.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/metadata/ISAMOP/.well-known/oauth-authorization-server" - }, - { - "name": "VA_SSO_AUTHORIZE_URL", - "value": "https://int.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/authorize" - }, - { - "name": "VA_SSO_ACCESS_TOKEN_URL", - "value": "https://int.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/token" - }, - { - "name": "COMP_AND_PEN_MESSAGES_ENABLED", - "value": "True" - }, - { - "name": "COMP_AND_PEN_DYNAMODB_NAME", - "value": "dev-bip-payment-notification-table" } ], "secrets": [ diff --git a/cd/application-deployment/dev/vaec-celery-beat-task-definition.json b/cd/application-deployment/dev/vaec-celery-beat-task-definition.json index 41a9ce6214..33c43367bc 100644 --- a/cd/application-deployment/dev/vaec-celery-beat-task-definition.json +++ b/cd/application-deployment/dev/vaec-celery-beat-task-definition.json @@ -16,182 +16,20 @@ "awslogs-stream-prefix": "ecs" } }, - "environment": [ - { - "name": "ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED", - "value": "True" - }, - { - "name": "API_HOST_NAME", - "value": "https://dev.api.notifications.va.gov" - }, - { - "name": "API_MESSAGE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "API_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "ATTACHMENTS_BUCKET", - "value": "dev-notifications-va-gov-attachments" - }, - { - "name": "AWS_PINPOINT_APP_ID", - "value": "df55c01206b742d2946ef226410af94f" - }, - { - "name": "AWS_SES_EMAIL_FROM_USER", - "value": "dev-do-not-reply" - }, - { - "name": "CHECK_GITHUB_SCOPE_ENABLED", - "value": "True" - }, - { - "name": "CHECK_TEMPLATE_NAME_EXISTS_ENABLED", - "value": "True" - }, - { - "name": "DD_ENV", - "value": "dev" - }, + "environmentFiles": [ { - "name": "DD_SITE", - "value": "ddog-gov.com" - }, + "type": "s3", + "value": "arn:aws-us-gov:s3:::vanotify-environment-variables-dev/dev.env" + } + ], + "environment": [ { "name": "DD_SERVICE", "value": "celery-beat" }, - { - "name": "DD_PROFILING_ENABLE_CODE_PROVENANCE", - "value": "True" - }, - { - "name": "DD_PROFILING_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_ATTACHMENTS_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_PASSWORD_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "LOAD_BALANCING" - }, { "name": "FLASK_APP", "value": "run_celery_beat.py" - }, - { - "name": "GITHUB_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "GOOGLE_ANALYTICS_ENABLED", - "value": "True" - }, - { - "name": "NIGHTLY_NOTIF_CSV_ENABLED", - "value": "True" - }, - { - "name": "GRANICUS_URL", - "value": "https://stage-tms.govdelivery.com" - }, - { - "name": "MPI_URL", - "value": "https://int.services.eauth.va.gov:9303/int" - }, - { - "name": "NOTIFICATION_FAILURE_REASON_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_QUEUE_PREFIX", - "value": "dev-notification-" - }, - { - "name": "NOTIFY_EMAIL_FROM_USER", - "value": "stage-notifications" - }, - { - "name": "NOTIFY_ENVIRONMENT", - "value": "development" - }, - { - "name": "PINPOINT_RECEIPTS_ENABLED", - "value": "True" - }, - { - "name": "PINPOINT_INBOUND_SMS_ENABLED", - "value": "True" - }, - { - "name": "PROVIDER_STRATEGIES_ENABLED", - "value": "True" - }, - { - "name": "REDIS_ENABLED", - "value": "True" - }, - { - "name": "SESSION_COOKIE_SECURE", - "value": "True" - }, - { - "name": "SMS_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "HIGHEST_PRIORITY" - }, - { - "name": "SMS_SENDER_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "STATSD_HOST", - "value": "localhost" - }, - { - "name": "TEMPLATE_SERVICE_PROVIDERS_ENABLED", - "value": "True" - }, - { - "name": "UI_HOST_NAME", - "value": "https://dev.notifications.va.gov" - }, - { - "name": "VA_ONSITE_URL", - "value": "https://staging-api.va.gov" - }, - { - "name": "VA_PROFILE_URL", - "value": "https://int.vaprofile.va.gov" - }, - { - "name": "VANOTIFY_SSL_CERT_PATH", - "value": "/app/certs/vanotify_ssl_cert.pem" - }, - { - "name": "VANOTIFY_SSL_KEY_PATH", - "value": "/app/certs/vanotify_ssl_key.pem" - }, - { - "name": "PLATFORM_STATS_ENABLED", - "value": "True" - }, - { - "name": "COMP_AND_PEN_MESSAGES_ENABLED", - "value": "True" - }, - { - "name": "COMP_AND_PEN_DYNAMODB_NAME", - "value": "dev-bip-payment-notification-table" } ], "secrets": [ diff --git a/cd/application-deployment/dev/vaec-celery-task-definition.json b/cd/application-deployment/dev/vaec-celery-task-definition.json index 0fe25d1772..5f5ba97582 100644 --- a/cd/application-deployment/dev/vaec-celery-task-definition.json +++ b/cd/application-deployment/dev/vaec-celery-task-definition.json @@ -16,218 +16,20 @@ "awslogs-stream-prefix": "ecs" } }, - "environment": [ - { - "name": "ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED", - "value": "True" - }, - { - "name": "NIGHTLY_NOTIF_CSV_ENABLED", - "value": "True" - }, - { - "name": "API_HOST_NAME", - "value": "https://dev.api.notifications.va.gov" - }, - { - "name": "DD_ENV", - "value": "dev" - }, - { - "name": "DD_SITE", - "value": "ddog-gov.com" - }, + "environmentFiles": [ { - "name": "DD_PROFILING_ENABLED", - "value": "True" - }, + "type": "s3", + "value": "arn:aws-us-gov:s3:::vanotify-environment-variables-dev/dev.env" + } + ], + "environment": [ { "name": "DD_SERVICE", "value": "celery" }, - { - "name": "DD_PROFILING_ENABLE_CODE_PROVENANCE", - "value": "True" - }, - { - "name": "API_MESSAGE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "API_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "ATTACHMENTS_BUCKET", - "value": "dev-notifications-va-gov-attachments" - }, - { - "name": "AWS_PINPOINT_APP_ID", - "value": "df55c01206b742d2946ef226410af94f" - }, - { - "name": "AWS_SES_EMAIL_FROM_USER", - "value": "dev-do-not-reply" - }, - { - "name": "CHECK_GITHUB_SCOPE_ENABLED", - "value": "True" - }, - { - "name": "CHECK_TEMPLATE_NAME_EXISTS_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_ATTACHMENTS_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_PASSWORD_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "LOAD_BALANCING" - }, { "name": "FLASK_APP", "value": "run_celery.py" - }, - { - "name": "GITHUB_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "GOOGLE_ANALYTICS_ENABLED", - "value": "True" - }, - { - "name": "GA4_URL", - "value": "https://www.google-analytics.com/mp/collect" - }, - { - "name": "GRANICUS_URL", - "value": "https://stage-tms.govdelivery.com" - }, - { - "name": "MPI_URL", - "value": "https://int.services.eauth.va.gov:9303/int" - }, - { - "name": "NOTIFICATION_FAILURE_REASON_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_QUEUE_PREFIX", - "value": "dev-notification-" - }, - { - "name": "NOTIFY_EMAIL_FROM_USER", - "value": "stage-notifications" - }, - { - "name": "NOTIFY_ENVIRONMENT", - "value": "development" - }, - { - "name": "PINPOINT_INBOUND_SMS_ENABLED", - "value": "True" - }, - { - "name": "PINPOINT_RECEIPTS_ENABLED", - "value": "True" - }, - { - "name": "PROVIDER_STRATEGIES_ENABLED", - "value": "True" - }, - { - "name": "REDIS_ENABLED", - "value": "True" - }, - { - "name": "SESSION_COOKIE_SECURE", - "value": "True" - }, - { - "name": "SMS_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "HIGHEST_PRIORITY" - }, - { - "name": "SMS_SENDER_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "STATSD_HOST", - "value": "localhost" - }, - { - "name": "TEMPLATE_SERVICE_PROVIDERS_ENABLED", - "value": "True" - }, - { - "name": "UI_HOST_NAME", - "value": "https://dev.notifications.va.gov" - }, - { - "name": "VA_ONSITE_URL", - "value": "https://staging-api.va.gov" - }, - { - "name": "VA_PROFILE_URL", - "value": "https://int.vaprofile.va.gov" - }, - { - "name": "VA_PROFILE_EMAIL_STATUS_ENABLED", - "value": "True" - }, - { - "name": "VANOTIFY_SSL_CERT_PATH", - "value": "/app/certs/vanotify_ssl_cert.pem" - }, - { - "name": "VANOTIFY_SSL_KEY_PATH", - "value": "/app/certs/vanotify_ssl_key.pem" - }, - { - "name": "VA_FLAGSHIP_APP_SID", - "value": "A20623E2321D4053A6C34C9307C6C221" - }, - { - "name": "VETEXT_SID", - "value": "C9BEC63F53CE4C1D992CE73E8D1D8D94" - }, - { - "name": "PUSH_NOTIFICATIONS_ENABLED", - "value": "True" - }, - { - "name": "PLATFORM_STATS_ENABLED", - "value": "True" - }, - { - "name": "VA_SSO_ENABLED", - "value": "True" - }, - { - "name": "VA_SSO_SERVER_METADATA_URL", - "value": "https://int.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/metadata/ISAMOP/.well-known/oauth-authorization-server" - }, - { - "name": "VA_SSO_AUTHORIZE_URL", - "value": "https://int.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/authorize" - }, - { - "name": "VA_SSO_ACCESS_TOKEN_URL", - "value": "https://int.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/token" - }, - { - "name": "COMP_AND_PEN_MESSAGES_ENABLED", - "value": "True" - }, - { - "name": "COMP_AND_PEN_DYNAMODB_NAME", - "value": "dev-bip-payment-notification-table" } ], "secrets": [ diff --git a/cd/application-deployment/perf/perf.env b/cd/application-deployment/perf/perf.env new file mode 100644 index 0000000000..46d8f59408 --- /dev/null +++ b/cd/application-deployment/perf/perf.env @@ -0,0 +1,51 @@ +ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED=True +API_HOST_NAME=https://perf.api.notifications.va.gov +ATTACHMENTS_BUCKET=perf-notifications-va-gov-attachments +AWS_PINPOINT_APP_ID=f8cab892fe2740c2901560b55a398440 +AWS_SES_EMAIL_FROM_USER=perf-do-not-reply +CHECK_GITHUB_SCOPE_ENABLED=False +CHECK_TEMPLATE_NAME_EXISTS_ENABLED=False +COMP_AND_PEN_DYNAMODB_NAME=perf-bip-payment-notification-table +COMP_AND_PEN_MESSAGES_ENABLED=True +COMP_AND_PEN_PERF_TO_NUMBER=+14254147755 +DD_ENV=perf +DD_PROFILING_ENABLED=True +DD_PROFILING_ENABLE_CODE_PROVENANCE=True +DD_SERVICE=celery-beat +DD_SITE=ddog-gov.com +EMAIL_ATTACHMENTS_ENABLED=True +EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL=LOAD_BALANCING +FLASK_APP=run_celery_beat.py +GA4_URL=https://www.google-analytics.com/mp/collect +GOOGLE_ANALYTICS_ENABLED=True +GRANICUS_URL=https://stage-tms.govdelivery.com +MPI_URL=https://sqa.services.eauth.va.gov:9303/sqa +NIGHTLY_NOTIF_CSV_ENABLED=True +NOTIFICATION_FAILURE_REASON_ENABLED=True +NOTIFICATION_QUEUE_PREFIX=perf-notification- +NOTIFY_EMAIL_FROM_USER=stage-notifications +NOTIFY_ENVIRONMENT=performance +PINPOINT_RECEIPTS_ENABLED=True +PLATFORM_STATS_ENABLED=False +PROVIDER_STRATEGIES_ENABLED=True +PUSH_NOTIFICATIONS_ENABLED=True +REDIS_ENABLED=True +SMS_PROVIDER_SELECTION_STRATEGY_LABEL=HIGHEST_PRIORITY +SMS_SENDER_RATE_LIMIT_ENABLED=True +STATSD_HOST=localhost +TEMPLATE_SERVICE_PROVIDERS_ENABLED=True +TWILIO_ACCOUNT_SID=fake +TWILIO_AUTH_TOKEN=fake +UI_HOST_NAME=https://perf.notifications.va.gov +V3_ENABLED=True +VANOTIFY_SSL_CERT_PATH=/app/certs/vanotify_ssl_cert.pem +VANOTIFY_SSL_KEY_PATH=/app/certs/vanotify_ssl_key.pem +VA_FLAGSHIP_APP_SID=A20623E2321D4053A6C34C9307C6C221 +VA_ONSITE_URL=https://staging-api.va.gov +VA_PROFILE_EMAIL_STATUS_ENABLED=True +VA_PROFILE_URL=https://qa.vaprofile.va.gov +VA_SSO_ACCESS_TOKEN_URL=https://preprod.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/token +VA_SSO_AUTHORIZE_URL=https://preprod.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/authorize +VA_SSO_ENABLED=True +VA_SSO_SERVER_METADATA_URL=https://preprod.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/metadata/ISAMOP/.well-known/oauth-authorization-server +VETEXT_SID=C9BEC63F53CE4C1D992CE73E8D1D8D94 diff --git a/cd/application-deployment/perf/vaec-api-task-definition.json b/cd/application-deployment/perf/vaec-api-task-definition.json index 74ab387a3d..f9265f1348 100644 --- a/cd/application-deployment/perf/vaec-api-task-definition.json +++ b/cd/application-deployment/perf/vaec-api-task-definition.json @@ -22,182 +22,20 @@ "hostPort": 6011 } ], - "environment": [ - { - "name": "NOTIFY_ENVIRONMENT", - "value": "performance" - }, + "environmentFiles": [ { - "name": "NIGHTLY_NOTIF_CSV_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_FAILURE_REASON_ENABLED", - "value": "True" - }, + "type": "s3", + "value": "arn:aws-us-gov:s3:::vanotify-environment-variables-perf/perf.env" + } + ], + "environment": [ { "name": "FLASK_APP", "value": "application.py" }, - { - "name": "API_HOST_NAME", - "value": "https://perf.api.notifications.va.gov" - }, - { - "name": "DD_ENV", - "value": "perf" - }, - { - "name": "DD_SITE", - "value": "ddog-gov.com" - }, { "name": "DD_SERVICE", "value": "notification-api" - }, - { - "name": "DD_PROFILING_ENABLE_CODE_PROVENANCE", - "value": "True" - }, - { - "name": "DD_PROFILING_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_QUEUE_PREFIX", - "value": "perf-notification-" - }, - { - "name": "STATSD_HOST", - "value": "localhost" - }, - { - "name": "SMS_SENDER_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "GRANICUS_URL", - "value": "https://stage-tms.govdelivery.com" - }, - { - "name": "NOTIFY_EMAIL_FROM_USER", - "value": "stage-notifications" - }, - { - "name": "ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED", - "value": "True" - }, - { - "name": "GOOGLE_ANALYTICS_ENABLED", - "value": "True" - }, - { - "name": "VA_ONSITE_URL", - "value": "https://staging-api.va.gov" - }, - { - "name": "VA_PROFILE_URL", - "value": "https://qa.vaprofile.va.gov" - }, - { - "name": "UI_HOST_NAME", - "value": "https://perf.notifications.va.gov" - }, - { - "name": "MPI_URL", - "value": "https://sqa.services.eauth.va.gov:9303/sqa" - }, - { - "name": "AWS_PINPOINT_APP_ID", - "value": "f8cab892fe2740c2901560b55a398440" - }, - { - "name": "AWS_SES_EMAIL_FROM_USER", - "value": "perf-do-not-reply" - }, - { - "name": "TEMPLATE_SERVICE_PROVIDERS_ENABLED", - "value": "True" - }, - { - "name": "PROVIDER_STRATEGIES_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "LOAD_BALANCING" - }, - { - "name": "SMS_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "HIGHEST_PRIORITY" - }, - { - "name": "PINPOINT_RECEIPTS_ENABLED", - "value": "True" - }, - { - "name": "CHECK_GITHUB_SCOPE_ENABLED", - "value": "False" - }, - { - "name": "REDIS_ENABLED", - "value": "True" - }, - { - "name": "CHECK_TEMPLATE_NAME_EXISTS_ENABLED", - "value": "False" - }, - { - "name": "EMAIL_ATTACHMENTS_ENABLED", - "value": "True" - }, - { - "name": "ATTACHMENTS_BUCKET", - "value": "perf-notifications-va-gov-attachments" - }, - { - "name": "VA_FLAGSHIP_APP_SID", - "value": "A20623E2321D4053A6C34C9307C6C221" - }, - { - "name": "VETEXT_SID", - "value": "C9BEC63F53CE4C1D992CE73E8D1D8D94" - }, - { - "name": "PUSH_NOTIFICATIONS_ENABLED", - "value": "True" - }, - { - "name": "PLATFORM_STATS_ENABLED", - "value": "False" - }, - { - "name": "VA_SSO_ENABLED", - "value": "True" - }, - { - "name": "V3_ENABLED", - "value": "True" - }, - { - "name": "VA_SSO_SERVER_METADATA_URL", - "value": "https://preprod.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/metadata/ISAMOP/.well-known/oauth-authorization-server" - }, - { - "name": "VA_SSO_AUTHORIZE_URL", - "value": "https://preprod.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/authorize" - }, - { - "name": "VA_SSO_ACCESS_TOKEN_URL", - "value": "https://preprod.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/token" - }, - { - "name": "COMP_AND_PEN_MESSAGES_ENABLED", - "value": "True" - }, - { - "name": "COMP_AND_PEN_DYNAMODB_NAME", - "value": "perf-bip-payment-notification-table" } ], "secrets": [ diff --git a/cd/application-deployment/perf/vaec-celery-beat-task-definition.json b/cd/application-deployment/perf/vaec-celery-beat-task-definition.json index 3daf1bfecf..ad4c557121 100644 --- a/cd/application-deployment/perf/vaec-celery-beat-task-definition.json +++ b/cd/application-deployment/perf/vaec-celery-beat-task-definition.json @@ -16,6 +16,12 @@ "awslogs-stream-prefix": "ecs" } }, + "environmentFiles": [ + { + "type": "s3", + "value": "arn:aws-us-gov:s3:::vanotify-environment-variables-perf/perf.env" + } + ], "environment": [ { "name": "NOTIFY_ENVIRONMENT", diff --git a/cd/application-deployment/perf/vaec-celery-task-definition.json b/cd/application-deployment/perf/vaec-celery-task-definition.json index da3857fc52..a4990c0598 100644 --- a/cd/application-deployment/perf/vaec-celery-task-definition.json +++ b/cd/application-deployment/perf/vaec-celery-task-definition.json @@ -16,178 +16,20 @@ "awslogs-stream-prefix": "ecs" } }, - "environment": [ - { - "name": "SMS_SENDER_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_FAILURE_REASON_ENABLED", - "value": "True" - }, - { - "name": "NIGHTLY_NOTIF_CSV_ENABLED", - "value": "True" - }, + "environmentFiles": [ { - "name": "NOTIFY_ENVIRONMENT", - "value": "performance" - }, + "type": "s3", + "value": "arn:aws-us-gov:s3:::vanotify-environment-variables-perf/perf.env" + } + ], + "environment": [ { "name": "FLASK_APP", "value": "run_celery.py" }, - { - "name": "API_HOST_NAME", - "value": "https://perf.api.notifications.va.gov" - }, - { - "name": "DD_ENV", - "value": "perf" - }, - { - "name": "DD_SITE", - "value": "ddog-gov.com" - }, { "name": "DD_SERVICE", "value": "celery" - }, - { - "name": "DD_PROFILING_ENABLE_CODE_PROVENANCE", - "value": "True" - }, - { - "name": "DD_PROFILING_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_QUEUE_PREFIX", - "value": "perf-notification-" - }, - { - "name": "STATSD_HOST", - "value": "localhost" - }, - { - "name": "GRANICUS_URL", - "value": "https://stage-tms.govdelivery.com" - }, - { - "name": "NOTIFY_EMAIL_FROM_USER", - "value": "stage-notifications" - }, - { - "name": "ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED", - "value": "True" - }, - { - "name": "GOOGLE_ANALYTICS_ENABLED", - "value": "True" - }, - { - "name": "GA4_URL", - "value": "https://www.google-analytics.com/mp/collect" - }, - { - "name": "VA_ONSITE_URL", - "value": "https://staging-api.va.gov" - }, - { - "name": "VA_PROFILE_URL", - "value": "https://qa.vaprofile.va.gov" - }, - { - "name": "VA_PROFILE_EMAIL_STATUS_ENABLED", - "value": "True" - }, - { - "name": "VANOTIFY_SSL_CERT_PATH", - "value": "/app/certs/vanotify_ssl_cert.pem" - }, - { - "name": "VANOTIFY_SSL_KEY_PATH", - "value": "/app/certs/vanotify_ssl_key.pem" - }, - { - "name": "MPI_URL", - "value": "https://sqa.services.eauth.va.gov:9303/sqa" - }, - { - "name": "AWS_PINPOINT_APP_ID", - "value": "f8cab892fe2740c2901560b55a398440" - }, - { - "name": "AWS_SES_EMAIL_FROM_USER", - "value": "perf-do-not-reply" - }, - { - "name": "TEMPLATE_SERVICE_PROVIDERS_ENABLED", - "value": "True" - }, - { - "name": "PROVIDER_STRATEGIES_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "LOAD_BALANCING" - }, - { - "name": "SMS_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "HIGHEST_PRIORITY" - }, - { - "name": "PINPOINT_RECEIPTS_ENABLED", - "value": "True" - }, - { - "name": "CHECK_GITHUB_SCOPE_ENABLED", - "value": "False" - }, - { - "name": "REDIS_ENABLED", - "value": "True" - }, - { - "name": "CHECK_TEMPLATE_NAME_EXISTS_ENABLED", - "value": "False" - }, - { - "name": "EMAIL_ATTACHMENTS_ENABLED", - "value": "True" - }, - { - "name": "ATTACHMENTS_BUCKET", - "value": "perf-notifications-va-gov-attachments" - }, - { - "name": "VA_FLAGSHIP_APP_SID", - "value": "A20623E2321D4053A6C34C9307C6C221" - }, - { - "name": "VETEXT_SID", - "value": "C9BEC63F53CE4C1D992CE73E8D1D8D94" - }, - { - "name": "PUSH_NOTIFICATIONS_ENABLED", - "value": "True" - }, - { - "name": "PLATFORM_STATS_ENABLED", - "value": "False" - }, - { - "name": "COMP_AND_PEN_MESSAGES_ENABLED", - "value": "True" - }, - { - "name": "COMP_AND_PEN_DYNAMODB_NAME", - "value": "perf-bip-payment-notification-table" - }, - { - "name": "COMP_AND_PEN_PERF_TO_NUMBER", - "value": "+14254147755" } ], "secrets": [ diff --git a/cd/application-deployment/prod/prod.env b/cd/application-deployment/prod/prod.env new file mode 100644 index 0000000000..a411657696 --- /dev/null +++ b/cd/application-deployment/prod/prod.env @@ -0,0 +1,51 @@ +ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED=True +API_HOST_NAME=https://api.notifications.va.gov +ATTACHMENTS_BUCKET=prod-notifications-va-gov-attachments +AWS_PINPOINT_APP_ID=9535150638b04a49b49755af2b2d316b +AWS_SES_EMAIL_FROM_USER=do-not-reply +CHECK_GITHUB_SCOPE_ENABLED=False +CHECK_TEMPLATE_NAME_EXISTS_ENABLED=False +COMP_AND_PEN_DYNAMODB_NAME=prod-bip-payment-notification-table +COMP_AND_PEN_MESSAGES_ENABLED=True +DD_ENV=prod +DD_PROFILING_ENABLED=True +DD_PROFILING_ENABLE_CODE_PROVENANCE=True +DD_SERVICE=celery-beat +DD_SITE=ddog-gov.com +EMAIL_ATTACHMENTS_ENABLED=True +EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL=LOAD_BALANCING +FLASK_APP=run_celery_beat.py +GA4_URL=https://www.google-analytics.com/mp/collect +GITHUB_LOGIN_ENABLED=True +GOOGLE_ANALYTICS_ENABLED=True +GOOGLE_ANALYTICS_TID=UA-50123418-16 +MPI_URL=https://services.eauth.va.gov:9303/prod +NIGHTLY_NOTIF_CSV_ENABLED=True +NOTIFICATION_FAILURE_REASON_ENABLED=True +NOTIFICATION_QUEUE_PREFIX=prod-notification- +NOTIFY_ENVIRONMENT=production +PINPOINT_INBOUND_SMS_ENABLED=True +PINPOINT_RECEIPTS_ENABLED=True +PLATFORM_STATS_ENABLED=False +PROVIDER_STRATEGIES_ENABLED=True +PUSH_NOTIFICATIONS_ENABLED=True +REDIS_ENABLED=True +SMS_PROVIDER_SELECTION_STRATEGY_LABEL=HIGHEST_PRIORITY +SMS_SENDER_RATE_LIMIT_ENABLED=True +STATSD_HOST=localhost +TEMPLATE_SERVICE_PROVIDERS_ENABLED=True +TWILIO_ACCOUNT_SID=fake +TWILIO_AUTH_TOKEN=fake +UI_HOST_NAME=https://notifications.va.gov +V3_ENABLED=False +VANOTIFY_SSL_CERT_PATH=/app/certs/vanotify_ssl_cert.pem +VANOTIFY_SSL_KEY_PATH=/app/certs/vanotify_ssl_key.pem +VA_FLAGSHIP_APP_SID=D288162B716A4F55B9F5CF9AA9DDEA5E +VA_ONSITE_URL=https://api.va.gov +VA_PROFILE_EMAIL_STATUS_ENABLED=True +VA_PROFILE_URL=https://www.vaprofile.va.gov +VA_SSO_ACCESS_TOKEN_URL=https://fed.eauth.va.gov/oauthi/sps/oauth/oauth20/token +VA_SSO_AUTHORIZE_URL=https://fed.eauth.va.gov/oauthi/sps/oauth/oauth20/authorize +VA_SSO_ENABLED=True +VA_SSO_SERVER_METADATA_URL=https://fed.eauth.va.gov/oauthi/sps/oauth/oauth20/metadata/ISAMOP/.well-known/oauth-authorization-server +VETEXT_URL=https://alb.api.vetext.va.gov/api/vetext/pub diff --git a/cd/application-deployment/prod/vaec-api-task-definition.json b/cd/application-deployment/prod/vaec-api-task-definition.json index 0898d56594..a11b881b38 100644 --- a/cd/application-deployment/prod/vaec-api-task-definition.json +++ b/cd/application-deployment/prod/vaec-api-task-definition.json @@ -22,186 +22,20 @@ "hostPort": 6011 } ], - "environment": [ - { - "name": "SMS_SENDER_RATE_LIMIT_ENABLED", - "value": "True" - }, + "environmentFiles": [ { - "name": "NOTIFY_ENVIRONMENT", - "value": "production" - }, - { - "name": "NIGHTLY_NOTIF_CSV_ENABLED", - "value": "True" - }, + "type": "s3", + "value": "arn:aws-us-gov:s3:::vanotify-environment-variables-prod/prod.env" + } + ], + "environment": [ { "name": "FLASK_APP", "value": "application.py" }, - { - "name": "API_HOST_NAME", - "value": "https://api.notifications.va.gov" - }, - { - "name": "DD_ENV", - "value": "prod" - }, - { - "name": "DD_SITE", - "value": "ddog-gov.com" - }, { "name": "DD_SERVICE", "value": "notification-api" - }, - { - "name": "DD_PROFILING_ENABLE_CODE_PROVENANCE", - "value": "True" - }, - { - "name": "DD_PROFILING_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_QUEUE_PREFIX", - "value": "prod-notification-" - }, - { - "name": "STATSD_HOST", - "value": "localhost" - }, - { - "name": "ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED", - "value": "True" - }, - { - "name": "GOOGLE_ANALYTICS_ENABLED", - "value": "True" - }, - { - "name": "GOOGLE_ANALYTICS_TID", - "value": "UA-50123418-16" - }, - { - "name": "VA_ONSITE_URL", - "value": "https://api.va.gov" - }, - { - "name": "VA_PROFILE_URL", - "value": "https://www.vaprofile.va.gov" - }, - { - "name": "MPI_URL", - "value": "https://services.eauth.va.gov:9303/prod" - }, - { - "name": "AWS_PINPOINT_APP_ID", - "value": "9535150638b04a49b49755af2b2d316b" - }, - { - "name": "AWS_SES_EMAIL_FROM_USER", - "value": "do-not-reply" - }, - { - "name": "TEMPLATE_SERVICE_PROVIDERS_ENABLED", - "value": "True" - }, - { - "name": "PROVIDER_STRATEGIES_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_FAILURE_REASON_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "LOAD_BALANCING" - }, - { - "name": "SMS_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "HIGHEST_PRIORITY" - }, - { - "name": "PINPOINT_RECEIPTS_ENABLED", - "value": "True" - }, - { - "name": "CHECK_GITHUB_SCOPE_ENABLED", - "value": "False" - }, - { - "name": "GITHUB_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "PINPOINT_INBOUND_SMS_ENABLED", - "value": "True" - }, - { - "name": "REDIS_ENABLED", - "value": "True" - }, - { - "name": "CHECK_TEMPLATE_NAME_EXISTS_ENABLED", - "value": "False" - }, - { - "name": "EMAIL_ATTACHMENTS_ENABLED", - "value": "True" - }, - { - "name": "UI_HOST_NAME", - "value": "https://notifications.va.gov" - }, - { - "name": "ATTACHMENTS_BUCKET", - "value": "prod-notifications-va-gov-attachments" - }, - { - "name": "VETEXT_URL", - "value": "https://alb.api.vetext.va.gov/api/vetext/pub" - }, - { - "name": "VA_FLAGSHIP_APP_SID", - "value": "D288162B716A4F55B9F5CF9AA9DDEA5E" - }, - { - "name": "PUSH_NOTIFICATIONS_ENABLED", - "value": "True" - }, - { - "name": "PLATFORM_STATS_ENABLED", - "value": "False" - }, - { - "name": "VA_SSO_ENABLED", - "value": "True" - }, - { - "name": "V3_ENABLED", - "value": "False" - }, - { - "name": "VA_SSO_SERVER_METADATA_URL", - "value": "https://fed.eauth.va.gov/oauthi/sps/oauth/oauth20/metadata/ISAMOP/.well-known/oauth-authorization-server" - }, - { - "name": "VA_SSO_AUTHORIZE_URL", - "value": "https://fed.eauth.va.gov/oauthi/sps/oauth/oauth20/authorize" - }, - { - "name": "VA_SSO_ACCESS_TOKEN_URL", - "value": "https://fed.eauth.va.gov/oauthi/sps/oauth/oauth20/token" - }, - { - "name": "COMP_AND_PEN_MESSAGES_ENABLED", - "value": "False" - }, - { - "name": "COMP_AND_PEN_DYNAMODB_NAME", - "value": "prod-bip-payment-notification-table" } ], "secrets": [ diff --git a/cd/application-deployment/prod/vaec-celery-beat-task-definition.json b/cd/application-deployment/prod/vaec-celery-beat-task-definition.json index 088d7f39de..bbf9b9694f 100644 --- a/cd/application-deployment/prod/vaec-celery-beat-task-definition.json +++ b/cd/application-deployment/prod/vaec-celery-beat-task-definition.json @@ -16,162 +16,20 @@ "awslogs-stream-prefix": "ecs" } }, - "environment": [ + "environmentFiles": [ { - "name": "NOTIFY_ENVIRONMENT", - "value": "production" - }, + "type": "s3", + "value": "arn:aws-us-gov:s3:::vanotify-environment-variables-prod/prod.env" + } + ], + "environment": [ { "name": "FLASK_APP", "value": "run_celery_beat.py" }, - { - "name": "SMS_SENDER_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "NIGHTLY_NOTIF_CSV_ENABLED", - "value": "True" - }, - { - "name": "API_HOST_NAME", - "value": "https://api.notifications.va.gov" - }, - { - "name": "DD_ENV", - "value": "prod" - }, - { - "name": "DD_SITE", - "value": "ddog-gov.com" - }, { "name": "DD_SERVICE", "value": "celery-beat" - }, - { - "name": "DD_PROFILING_ENABLE_CODE_PROVENANCE", - "value": "True" - }, - { - "name": "DD_PROFILING_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_QUEUE_PREFIX", - "value": "prod-notification-" - }, - { - "name": "STATSD_HOST", - "value": "localhost" - }, - { - "name": "ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED", - "value": "True" - }, - { - "name": "GOOGLE_ANALYTICS_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_FAILURE_REASON_ENABLED", - "value": "True" - }, - { - "name": "GOOGLE_ANALYTICS_TID", - "value": "UA-50123418-16" - }, - { - "name": "VA_ONSITE_URL", - "value": "https://api.va.gov" - }, - { - "name": "VA_PROFILE_URL", - "value": "https://www.vaprofile.va.gov" - }, - { - "name": "VANOTIFY_SSL_CERT_PATH", - "value": "/app/certs/vanotify_ssl_cert.pem" - }, - { - "name": "VANOTIFY_SSL_KEY_PATH", - "value": "/app/certs/vanotify_ssl_key.pem" - }, - { - "name": "MPI_URL", - "value": "https://services.eauth.va.gov:9303/prod" - }, - { - "name": "AWS_PINPOINT_APP_ID", - "value": "9535150638b04a49b49755af2b2d316b" - }, - { - "name": "AWS_SES_EMAIL_FROM_USER", - "value": "do-not-reply" - }, - { - "name": "TEMPLATE_SERVICE_PROVIDERS_ENABLED", - "value": "True" - }, - { - "name": "PROVIDER_STRATEGIES_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "LOAD_BALANCING" - }, - { - "name": "SMS_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "HIGHEST_PRIORITY" - }, - { - "name": "PINPOINT_RECEIPTS_ENABLED", - "value": "True" - }, - { - "name": "CHECK_GITHUB_SCOPE_ENABLED", - "value": "False" - }, - { - "name": "GITHUB_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "PINPOINT_INBOUND_SMS_ENABLED", - "value": "True" - }, - { - "name": "REDIS_ENABLED", - "value": "True" - }, - { - "name": "CHECK_TEMPLATE_NAME_EXISTS_ENABLED", - "value": "False" - }, - { - "name": "EMAIL_ATTACHMENTS_ENABLED", - "value": "True" - }, - { - "name": "UI_HOST_NAME", - "value": "https://notifications.va.gov" - }, - { - "name": "ATTACHMENTS_BUCKET", - "value": "prod-notifications-va-gov-attachments" - }, - { - "name": "PLATFORM_STATS_ENABLED", - "value": "False" - }, - { - "name": "COMP_AND_PEN_MESSAGES_ENABLED", - "value": "False" - }, - { - "name": "COMP_AND_PEN_DYNAMODB_NAME", - "value": "prod-bip-payment-notification-table" } ], "secrets": [ diff --git a/cd/application-deployment/prod/vaec-celery-task-definition.json b/cd/application-deployment/prod/vaec-celery-task-definition.json index 69ded18322..9701abe42a 100644 --- a/cd/application-deployment/prod/vaec-celery-task-definition.json +++ b/cd/application-deployment/prod/vaec-celery-task-definition.json @@ -16,182 +16,20 @@ "awslogs-stream-prefix": "ecs" } }, - "environment": [ + "environmentFiles": [ { - "name": "NOTIFY_ENVIRONMENT", - "value": "production" - }, + "type": "s3", + "value": "arn:aws-us-gov:s3:::vanotify-environment-variables-prod/prod.env" + } + ], + "environment": [ { "name": "FLASK_APP", "value": "run_celery.py" }, - { - "name": "SMS_SENDER_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "NIGHTLY_NOTIF_CSV_ENABLED", - "value": "True" - }, - { - "name": "API_HOST_NAME", - "value": "https://api.notifications.va.gov" - }, - { - "name": "DD_ENV", - "value": "prod" - }, - { - "name": "DD_SITE", - "value": "ddog-gov.com" - }, { "name": "DD_SERVICE", "value": "celery" - }, - { - "name": "DD_PROFILING_ENABLE_CODE_PROVENANCE", - "value": "True" - }, - { - "name": "DD_PROFILING_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_QUEUE_PREFIX", - "value": "prod-notification-" - }, - { - "name": "STATSD_HOST", - "value": "localhost" - }, - { - "name": "ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_FAILURE_REASON_ENABLED", - "value": "True" - }, - { - "name": "GOOGLE_ANALYTICS_ENABLED", - "value": "True" - }, - { - "name": "GOOGLE_ANALYTICS_TID", - "value": "UA-50123418-16" - }, - { - "name": "GA4_URL", - "value": "https://www.google-analytics.com/mp/collect" - }, - { - "name": "VA_ONSITE_URL", - "value": "https://api.va.gov" - }, - { - "name": "VA_PROFILE_URL", - "value": "https://www.vaprofile.va.gov" - }, - { - "name": "VA_PROFILE_EMAIL_STATUS_ENABLED", - "value": "True" - }, - { - "name": "VANOTIFY_SSL_CERT_PATH", - "value": "/app/certs/vanotify_ssl_cert.pem" - }, - { - "name": "VANOTIFY_SSL_KEY_PATH", - "value": "/app/certs/vanotify_ssl_key.pem" - }, - { - "name": "MPI_URL", - "value": "https://services.eauth.va.gov:9303/prod" - }, - { - "name": "AWS_PINPOINT_APP_ID", - "value": "9535150638b04a49b49755af2b2d316b" - }, - { - "name": "AWS_SES_EMAIL_FROM_USER", - "value": "do-not-reply" - }, - { - "name": "TEMPLATE_SERVICE_PROVIDERS_ENABLED", - "value": "True" - }, - { - "name": "PROVIDER_STRATEGIES_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "LOAD_BALANCING" - }, - { - "name": "SMS_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "HIGHEST_PRIORITY" - }, - { - "name": "PINPOINT_RECEIPTS_ENABLED", - "value": "True" - }, - { - "name": "CHECK_GITHUB_SCOPE_ENABLED", - "value": "False" - }, - { - "name": "GITHUB_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "PINPOINT_INBOUND_SMS_ENABLED", - "value": "True" - }, - { - "name": "REDIS_ENABLED", - "value": "True" - }, - { - "name": "CHECK_TEMPLATE_NAME_EXISTS_ENABLED", - "value": "False" - }, - { - "name": "EMAIL_ATTACHMENTS_ENABLED", - "value": "True" - }, - { - "name": "UI_HOST_NAME", - "value": "https://notifications.va.gov" - }, - { - "name": "ATTACHMENTS_BUCKET", - "value": "prod-notifications-va-gov-attachments" - }, - { - "name": "VETEXT_URL", - "value": "https://alb.api.vetext.va.gov/api/vetext/pub" - }, - { - "name": "VA_FLAGSHIP_APP_SID", - "value": "D288162B716A4F55B9F5CF9AA9DDEA5E" - }, - { - "name": "PUSH_NOTIFICATIONS_ENABLED", - "value": "True" - }, - { - "name": "PLATFORM_STATS_ENABLED", - "value": "False" - }, - { - "name": "COMP_AND_PEN_MESSAGES_ENABLED", - "value": "True" - }, - { - "name": "COMP_AND_PEN_DYNAMODB_NAME", - "value": "prod-bip-payment-notification-table" } ], "secrets": [ diff --git a/cd/application-deployment/staging/staging.env b/cd/application-deployment/staging/staging.env new file mode 100644 index 0000000000..926ecebd9c --- /dev/null +++ b/cd/application-deployment/staging/staging.env @@ -0,0 +1,55 @@ +ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED=True +API_HOST_NAME=https://staging.api.notifications.va.gov +API_MESSAGE_LIMIT_ENABLED=True +API_RATE_LIMIT_ENABLED=True +ATTACHMENTS_BUCKET=staging-notifications-va-gov-attachments +AWS_PINPOINT_APP_ID=164e77155a7a45299b3bc15562732540 +AWS_SES_EMAIL_FROM_USER=staging-do-not-reply +CHECK_GITHUB_SCOPE_ENABLED=False +CHECK_TEMPLATE_NAME_EXISTS_ENABLED=False +COMP_AND_PEN_DYNAMODB_NAME=staging-bip-payment-notification-table +COMP_AND_PEN_MESSAGES_ENABLED=True +DD_ENV=staging +DD_PROFILING_ENABLED=True +DD_PROFILING_ENABLE_CODE_PROVENANCE=True +DD_SERVICE=celery-beat +DD_SITE=ddog-gov.com +EMAIL_ATTACHMENTS_ENABLED=True +EMAIL_PASSWORD_LOGIN_ENABLED=True +EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL=LOAD_BALANCING +FLASK_APP=run_celery_beat.py +GA4_URL=https://www.google-analytics.com/mp/collect +GITHUB_LOGIN_ENABLED=True +GOOGLE_ANALYTICS_ENABLED=True +GRANICUS_URL=https://stage-tms.govdelivery.com +MPI_URL=https://sqa.services.eauth.va.gov:9303/sqa +NIGHTLY_NOTIF_CSV_ENABLED=True +NOTIFICATION_FAILURE_REASON_ENABLED=True +NOTIFICATION_QUEUE_PREFIX=staging-notification- +NOTIFY_EMAIL_FROM_USER=stage-notifications +NOTIFY_ENVIRONMENT=staging +PINPOINT_INBOUND_SMS_ENABLED=True +PINPOINT_RECEIPTS_ENABLED=True +PLATFORM_STATS_ENABLED=False +PROVIDER_STRATEGIES_ENABLED=True +PUSH_NOTIFICATIONS_ENABLED=True +REDIS_ENABLED=True +SMS_PROVIDER_SELECTION_STRATEGY_LABEL=HIGHEST_PRIORITY +SMS_SENDER_RATE_LIMIT_ENABLED=True +STATSD_HOST=localhost +TEMPLATE_SERVICE_PROVIDERS_ENABLED=True +TWILIO_ACCOUNT_SID=fake +TWILIO_AUTH_TOKEN=fake +UI_HOST_NAME=https://staging.notifications.va.gov +V3_ENABLED=False +VANOTIFY_SSL_CERT_PATH=/app/certs/vanotify_ssl_cert.pem +VANOTIFY_SSL_KEY_PATH=/app/certs/vanotify_ssl_key.pem +VA_FLAGSHIP_APP_SID=A20623E2321D4053A6C34C9307C6C221 +VA_ONSITE_URL=https://staging-api.va.gov +VA_PROFILE_EMAIL_STATUS_ENABLED=True +VA_PROFILE_URL=https://qa.vaprofile.va.gov +VA_SSO_ACCESS_TOKEN_URL=https://sqa.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/token +VA_SSO_AUTHORIZE_URL=https://sqa.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/authorize +VA_SSO_ENABLED=True +VA_SSO_SERVER_METADATA_URL=https://sqa.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/metadata/ISAMOP/.well-known/oauth-authorization-server +VETEXT_SID=C9BEC63F53CE4C1D992CE73E8D1D8D94 diff --git a/cd/application-deployment/staging/vaec-api-task-definition.json b/cd/application-deployment/staging/vaec-api-task-definition.json index c8c8508bea..53ae0e3800 100644 --- a/cd/application-deployment/staging/vaec-api-task-definition.json +++ b/cd/application-deployment/staging/vaec-api-task-definition.json @@ -22,202 +22,20 @@ "hostPort": 6011 } ], - "environment": [ - { - "name": "NOTIFY_ENVIRONMENT", - "value": "staging" - }, + "environmentFiles": [ { - "name": "NIGHTLY_NOTIF_CSV_ENABLED", - "value": "True" - }, - { - "name": "SMS_SENDER_RATE_LIMIT_ENABLED", - "value": "True" - }, + "type": "s3", + "value": "arn:aws-us-gov:s3:::vanotify-environment-variables-staging/staging.env" + } + ], + "environment": [ { "name": "FLASK_APP", "value": "application.py" }, - { - "name": "API_HOST_NAME", - "value": "https://staging.api.notifications.va.gov" - }, - { - "name": "DD_ENV", - "value": "staging" - }, - { - "name": "DD_SITE", - "value": "ddog-gov.com" - }, { "name": "DD_SERVICE", "value": "notification-api" - }, - { - "name": "DD_PROFILING_ENABLE_CODE_PROVENANCE", - "value": "True" - }, - { - "name": "DD_PROFILING_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_QUEUE_PREFIX", - "value": "staging-notification-" - }, - { - "name": "STATSD_HOST", - "value": "localhost" - }, - { - "name": "GRANICUS_URL", - "value": "https://stage-tms.govdelivery.com" - }, - { - "name": "NOTIFY_EMAIL_FROM_USER", - "value": "stage-notifications" - }, - { - "name": "ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED", - "value": "True" - }, - { - "name": "GOOGLE_ANALYTICS_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_FAILURE_REASON_ENABLED", - "value": "True" - }, - { - "name": "VA_ONSITE_URL", - "value": "https://staging-api.va.gov" - }, - { - "name": "VA_PROFILE_URL", - "value": "https://qa.vaprofile.va.gov" - }, - { - "name": "MPI_URL", - "value": "https://sqa.services.eauth.va.gov:9303/sqa" - }, - { - "name": "AWS_PINPOINT_APP_ID", - "value": "164e77155a7a45299b3bc15562732540" - }, - { - "name": "AWS_SES_EMAIL_FROM_USER", - "value": "staging-do-not-reply" - }, - { - "name": "TEMPLATE_SERVICE_PROVIDERS_ENABLED", - "value": "True" - }, - { - "name": "PROVIDER_STRATEGIES_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "LOAD_BALANCING" - }, - { - "name": "SMS_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "HIGHEST_PRIORITY" - }, - { - "name": "PINPOINT_RECEIPTS_ENABLED", - "value": "True" - }, - { - "name": "GITHUB_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "UI_HOST_NAME", - "value": "https://staging.notifications.va.gov" - }, - { - "name": "EMAIL_PASSWORD_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "CHECK_GITHUB_SCOPE_ENABLED", - "value": "False" - }, - { - "name": "PINPOINT_INBOUND_SMS_ENABLED", - "value": "True" - }, - { - "name": "REDIS_ENABLED", - "value": "True" - }, - { - "name": "API_MESSAGE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "API_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "CHECK_TEMPLATE_NAME_EXISTS_ENABLED", - "value": "False" - }, - { - "name": "EMAIL_ATTACHMENTS_ENABLED", - "value": "True" - }, - { - "name": "ATTACHMENTS_BUCKET", - "value": "staging-notifications-va-gov-attachments" - }, - { - "name": "VA_FLAGSHIP_APP_SID", - "value": "A20623E2321D4053A6C34C9307C6C221" - }, - { - "name": "VETEXT_SID", - "value": "C9BEC63F53CE4C1D992CE73E8D1D8D94" - }, - { - "name": "PUSH_NOTIFICATIONS_ENABLED", - "value": "True" - }, - { - "name": "PLATFORM_STATS_ENABLED", - "value": "False" - }, - { - "name": "VA_SSO_ENABLED", - "value": "True" - }, - { - "name": "V3_ENABLED", - "value": "False" - }, - { - "name": "VA_SSO_SERVER_METADATA_URL", - "value": "https://sqa.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/metadata/ISAMOP/.well-known/oauth-authorization-server" - }, - { - "name": "VA_SSO_AUTHORIZE_URL", - "value": "https://sqa.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/authorize" - }, - { - "name": "VA_SSO_ACCESS_TOKEN_URL", - "value": "https://sqa.fed.eauth.va.gov/oauthi/sps/oauth/oauth20/token" - }, - { - "name": "COMP_AND_PEN_MESSAGES_ENABLED", - "value": "False" - }, - { - "name": "COMP_AND_PEN_DYNAMODB_NAME", - "value": "staging-bip-payment-notification-table" } ], "secrets": [ diff --git a/cd/application-deployment/staging/vaec-celery-beat-task-definition.json b/cd/application-deployment/staging/vaec-celery-beat-task-definition.json index 564715402c..5cd0a981d7 100644 --- a/cd/application-deployment/staging/vaec-celery-beat-task-definition.json +++ b/cd/application-deployment/staging/vaec-celery-beat-task-definition.json @@ -16,178 +16,20 @@ "awslogs-stream-prefix": "ecs" } }, - "environment": [ + "environmentFiles": [ { - "name": "NOTIFY_ENVIRONMENT", - "value": "staging" - }, + "type": "s3", + "value": "arn:aws-us-gov:s3:::vanotify-environment-variables-staging/staging.env" + } + ], + "environment": [ { "name": "FLASK_APP", "value": "run_celery_beat.py" }, - { - "name": "SMS_SENDER_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "NIGHTLY_NOTIF_CSV_ENABLED", - "value": "True" - }, - { - "name": "API_HOST_NAME", - "value": "https://staging.api.notifications.va.gov" - }, - { - "name": "DD_ENV", - "value": "staging" - }, - { - "name": "DD_SITE", - "value": "ddog-gov.com" - }, { "name": "DD_SERVICE", "value": "celery-beat" - }, - { - "name": "DD_PROFILING_ENABLE_CODE_PROVENANCE", - "value": "True" - }, - { - "name": "DD_PROFILING_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_QUEUE_PREFIX", - "value": "staging-notification-" - }, - { - "name": "STATSD_HOST", - "value": "localhost" - }, - { - "name": "GRANICUS_URL", - "value": "https://stage-tms.govdelivery.com" - }, - { - "name": "NOTIFY_EMAIL_FROM_USER", - "value": "stage-notifications" - }, - { - "name": "ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED", - "value": "True" - }, - { - "name": "GOOGLE_ANALYTICS_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_FAILURE_REASON_ENABLED", - "value": "True" - }, - { - "name": "VA_ONSITE_URL", - "value": "https://staging-api.va.gov" - }, - { - "name": "VA_PROFILE_URL", - "value": "https://qa.vaprofile.va.gov" - }, - { - "name": "VANOTIFY_SSL_CERT_PATH", - "value": "/app/certs/vanotify_ssl_cert.pem" - }, - { - "name": "VANOTIFY_SSL_KEY_PATH", - "value": "/app/certs/vanotify_ssl_key.pem" - }, - { - "name": "MPI_URL", - "value": "https://sqa.services.eauth.va.gov:9303/sqa" - }, - { - "name": "AWS_PINPOINT_APP_ID", - "value": "164e77155a7a45299b3bc15562732540" - }, - { - "name": "AWS_SES_EMAIL_FROM_USER", - "value": "staging-do-not-reply" - }, - { - "name": "TEMPLATE_SERVICE_PROVIDERS_ENABLED", - "value": "True" - }, - { - "name": "PROVIDER_STRATEGIES_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "LOAD_BALANCING" - }, - { - "name": "SMS_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "HIGHEST_PRIORITY" - }, - { - "name": "PINPOINT_RECEIPTS_ENABLED", - "value": "True" - }, - { - "name": "GITHUB_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "UI_HOST_NAME", - "value": "https://staging.notifications.va.gov" - }, - { - "name": "EMAIL_PASSWORD_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "CHECK_GITHUB_SCOPE_ENABLED", - "value": "False" - }, - { - "name": "PINPOINT_INBOUND_SMS_ENABLED", - "value": "True" - }, - { - "name": "REDIS_ENABLED", - "value": "True" - }, - { - "name": "API_MESSAGE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "API_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "CHECK_TEMPLATE_NAME_EXISTS_ENABLED", - "value": "False" - }, - { - "name": "EMAIL_ATTACHMENTS_ENABLED", - "value": "True" - }, - { - "name": "ATTACHMENTS_BUCKET", - "value": "staging-notifications-va-gov-attachments" - }, - { - "name": "PLATFORM_STATS_ENABLED", - "value": "False" - }, - { - "name": "COMP_AND_PEN_MESSAGES_ENABLED", - "value": "False" - }, - { - "name": "COMP_AND_PEN_DYNAMODB_NAME", - "value": "staging-bip-payment-notification-table" } ], "secrets": [ diff --git a/cd/application-deployment/staging/vaec-celery-task-definition.json b/cd/application-deployment/staging/vaec-celery-task-definition.json index 1722bc517c..b2a35c9d16 100644 --- a/cd/application-deployment/staging/vaec-celery-task-definition.json +++ b/cd/application-deployment/staging/vaec-celery-task-definition.json @@ -16,198 +16,20 @@ "awslogs-stream-prefix": "ecs" } }, - "environment": [ + "environmentFiles": [ { - "name": "NOTIFY_ENVIRONMENT", - "value": "staging" - }, + "type": "s3", + "value": "arn:aws-us-gov:s3:::vanotify-environment-variables-staging/staging.env" + } + ], + "environment": [ { "name": "FLASK_APP", "value": "run_celery.py" }, - { - "name": "NIGHTLY_NOTIF_CSV_ENABLED", - "value": "True" - }, - { - "name": "API_HOST_NAME", - "value": "https://staging.api.notifications.va.gov" - }, - { - "name": "DD_ENV", - "value": "staging" - }, - { - "name": "DD_SITE", - "value": "ddog-gov.com" - }, { "name": "DD_SERVICE", "value": "celery" - }, - { - "name": "DD_PROFILING_ENABLE_CODE_PROVENANCE", - "value": "True" - }, - { - "name": "DD_PROFILING_ENABLED", - "value": "True" - }, - { - "name": "SMS_SENDER_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "NOTIFICATION_QUEUE_PREFIX", - "value": "staging-notification-" - }, - { - "name": "STATSD_HOST", - "value": "localhost" - }, - { - "name": "GRANICUS_URL", - "value": "https://stage-tms.govdelivery.com" - }, - { - "name": "NOTIFY_EMAIL_FROM_USER", - "value": "stage-notifications" - }, - { - "name": "ACCEPT_RECIPIENT_IDENTIFIERS_ENABLED", - "value": "True" - }, - { - "name": "GOOGLE_ANALYTICS_ENABLED", - "value": "True" - }, - { - "name": "GA4_URL", - "value": "https://www.google-analytics.com/mp/collect" - }, - { - "name": "NOTIFICATION_FAILURE_REASON_ENABLED", - "value": "True" - }, - { - "name": "VA_ONSITE_URL", - "value": "https://staging-api.va.gov" - }, - { - "name": "VA_PROFILE_URL", - "value": "https://qa.vaprofile.va.gov" - }, - { - "name": "VA_PROFILE_EMAIL_STATUS_ENABLED", - "value": "True" - }, - { - "name": "VANOTIFY_SSL_CERT_PATH", - "value": "/app/certs/vanotify_ssl_cert.pem" - }, - { - "name": "VANOTIFY_SSL_KEY_PATH", - "value": "/app/certs/vanotify_ssl_key.pem" - }, - { - "name": "MPI_URL", - "value": "https://sqa.services.eauth.va.gov:9303/sqa" - }, - { - "name": "AWS_PINPOINT_APP_ID", - "value": "164e77155a7a45299b3bc15562732540" - }, - { - "name": "AWS_SES_EMAIL_FROM_USER", - "value": "staging-do-not-reply" - }, - { - "name": "TEMPLATE_SERVICE_PROVIDERS_ENABLED", - "value": "True" - }, - { - "name": "PROVIDER_STRATEGIES_ENABLED", - "value": "True" - }, - { - "name": "EMAIL_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "LOAD_BALANCING" - }, - { - "name": "SMS_PROVIDER_SELECTION_STRATEGY_LABEL", - "value": "HIGHEST_PRIORITY" - }, - { - "name": "PINPOINT_RECEIPTS_ENABLED", - "value": "True" - }, - { - "name": "GITHUB_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "UI_HOST_NAME", - "value": "https://staging.notifications.va.gov" - }, - { - "name": "EMAIL_PASSWORD_LOGIN_ENABLED", - "value": "True" - }, - { - "name": "CHECK_GITHUB_SCOPE_ENABLED", - "value": "False" - }, - { - "name": "PINPOINT_INBOUND_SMS_ENABLED", - "value": "True" - }, - { - "name": "REDIS_ENABLED", - "value": "True" - }, - { - "name": "API_MESSAGE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "API_RATE_LIMIT_ENABLED", - "value": "True" - }, - { - "name": "CHECK_TEMPLATE_NAME_EXISTS_ENABLED", - "value": "False" - }, - { - "name": "EMAIL_ATTACHMENTS_ENABLED", - "value": "True" - }, - { - "name": "ATTACHMENTS_BUCKET", - "value": "staging-notifications-va-gov-attachments" - }, - { - "name": "VA_FLAGSHIP_APP_SID", - "value": "A20623E2321D4053A6C34C9307C6C221" - }, - { - "name": "VETEXT_SID", - "value": "C9BEC63F53CE4C1D992CE73E8D1D8D94" - }, - { - "name": "PUSH_NOTIFICATIONS_ENABLED", - "value": "True" - }, - { - "name": "PLATFORM_STATS_ENABLED", - "value": "False" - }, - { - "name": "COMP_AND_PEN_MESSAGES_ENABLED", - "value": "True" - }, - { - "name": "COMP_AND_PEN_DYNAMODB_NAME", - "value": "staging-bip-payment-notification-table" } ], "secrets": [ From 5ebcd7f437ff8f45286a9f2a58cdb59e2674f04a Mon Sep 17 00:00:00 2001 From: Kyle MacMillan <16893311+k-macmillan@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:56:05 -0400 Subject: [PATCH 5/6] HOTFIX - Phone Lookup and Log Cleanup (#2034) --- .talismanrc | 8 +- app/schema_validation/__init__.py | 24 +++- app/va/va_profile/va_profile_client.py | 11 +- app/va/vetext/client.py | 2 +- tests/app/mobile_app/test_mobile_app.py | 7 -- .../mobile_app/test_mobile_app_registry.py | 17 ++- tests/app/test_validate.py | 106 +++++++++++++++++- .../va/va_profile/test_va_profile_client.py | 38 +++++++ tests/conftest.py | 22 ++-- 9 files changed, 201 insertions(+), 34 deletions(-) diff --git a/.talismanrc b/.talismanrc index ca4f14fd41..a195046ec4 100644 --- a/.talismanrc +++ b/.talismanrc @@ -16,15 +16,15 @@ fileignoreconfig: - filename: app/template/rest.py checksum: 1e5bdac8bc694d50f8f656dec127dd036b7b1b5b6156e3282d3411956c71ba0b - filename: cd/application-deployment/dev/dev.env - checksum: da33c215ecd18a2b3d44aa0d35c70a77b94cd5b2015d958a8103e96e7c707e23 + checksum: a6bed7de359c7cec67940c1f0113826365400684c5a3bd182e8237d48ad5c1f1 - filename: cd/application-deployment/dev/vaec-api-task-definition.json checksum: f328ff821339b802eb1d82559e624d5b719857c813d427da5aaa39b240331ddd - filename: cd/application-deployment/perf/perf.env - checksum: ea3f1cf6feea351e2ad414e7edd55ea98b5c31d93383d890b6ec6dfeb08023c0 + checksum: 1b3b7539dd80b0661594082956e61fd86451692946d845cfe676798aac75618d - filename: cd/application-deployment/prod/prod.env - checksum: a5bc436f5567767174804947dace7b58063e863dc9a2a047362ee797ffe7226b + checksum: 55252b1cb0e16b02301ae8bffb1015f7da5286d4bce0b415a95842cdb368c275 - filename: cd/application-deployment/staging/staging.env - checksum: 786d9297860520e4ac71e491623ac12b98430d6b73e4c33cede67a6afdeaec07 + checksum: 9e5161e8a0a13974d9b67d8a7e61d1b3fed9657a7e2dfeb6d82fd8ace64e2715 - filename: ci/docker-compose-test.yml checksum: e3efec2749e8c19e60f5bfc68eafabe24eba647530a482ceccfc4e0e62cff424 - filename: lambda_functions/pinpoint_callback/pinpoint_callback_lambda.py diff --git a/app/schema_validation/__init__.py b/app/schema_validation/__init__.py index 8f7a9d5fae..5bd5a4160b 100644 --- a/app/schema_validation/__init__.py +++ b/app/schema_validation/__init__.py @@ -68,11 +68,25 @@ def validate( validator = Draft7Validator(schema, format_checker=format_checker) errors = list(validator.iter_errors(json_to_validate)) if len(errors) > 0: - if isinstance(json_to_validate, dict) and 'personalisation' in json_to_validate: - if isinstance(json_to_validate['personalisation'], str): - json_to_validate['personalisation'] = '' - elif isinstance(json_to_validate['personalisation'], dict): - json_to_validate['personalisation'] = {key: '' for key in json_to_validate['personalisation']} + if isinstance(json_to_validate, dict): + # Redact "personalisation" + if 'personalisation' in json_to_validate: + if isinstance(json_to_validate.get('personalisation'), dict): + json_to_validate['personalisation'] = { + key: '' for key in json_to_validate['personalisation'] + } + else: + json_to_validate['personalisation'] = '' + + # Redact ICN + if 'recipient_identifier' in json_to_validate: + if ( + isinstance(json_to_validate.get('recipient_identifier'), dict) # Short circuit dictionary check + and json_to_validate['recipient_identifier'].get('id_type') == 'ICN' + ): + json_to_validate['recipient_identifier']['id_value'] = '' + else: + json_to_validate['recipient_identifier'] = '' current_app.logger.info('Validation failed for: %s', json_to_validate) raise ValidationError(build_error_message(errors)) diff --git a/app/va/va_profile/va_profile_client.py b/app/va/va_profile/va_profile_client.py index d4405d3a0d..1ca159a1a8 100644 --- a/app/va/va_profile/va_profile_client.py +++ b/app/va/va_profile/va_profile_client.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from enum import Enum from http.client import responses +from logging import Logger from typing import TYPE_CHECKING, Optional @@ -85,7 +86,7 @@ def init_app( va_profile_token, statsd_client, ): - self.logger = logger + self.logger: Logger = logger self.va_profile_url = va_profile_url self.ssl_cert_path = ssl_cert_path self.ssl_key_path = ssl_key_path @@ -159,6 +160,14 @@ def get_mobile_telephone_from_contact_info(self, contact_info: ContactInformatio if telephone.get('countryCode') and telephone.get('areaCode') and telephone.get('phoneNumber'): self.statsd_client.incr('clients.va-profile.get-telephone.success') return f"+{telephone['countryCode']}{telephone['areaCode']}{telephone['phoneNumber']}" + else: + self.statsd_client.incr('clients.va-profile.get-telephone.failure') + self.logger.warning( + 'Expected country code: %s | area code: %s | phone number (str length): %s', + telephone.get('countryCode'), + telephone.get('areaCode'), + len(str(telephone.get('phoneNumber', ''))), # Do not log phone numbers. Cast to str to prevent errors. + ) def get_telephone( self, diff --git a/app/va/vetext/client.py b/app/va/vetext/client.py index f2b3c476cd..bf49221f81 100644 --- a/app/va/vetext/client.py +++ b/app/va/vetext/client.py @@ -53,7 +53,7 @@ def send_push_notification( 'templateSid': template_id, 'personalization': formatted_personalization, } - self.logger.info('VEText Payload information: %s', payload) + self.logger.debug('VEText Payload information: %s', payload) try: start_time = monotonic() diff --git a/tests/app/mobile_app/test_mobile_app.py b/tests/app/mobile_app/test_mobile_app.py index 9648e524ad..fefab80cb4 100644 --- a/tests/app/mobile_app/test_mobile_app.py +++ b/tests/app/mobile_app/test_mobile_app.py @@ -25,13 +25,6 @@ def test_mobile_app_init_reads_sid_from_env(mocker, app_type, app_sid): ) def test_mobile_app_raises_exception_at_invalid_sid(client, mocker, app_type: MobileAppType, app_sid: str): mocker.patch.dict(os.environ, {f'{app_type.value}_SID': app_sid}) - mock_logger = mocker.patch('app.mobile_app.mobile_app_registry.current_app.logger.warning') with pytest.raises(ValueError) as e: MobileApp(app_type) assert str(e.value) == f'Missing SID for app: {app_type.value}' - # Ensure logging the enum type and value works as expected - assert mock_logger.called_once_with( - 'Missing environment sid for type: %s and value: %s_SID', - app_type, - app_type.value, - ) diff --git a/tests/app/mobile_app/test_mobile_app_registry.py b/tests/app/mobile_app/test_mobile_app_registry.py index f3d1b216a5..d1758c325c 100644 --- a/tests/app/mobile_app/test_mobile_app_registry.py +++ b/tests/app/mobile_app/test_mobile_app_registry.py @@ -62,7 +62,7 @@ def test_registry_initilizes_only_apps_with_sids_in_env( assert registry.get_registered_apps() == expected_list -def test_should_log_error_for_uninitilized_apps( +def test_should_log_warning_for_uninitialized_apps_with_correct_count( client, mock_logger, mocker, @@ -71,3 +71,18 @@ def test_should_log_error_for_uninitilized_apps( mocker.patch.dict(os.environ, {f'{app}_SID': ''}) MobileAppRegistry() assert mock_logger.warning.call_count == len(MobileAppType.values()) + + +@pytest.mark.parametrize('app_type_str', [*MobileAppType.values()]) +def test_should_correctly_log_warning_for_uninitialized_apps( + client, + mock_logger, + mocker, + app_type_str, +): + mocker.patch.dict(os.environ, {f'{app_type_str}_SID': ''}) + MobileAppRegistry() + app_type = MobileAppType(app_type_str) + mock_logger.warning.assert_called_once_with( + 'Missing environment sid for type: %s and value: %s_SID', app_type, app_type.value + ) diff --git a/tests/app/test_validate.py b/tests/app/test_validate.py index c15ad97934..73952274ec 100644 --- a/tests/app/test_validate.py +++ b/tests/app/test_validate.py @@ -4,11 +4,12 @@ from jsonschema import ValidationError -def test_validate_v2_notifications_redaction(notify_api, caplog): +def test_validate_v2_notifications_personalisation_redaction(notify_api, mocker): """ When POST data validation fails for a Notification, the request body should be logged with personalized information redacted. """ + mock_logger = mocker.patch('app.schema_validation.current_app.logger.info') # This is not valid POST data. notification_POST_request_data = { @@ -20,5 +21,104 @@ def test_validate_v2_notifications_redaction(notify_api, caplog): with pytest.raises(ValidationError): validate(notification_POST_request_data, post_sms_request) - for record in caplog.records: - assert record.args['personalisation']['sensitive_data'] == '' + # Cannot use a variable with loggers and assertions, falsely passes assertions + mock_logger.assert_called_once_with( + 'Validation failed for: %s', {'personalisation': {'sensitive_data': ''}} + ) + + +@pytest.mark.parametrize( + 'personalisation_value', + [ + ['hello', 'world'], + 'a string', + 12345, + {'personalization': 'spelled it wrong'}, + ], +) +def test_validate_v2_notifications_personalisation_redaction_unexpected_format( + notify_api, + mocker, + personalisation_value, +): + """ + When POST data validation fails for a Notification, the request body + should be logged with personalized information redacted. + """ + mock_logger = mocker.patch('app.schema_validation.current_app.logger.info') + with pytest.raises(ValidationError): + validate({'personalisation': personalisation_value}, post_sms_request) + + # Cannot use a variable with loggers and assertions, falsely passes assertions + if isinstance(personalisation_value, dict): + mock_logger.assert_called_once_with( + 'Validation failed for: %s', {'personalisation': {key: '' for key in personalisation_value}} + ) + else: + mock_logger.assert_called_once_with('Validation failed for: %s', {'personalisation': ''}) + + +def test_validate_v2_notifications_icn_redaction( + notify_api, + mocker, +): + """ + When POST data validation fails for a Notification, the request body + should be logged with personalized information redacted. + """ + mock_logger = mocker.patch('app.schema_validation.current_app.logger.info') + with pytest.raises(ValidationError): + validate({'recipient_identifier': {'id_type': 'ICN', 'id_value': '1234567890'}}, post_sms_request) + + # Cannot use a variable with loggers and assertions, falsely passes assertions + mock_logger.assert_called_once_with( + 'Validation failed for: %s', {'recipient_identifier': {'id_type': 'ICN', 'id_value': ''}} + ) + + +@pytest.mark.parametrize( + 'recipient_identifier_value', + [ + ['hello', 'world'], + 'a string', + 12345, + {'id_value': 'not id_type'}, + ], +) +def test_validate_v2_notifications_icn_redaction_unexpected_format( + notify_api, + mocker, + recipient_identifier_value, +): + """ + When POST data validation fails for a Notification, the request body + should be logged with personalized information redacted. + """ + mock_logger = mocker.patch('app.schema_validation.current_app.logger.info') + with pytest.raises(ValidationError): + validate({'recipient_identifier': recipient_identifier_value}, post_sms_request) + + # Cannot use a variable with loggers and assertions, falsely passes assertions + mock_logger.assert_called_once_with('Validation failed for: %s', {'recipient_identifier': ''}) + + +def test_validate_v2_notifications_icn_and_personalisation_redaction( + notify_api, + mocker, +): + """ + When POST data validation fails for a Notification, the request body + should be logged with personalized information redacted. + """ + mock_logger = mocker.patch('app.schema_validation.current_app.logger.info') + with pytest.raises(ValidationError): + validate( + {'recipient_identifier': {'id_type': 'ICN', 'id_value': '1234567890'}, 'personalisation': 'asdf'}, + post_sms_request, + ) + + # Cannot use a variable with loggers and assertions, falsely passes assertions + mock_logger.assert_called_once_with( + 'Validation failed for: %s', + {'recipient_identifier': {'id_type': 'ICN', 'id_value': ''}, 'personalisation': ''}, + ) diff --git a/tests/app/va/va_profile/test_va_profile_client.py b/tests/app/va/va_profile/test_va_profile_client.py index 749caa4324..66ae301295 100644 --- a/tests/app/va/va_profile/test_va_profile_client.py +++ b/tests/app/va/va_profile/test_va_profile_client.py @@ -222,6 +222,44 @@ def test_has_valid_telephone_classification(self, mock_va_profile_client, classi with pytest.raises(InvalidPhoneNumberException): mock_va_profile_client.get_mobile_telephone_from_contact_info(mock_contact_info) + @pytest.mark.parametrize( + 'country_code, area_code, phone_number', + [ + (None, '123', '4567890'), + (1, None, '4567890'), + (1, '123', None), + (None, None, None), + ], + ) + def test_log_unexpected_telephone_build_result(self, mock_va_profile_client, country_code, area_code, phone_number): + telephone_instance: Telephone = { + 'createDate': '2023-10-01', + 'updateDate': '2023-10-02', + 'txAuditId': 'TX123456', + 'sourceSystem': 'SystemA', + 'sourceDate': '2023-10-01', + 'originatingSourceSystem': 'SystemB', + 'sourceSystemUser': 'User123', + 'effectiveStartDate': '2023-10-01', + 'vaProfileId': 12345, + 'telephoneId': 67890, + 'internationalIndicator': False, + 'phoneType': 'MOBILE', + 'countryCode': country_code, + 'areaCode': area_code, + 'phoneNumber': phone_number, + 'classification': {'classificationCode': 0, 'classificationName': 'SOME NAME'}, + } + + mock_contact_info = {'vaProfileId': 'test', 'txAuditId': '1234', 'telephones': [telephone_instance]} + mock_va_profile_client.get_mobile_telephone_from_contact_info(mock_contact_info) + mock_va_profile_client.logger.warning.assert_called_once_with( + 'Expected country code: %s | area code: %s | phone number (str length): %s', + country_code, + area_code, + len(str(phone_number)), + ) + class TestVAProfileClientExceptionHandling: def test_get_telephone_raises_NoContactInfoException_if_no_telephones_exist( diff --git a/tests/conftest.py b/tests/conftest.py index 84f94da3e6..5a133eca7e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,18 +1,18 @@ -from contextlib import contextmanager import os -from time import sleep import warnings +from contextlib import contextmanager - +import pytest import sqlalchemy -from sqlalchemy.sql import delete, select, text as sa_text +import xdist from alembic.command import upgrade from alembic.config import Config -from app import create_app, db, schemas from flask import Flask -import pytest from sqlalchemy.exc import SAWarning +from sqlalchemy.sql import delete, select +from sqlalchemy.sql import text as sa_text +from app import create_app, db, schemas application = None @@ -41,7 +41,7 @@ def pytest_sessionstart(session): error_handlers[None] = { exc_class: error_handler for exc_class, error_handler in error_handlers[None].items() - if exc_class != Exception + if exc_class is not Exception } if error_handlers[None] == []: error_handlers.pop(None) @@ -249,12 +249,13 @@ def pytest_sessionfinish(session, exitstatus): A pytest hook that runs after all tests. Reports database is clear of extra entries after all tests have ran. Exit code is set to 1 if anything is left in any table. """ + # Guard to prevent this from running early + if xdist.is_xdist_worker(session): + return color = '\033[91m' reset = '\033[0m' - SLEEP_DURATION = 5 # Adjustable - Allow fixtures to finish their work, multi-worker fails otherwise TRUNCATE_ARTIFACTS = os.environ['TRUNCATE_ARTIFACTS'] == 'True' - sleep(SLEEP_DURATION) with application.app_context(): with warnings.catch_warnings(): @@ -316,9 +317,6 @@ def pytest_sessionfinish(session, exitstatus): session.exitstatus = 1 if tables_with_artifacts and TRUNCATE_ARTIFACTS: - # Give the tests time to finish self-cleanup - extra time to reduce deadlocks - sleep(SLEEP_DURATION) - print('\n') for i, table in enumerate(tables_with_artifacts): # Skip tables that may have necessary information From 8cd65f077f0878f02e27f794fdc4d637fd64185f Mon Sep 17 00:00:00 2001 From: Kyle MacMillan <16893311+k-macmillan@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:21:55 -0400 Subject: [PATCH 6/6] #2069 - Reduced Write Instance Queries (#2071) --- .talismanrc | 2 ++ README.md | 5 +-- app/__init__.py | 1 + app/callback/webhook_callback_strategy.py | 4 +-- app/celery/process_ga4_measurement_tasks.py | 34 ++++++++++--------- app/celery/process_ses_receipts_tasks.py | 9 ++--- app/celery/service_callback_tasks.py | 4 +-- app/clients/email/govdelivery_client.py | 6 +++- .../performance_platform_client.py | 5 ++- app/cronitor.py | 4 ++- app/notifications/utils.py | 4 ++- app/notifications/validators.py | 4 ++- app/template/rest.py | 5 +-- app/user/rest.py | 7 ++-- app/va/mpi/mpi.py | 6 +++- app/va/va_onsite/va_onsite_client.py | 5 ++- app/va/va_profile/va_profile_client.py | 14 +++++--- .../versions/0372_remove_service_id_index.py | 19 +++++++++++ 18 files changed, 97 insertions(+), 41 deletions(-) create mode 100644 migrations/versions/0372_remove_service_id_index.py diff --git a/.talismanrc b/.talismanrc index a195046ec4..9733aa8cad 100644 --- a/.talismanrc +++ b/.talismanrc @@ -47,4 +47,6 @@ fileignoreconfig: checksum: 4e15e63d349635131173ffdd7aebcd547621db08de877ef926d3a41fde72d065 - filename: tests/app/v2/notifications/test_post_notifications.py checksum: 3181930a13e3679bb2f17eaa3f383512eb9caf4ed5d5e14496ca4193c6083965 +- filename: app/va/va_profile/va_profile_client.py + checksum: fe634f26f7dc3874f4afcfd1ba3f03bae380b53befe973a752c7347097a88701 version: "1.0" diff --git a/README.md b/README.md index d3d2087173..a487e41f49 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,9 @@ To support running locally, the repository includes a default `app/version.py` f Running `flask db migrate` on the container ci_app_1 errors because the files in the migrations folder are read-only. Follow this procedure to create a database migration using Flask: 1. Ensure all containers are stopped and that the notification_api image has been built -2. Run `docker compose -f ci/docker-compose-local-migrate.yml up`. This creates the container ci_app_migrate with your local notification-api directory mounted in read-write mode. The container runs `flask db migrate` and exits. -3. Press Ctrl-C to stop the containers, and identify the new file in `migrations/versions/`. +2. Run migrations at least once e.g. `docker compose -f ci/docker-compose-local.yml up` and stop any running containers. +3. Run `docker compose -f ci/docker-compose-local-migrate.yml up`. This creates the container ci_app_migrate with your local notification-api directory mounted in read-write mode. The container runs `flask db migrate` and exits. +4. Press Ctrl-C to stop the containers, and identify the new file in `migrations/versions/`. ### Unit testing See the [tests README.md](tests/README.md) for information. diff --git a/app/__init__.py b/app/__init__.py index e854b88177..c1ec78b56e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -43,6 +43,7 @@ DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' DATE_FORMAT = '%Y-%m-%d' +HTTP_TIMEOUT = (3.05, 1) if os.getenv('NOTIFY_ENVIRONMENT') in ('production', 'staging') else (30, 30) load_dotenv() diff --git a/app/callback/webhook_callback_strategy.py b/app/callback/webhook_callback_strategy.py index 78caee839d..978b08b567 100644 --- a/app/callback/webhook_callback_strategy.py +++ b/app/callback/webhook_callback_strategy.py @@ -8,7 +8,7 @@ from requests.api import request from requests.exceptions import HTTPError, RequestException -from app import statsd_client +from app import statsd_client, HTTP_TIMEOUT from app.callback.service_callback_strategy_interface import ServiceCallbackStrategyInterface from app.celery.exceptions import NonRetryableException, RetryableException from app.dao.api_key_dao import get_unsigned_secret @@ -32,7 +32,7 @@ def send_callback( 'Content-Type': 'application/json', 'Authorization': 'Bearer {}'.format(callback.bearer_token), }, - timeout=(3.05, 1), + timeout=HTTP_TIMEOUT, ) current_app.logger.info('Callback sent to %s, response %d, %s', callback.url, response.status_code, tags) response.raise_for_status() diff --git a/app/celery/process_ga4_measurement_tasks.py b/app/celery/process_ga4_measurement_tasks.py index cb1bb6cfb2..84e69856d3 100644 --- a/app/celery/process_ga4_measurement_tasks.py +++ b/app/celery/process_ga4_measurement_tasks.py @@ -4,8 +4,9 @@ from flask import current_app -from app import db, notify_celery +from app import notify_celery from app.celery.exceptions import AutoRetryException +from app.dao.dao_utils import get_reader_session from app.models import Notification, NotificationHistory, TemplateHistory @@ -54,24 +55,25 @@ def post_to_ga4(notification_id: str, event_name, event_source, event_medium) -> current_app.logger.error('GA4_MEASUREMENT_ID is not set') return False - # Retrieve the notification from the database. It might have moved to history. - notification = db.session.get(Notification, notification_id) - if notification is None: - notification = db.session.get(NotificationHistory, notification_id) + with get_reader_session() as session: + # Retrieve the notification from the database. It might have moved to history. + notification = session.get(Notification, notification_id) if notification is None: - current_app.logger.warning('GA4: Notification %s not found', notification_id) - return False + notification = session.get(NotificationHistory, notification_id) + if notification is None: + current_app.logger.warning('GA4: Notification %s not found', notification_id) + return False + else: + # The notification is a NotificationHistory instance. + template_id = notification.template_id + template_name = session.get(TemplateHistory, (template_id, notification.template_version)).name else: - # The notification is a NotificationHistory instance. - template_id = notification.template_id - template_name = db.session.get(TemplateHistory, (template_id, notification.template_version)).name - else: - # The notification is a Notification instance. - template_id = notification.template.id - template_name = notification.template.name + # The notification is a Notification instance. + template_id = notification.template.id + template_name = notification.template.name - service_id = notification.service_id - service_name = notification.service.name + service_id = notification.service_id + service_name = notification.service.name url_str = current_app.config.get('GA4_URL', '') url_params_dict = { diff --git a/app/celery/process_ses_receipts_tasks.py b/app/celery/process_ses_receipts_tasks.py index 0572aa7bd1..21533e495f 100644 --- a/app/celery/process_ses_receipts_tasks.py +++ b/app/celery/process_ses_receipts_tasks.py @@ -14,7 +14,8 @@ from sqlalchemy.orm.exc import NoResultFound import enum import requests -from app import DATETIME_FORMAT, notify_celery, statsd_client, va_profile_client + +from app import DATETIME_FORMAT, HTTP_TIMEOUT, notify_celery, statsd_client, va_profile_client from app.celery.exceptions import AutoRetryException from app.celery.service_callback_tasks import publish_complaint from app.config import QueueNames @@ -74,7 +75,7 @@ def get_certificate(url): res = certificate_cache.get(url) if res is not None: return res - res = requests.get(url, timeout=(3.05, 1)).content + res = requests.get(url, timeout=HTTP_TIMEOUT).content certificate_cache.set(url, res, timeout=60 * 60) # 60 minutes return res @@ -106,7 +107,7 @@ def sns_callback_handler(): if message.get('Type') == 'SubscriptionConfirmation': url = message.get('SubscribeURL') try: - response = requests.get(url, timeout=(3.05, 1)) + response = requests.get(url, timeout=HTTP_TIMEOUT) response.raise_for_status() except requests.RequestException as e: current_app.logger.warning('Response: %s', response.text) @@ -140,7 +141,7 @@ def sns_smtp_callback_handler(): if message.get('Type') == 'SubscriptionConfirmation': url = message.get('SubscribeURL') try: - response = requests.get(url, timeout=(3.05, 1)) + response = requests.get(url, timeout=HTTP_TIMEOUT) response.raise_for_status() except requests.RequestException as e: current_app.logger.warning('Response: %s', response.text) diff --git a/app/celery/service_callback_tasks.py b/app/celery/service_callback_tasks.py index bef304c5ba..34fbdb20ed 100644 --- a/app/celery/service_callback_tasks.py +++ b/app/celery/service_callback_tasks.py @@ -6,7 +6,7 @@ from requests.exceptions import Timeout, RequestException from notifications_utils.statsd_decorators import statsd -from app import notify_celery, encryption, statsd_client, DATETIME_FORMAT +from app import notify_celery, encryption, statsd_client, DATETIME_FORMAT, HTTP_TIMEOUT from app.callback.webhook_callback_strategy import generate_callback_signature from app.celery.exceptions import AutoRetryException, NonRetryableException, RetryableException from app.config import QueueNames @@ -394,7 +394,7 @@ def send_delivery_status_from_notification( 'Content-Type': 'application/json', 'x-enp-signature': callback_signature, }, - timeout=(3.05, 1), + timeout=HTTP_TIMEOUT, ) response.raise_for_status() except Timeout as e: diff --git a/app/clients/email/govdelivery_client.py b/app/clients/email/govdelivery_client.py index 6299031994..d540d2387d 100644 --- a/app/clients/email/govdelivery_client.py +++ b/app/clients/email/govdelivery_client.py @@ -1,4 +1,5 @@ import requests + from app.clients.email import EmailClient, EmailClientException from app.models import ( NOTIFICATION_CANCELLED, @@ -36,6 +37,9 @@ def init_app( *args, **kwargs, ): + from app import HTTP_TIMEOUT # Circular import + + self.timeout = HTTP_TIMEOUT self.name = 'govdelivery' self.token = token self.statsd_client = statsd_client @@ -75,7 +79,7 @@ def send_email( start_time = monotonic() response = requests.post( - self.govdelivery_url, json=payload, headers={'X-AUTH-TOKEN': self.token}, timeout=(3.05, 1) + self.govdelivery_url, json=payload, headers={'X-AUTH-TOKEN': self.token}, timeout=self.timeout ) response.raise_for_status() diff --git a/app/clients/performance_platform/performance_platform_client.py b/app/clients/performance_platform/performance_platform_client.py index 8641c6d96b..c01f7a140e 100644 --- a/app/clients/performance_platform/performance_platform_client.py +++ b/app/clients/performance_platform/performance_platform_client.py @@ -14,6 +14,9 @@ def init_app( self, app, ): + from app import HTTP_TIMEOUT # Circular import + + self.timeout = HTTP_TIMEOUT self._active = app.config.get('PERFORMANCE_PLATFORM_ENABLED') if self.active: self.performance_platform_url = app.config.get('PERFORMANCE_PLATFORM_URL') @@ -27,7 +30,7 @@ def send_stats_to_performance_platform( bearer_token = self.performance_platform_endpoints[payload['dataType']] headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer {}'.format(bearer_token)} resp = requests.post( - self.performance_platform_url + payload['dataType'], json=payload, headers=headers, timeout=(3.05, 1) + self.performance_platform_url + payload['dataType'], json=payload, headers=headers, timeout=self.timeout ) if resp.status_code == 200: diff --git a/app/cronitor.py b/app/cronitor.py index 745556616e..c3459af9a9 100644 --- a/app/cronitor.py +++ b/app/cronitor.py @@ -4,6 +4,8 @@ def cronitor(task_name): + from app import HTTP_TIMEOUT # Circular import + # check if task_name is in config def decorator(func): def ping_cronitor(command): @@ -25,7 +27,7 @@ def ping_cronitor(command): params={ 'host': current_app.config['API_HOST_NAME'], }, - timeout=(3.05, 1), + timeout=HTTP_TIMEOUT, ) resp.raise_for_status() except requests.RequestException: diff --git a/app/notifications/utils.py b/app/notifications/utils.py index 7eee0feb93..acef33c4d8 100644 --- a/app/notifications/utils.py +++ b/app/notifications/utils.py @@ -3,13 +3,15 @@ def confirm_subscription(confirmation_request): + from app import HTTP_TIMEOUT # Circular import + url = confirmation_request.get('SubscribeURL') if not url: current_app.logger.warning('SubscribeURL does not exist or empty.') return try: - response = requests.get(url, timeout=(3.05, 1)) + response = requests.get(url, timeout=HTTP_TIMEOUT) response.raise_for_status() except requests.RequestException: current_app.logger.exception('Response: %s', response.text) diff --git a/app/notifications/validators.py b/app/notifications/validators.py index a051bb36b3..390606fd2b 100644 --- a/app/notifications/validators.py +++ b/app/notifications/validators.py @@ -1,7 +1,7 @@ import base64 import binascii -from sqlalchemy.orm.exc import NoResultFound +from cachetools import TTLCache, cached from flask import current_app from notifications_utils import SMS_CHAR_COUNT_LIMIT from notifications_utils.recipients import ( @@ -10,6 +10,7 @@ get_international_phone_info, ) from notifications_utils.clients.redis import rate_limit_cache_key, daily_limit_cache_key +from sqlalchemy.orm.exc import NoResultFound from app.dao import services_dao, templates_dao from app.dao.service_sms_sender_dao import dao_get_service_sms_sender_by_id @@ -228,6 +229,7 @@ def check_service_email_reply_to_id( raise BadRequestError(message=message) +@cached(cache=TTLCache(maxsize=1024, ttl=600)) def check_service_sms_sender_id( service_id, sms_sender_id, diff --git a/app/template/rest.py b/app/template/rest.py index 743a9fccb2..1abd6a842b 100644 --- a/app/template/rest.py +++ b/app/template/rest.py @@ -18,6 +18,7 @@ from sqlalchemy.orm.exc import NoResultFound from notifications_utils.template import HTMLEmailTemplate +from app import HTTP_TIMEOUT from app.authentication.auth import requires_admin_auth_or_user_in_service, requires_user_in_service_or_admin from app.communication_item import validate_communication_items from app.dao.fact_notification_status_dao import fetch_template_usage_for_service_with_given_template @@ -453,14 +454,14 @@ def _get_png_preview_or_overlaid_pdf( url, json=data, headers={'Authorization': 'Token {}'.format(current_app.config['TEMPLATE_PREVIEW_API_KEY'])}, - timeout=(3.05, 1), + timeout=HTTP_TIMEOUT, ) else: resp = requests_post( url, data=data, headers={'Authorization': 'Token {}'.format(current_app.config['TEMPLATE_PREVIEW_API_KEY'])}, - timeout=(3.05, 1), + timeout=HTTP_TIMEOUT, ) if resp.status_code != 200: diff --git a/app/user/rest.py b/app/user/rest.py index e6433609c2..45fc2ac523 100644 --- a/app/user/rest.py +++ b/app/user/rest.py @@ -11,7 +11,7 @@ from sqlalchemy.exc import IntegrityError from urllib.parse import urlencode -from app import db +from app import db, HTTP_TIMEOUT from app.config import QueueNames, Config from app.dao.fido2_key_dao import ( save_fido2_key, @@ -438,7 +438,10 @@ def send_support_email(user_id): } response = requests.post( - '{}/api/v2/tickets'.format(API_URL), json=ticket, auth=requests.HTTPBasicAuth(API_KEY, 'x'), timeout=(3.05, 1) + '{}/api/v2/tickets'.format(API_URL), + json=ticket, + auth=requests.HTTPBasicAuth(API_KEY, 'x'), + timeout=HTTP_TIMEOUT, ) if response.status_code != 201: diff --git a/app/va/mpi/mpi.py b/app/va/mpi/mpi.py index c64f965513..f8e1ed809c 100644 --- a/app/va/mpi/mpi.py +++ b/app/va/mpi/mpi.py @@ -5,6 +5,7 @@ from time import monotonic from http.client import responses from functools import reduce + from app.va.identifier import ( IdentifierType, transform_to_fhir_format, @@ -82,6 +83,9 @@ def init_app( ssl_key_path, statsd_client, ): + from app import HTTP_TIMEOUT # Circular import + + self.timeout = HTTP_TIMEOUT self.logger = logger self.base_url = url self.ssl_cert_path = ssl_cert_path @@ -132,7 +136,7 @@ def _make_request( f'{self.base_url}/psim_webservice/fhir/Patient/{fhir_identifier}', params={'-sender': self.SYSTEM_IDENTIFIER}, cert=(self.ssl_cert_path, self.ssl_key_path), - timeout=(3.05, 5), + timeout=self.timeout, ) response.raise_for_status() diff --git a/app/va/va_onsite/va_onsite_client.py b/app/va/va_onsite/va_onsite_client.py index 49929ec500..6c7c42fadd 100644 --- a/app/va/va_onsite/va_onsite_client.py +++ b/app/va/va_onsite/va_onsite_client.py @@ -19,6 +19,9 @@ def init_app( :param url: the url to send the information to in a string format :param va_onsite_secret: the secret key in string format used to validate the connection """ + from app import HTTP_TIMEOUT # Circular import + + self.timeout = HTTP_TIMEOUT self.logger = logger self.url_base = url self.va_onsite_secret = va_onsite_secret @@ -40,7 +43,7 @@ def post_onsite_notification( url=f'{ self.url_base }/v0/onsite_notifications', data=json.dumps(data), headers=self._build_header(), - timeout=(3.05, 1), + timeout=self.timeout, ) except Exception as e: self.logger.exception(e) diff --git a/app/va/va_profile/va_profile_client.py b/app/va/va_profile/va_profile_client.py index 1ca159a1a8..7683d3db81 100644 --- a/app/va/va_profile/va_profile_client.py +++ b/app/va/va_profile/va_profile_client.py @@ -86,6 +86,9 @@ def init_app( va_profile_token, statsd_client, ): + from app import HTTP_TIMEOUT # Circular import + + self.timeout = HTTP_TIMEOUT self.logger: Logger = logger self.va_profile_url = va_profile_url self.ssl_cert_path = ssl_cert_path @@ -109,7 +112,7 @@ def get_profile(self, va_profile_id: RecipientIdentifier) -> Profile: data = {'bios': [{'bioPath': 'contactInformation'}, {'bioPath': 'communicationPermissions'}]} try: - response = requests.post(url, json=data, cert=(self.ssl_cert_path, self.ssl_key_path), timeout=(3.05, 1)) + response = requests.post(url, json=data, cert=(self.ssl_cert_path, self.ssl_key_path), timeout=self.timeout) response.raise_for_status() except (requests.HTTPError, requests.RequestException, requests.Timeout) as e: self._handle_exceptions(va_profile_id.id_value, e) @@ -339,10 +342,13 @@ def _handle_exceptions(self, va_profile_id_value: str, error: Exception): elif isinstance(error, requests.RequestException): self.statsd_client.incr('clients.va-profile.error.request_exception') - failure_message = 'VA Profile returned RequestException while querying for VA Profile ID' + failure_message = f'VA Profile returned {error.__class__.__name__} while querying for VA Profile ID' if isinstance(error, requests.Timeout): - failure_message = f'VA Profile request timed out for VA Profile ID {va_profile_id_value}.' + failure_message = ( + f'VA Profile request timed out with {error.__class__.__name__} ' + f'for VA Profile ID {va_profile_id_value}.' + ) exception = VAProfileRetryableException(failure_message) exception.failure_reason = failure_message @@ -370,7 +376,7 @@ def send_va_profile_email_status(self, notification_data: dict) -> None: # make POST request to VA Profile endpoint for notification statuses # raise errors if they occur, they will be handled by the calling function try: - response = requests.post(url, json=notification_data, headers=headers, timeout=(3.05, 1)) + response = requests.post(url, json=notification_data, headers=headers, timeout=self.timeout) except requests.Timeout: self.logger.exception( 'Request timeout attempting to send email status to VA Profile for notification %s | retrying...', diff --git a/migrations/versions/0372_remove_service_id_index.py b/migrations/versions/0372_remove_service_id_index.py new file mode 100644 index 0000000000..91ad6992f6 --- /dev/null +++ b/migrations/versions/0372_remove_service_id_index.py @@ -0,0 +1,19 @@ +""" + +Revision ID: 0372_remove_service_id_index +Revises: 0371_replace_template_content +Create Date: 2024-10-21 17:22:11.386188 + +""" +from alembic import op + +revision = '0372_remove_service_id_index' +down_revision = '0371_replace_template_content' + + +def upgrade(): + op.drop_index(op.f('ix_notifications_service_id'), table_name='notifications') + + +def downgrade(): + op.create_index(op.f('ix_notifications_service_id'), 'notifications', ['service_id'], unique=False)