From 522ff39f8d021fc3c50e5db50097a65985acf9ba Mon Sep 17 00:00:00 2001 From: Mack Halliday Date: Thu, 24 Oct 2024 17:29:43 -0400 Subject: [PATCH] #1979 - Create enrollment confirmation SMS notifications for Comp and Pen opt in (#2056) #1979 - Create enrollment confirmation SMS notifications for Comp and Pen opt in (#2056) --- .talismanrc | 5 +- app/models.py | 1 + .../va_profile_opt_in_out_lambda.py | 210 ++++++++++++++++- .../va_profile/test_va_profile_integration.py | 223 +++++++++++++----- 4 files changed, 370 insertions(+), 69 deletions(-) diff --git a/.talismanrc b/.talismanrc index c184d85d4c..27ea9a5f84 100644 --- a/.talismanrc +++ b/.talismanrc @@ -41,14 +41,17 @@ fileignoreconfig: checksum: 6ffb8742a19c5b834c608826fd459cc1b6ea35ebfffd2d929a3a0f269c74183d - filename: tests/app/celery/test_service_callback_tasks.py checksum: 70575434f7a4fedd43d4c9164bc899a606768526d432c364db372524eec26542 +- filename: lambda_functions/va_profile/va_profile_opt_in_out_lambda.py + checksum: ab4099f87cacd5ebd5b7fdbffd77d062ab6b3880bdc033b0fd65358d392dde97 - filename: tests/app/conftest.py checksum: a80aa727586db82ed1b50bdb81ddfe1379e649a9dfc1ece2c36047486b41b83d - filename: tests/app/notifications/test_process_notifications_for_profile_v3.py checksum: 4e15e63d349635131173ffdd7aebcd547621db08de877ef926d3a41fde72d065 - filename: tests/app/v2/notifications/test_post_notifications.py checksum: 3181930a13e3679bb2f17eaa3f383512eb9caf4ed5d5e14496ca4193c6083965 +- filename: tests/lambda_functions/va_profile/test_va_profile_integration.py + checksum: 94f3e59c05ed2024a08495e15dab266cdc27d33eb6bc56d8256bd53b38668f9a - filename: app/va/va_profile/va_profile_client.py checksum: fe634f26f7dc3874f4afcfd1ba3f03bae380b53befe973a752c7347097a88701 - filename: tests/lambda_functions/vetext_incoming_forwarder_lambda/test_vetext_incoming_forwarder_lambda.py checksum: 7494eb4321fd2fbc3ff3915d8753d8fec7a936a69dc6ab78f0b532a701f032eb -version: "1.0" diff --git a/app/models.py b/app/models.py index 494e87fd97..e57aebc766 100644 --- a/app/models.py +++ b/app/models.py @@ -2201,6 +2201,7 @@ class VAProfileLocalCache(db.Model): participant_id = db.Column(db.BigInteger, nullable=True) has_duplicate_mappings = db.Column(db.Boolean, nullable=False, default=False) + notification_id = db.Column(UUID(as_uuid=True), nullable=True) __table_args__ = ( UniqueConstraint('va_profile_id', 'communication_item_id', 'communication_channel_id', name='uix_veteran_id'), diff --git a/lambda_functions/va_profile/va_profile_opt_in_out_lambda.py b/lambda_functions/va_profile/va_profile_opt_in_out_lambda.py index 542ee2461b..c1a57245c7 100644 --- a/lambda_functions/va_profile/va_profile_opt_in_out_lambda.py +++ b/lambda_functions/va_profile/va_profile_opt_in_out_lambda.py @@ -20,18 +20,22 @@ via lambda layers. """ -import boto3 +import calendar import json -import jwt import logging import os -import psycopg2 import ssl import sys +from datetime import datetime, timezone +from http.client import HTTPSConnection, HTTPResponse +from tempfile import NamedTemporaryFile +from typing import Optional + +import boto3 +import jwt +import psycopg2 from botocore.exceptions import ClientError, ValidationError from cryptography.x509 import Certificate, load_pem_x509_certificate -from http.client import HTTPSConnection -from tempfile import NamedTemporaryFile logger = logging.getLogger('VAProfileOptInOut') @@ -40,10 +44,25 @@ ALB_CERTIFICATE_ARN = os.getenv('ALB_CERTIFICATE_ARN') ALB_PRIVATE_KEY_PATH = os.getenv('ALB_PRIVATE_KEY_PATH') CA_PATH = '/opt/VA_CAs/' +COMP_AND_PEN_OPT_IN_CUTOFF_TIME_UTC = 11, 10, 0, 0 +COMP_AND_PEN_OPT_IN_API_KEY_PARAM_PATH = os.getenv('COMP_AND_PEN_OPT_IN_API_KEY') +COMP_AND_PEN_OPT_IN_TEMPLATE_ID = os.getenv('COMP_AND_PEN_OPT_IN_TEMPLATE_ID') +COMP_AND_PEN_SERVICE_ID = os.getenv('COMP_AND_PEN_SERVICE_ID') +COMP_AND_PEN_SMS_SENDER_ID = os.getenv('COMP_AND_PEN_SMS_SENDER_ID') NOTIFY_ENVIRONMENT = os.getenv('NOTIFY_ENVIRONMENT') OPT_IN_OUT_QUERY = """SELECT va_profile_opt_in_out(%s, %s, %s, %s, %s);""" +OPT_IN_OUT_ADD_NOTIFICATION_ID_QUERY = ( + """ UPDATE va_profile_local_cache SET notification_id = %s WHERE va_profile_id = %s AND source_datetime = %s; """ +) VA_PROFILE_DOMAIN = os.getenv('VA_PROFILE_DOMAIN') VA_PROFILE_PATH_BASE = '/communication-hub/communication/v1/status/changelog/' +VA_NOTIFY_SEND_SMS_PATH = '/v2/notifications/sms' + +# Build domain based on environment +if NOTIFY_ENVIRONMENT == 'prod': + VA_NOTIFY_DOMAIN = 'api.notifications.va.gov' +else: + VA_NOTIFY_DOMAIN = f'{NOTIFY_ENVIRONMENT}.api.notifications.va.gov' if NOTIFY_ENVIRONMENT is None: @@ -97,6 +116,10 @@ # The certificates are not necessary for testing, wherein the PUT request is mocked. ssl_context = None +# Use mock API Key in env variables for testing, else set to None +# Outside of testing, COMP_AND_PEN_OPT_IN_API_KEY is defined by its paramater store value +COMP_AND_PEN_OPT_IN_API_KEY = os.getenv('COMP_AND_PEN_OPT_IN_API_KEY', None) + if ALB_CERTIFICATE_ARN is None: logger.error('ALB_CERTIFICATE_ARN is not set.') elif ALB_PRIVATE_KEY_PATH is None: @@ -128,6 +151,12 @@ f.seek(0) ssl_context.load_cert_chain(f.name) + + # Get Comp and Pen API Key from Parameter Store + ssm_response_api_key = ssm_client.get_parameter( + Name=COMP_AND_PEN_OPT_IN_API_KEY_PARAM_PATH, WithDecryption=True + ) + COMP_AND_PEN_OPT_IN_API_KEY = ssm_response_api_key['Parameter']['Value'] except (OSError, ClientError, ssl.SSLError, ValidationError, KeyError) as e: logger.exception(e) if isinstance(e, ssl.SSLError): @@ -325,6 +354,7 @@ def va_profile_opt_in_out_lambda_handler( # noqa: C901 c.execute(OPT_IN_OUT_QUERY, params) put_body['status'] = 'COMPLETED_SUCCESS' if c.fetchone()[0] else 'COMPLETED_NOOP' db_connection.commit() + logger.debug('Executed the stored function.') except KeyError as e: # Bad Request. Required attributes are missing. @@ -365,6 +395,50 @@ def va_profile_opt_in_out_lambda_handler( # noqa: C901 } ) + va_profile_id = bio['vaProfileId'] + + try: + # Send Comp and Pen Opt-In confirmation if anticipated status code still 200 + # And if Opt-In confirmation (bio['allowed'] == True) + if post_response['statusCode'] == 200 and bio['allowed']: + response = send_comp_and_pen_opt_in_confirmation(va_profile_id) + + # Save notification_id from POST sms response if method returned + # a value AND the response status code is 201. + if response is None: + logger.critical( + 'Could not send Comp and Pen opt-in confirmation to VAProfileId: %s. No response status or response body to record.', + va_profile_id, + ) + elif response.status != 201: + response_data = response.read().decode() + response_json = json.loads(response_data) + logger.critical( + 'Could not send Comp and Pen opt-in confirmation to VAProfileId: %s. Response status: %s, Response: %s', + va_profile_id, + response.status, + response_data, + ) + else: + response_data = response.read().decode() + response_json = json.loads(response_data) + notification_id = response_json['id'] + + logger.info( + 'Sent Comp and Pen opt-in confirmation to VAProfileId: %s with notification_id: %s. Response status: %s', + va_profile_id, + notification_id, + response.status, + ) + save_notification_id_to_cache(va_profile_id, notification_id, bio['sourceDate']) + + except Exception as e: + logger.critical( + 'Critical error during the process of sending a Comp and Pen Opt-in confirmation notification to VaProfileId: %s. Error: %s', + va_profile_id, + e, + ) + logger.info('POST response: %s', post_response) return post_response @@ -448,6 +522,132 @@ def make_PUT_request( https_connection.close() +def send_comp_and_pen_opt_in_confirmation(va_profile_id: int) -> Optional[HTTPResponse]: + """ + Send an opt-in confirmation SMS notification based on user's VAProfile ID. + + Args: + va_profile_id (int): The VA profile ID of the user the notification should be sent to. + """ + + try: + # Personalization for opt-in confirmation notification SMS based on cutoff date + # to enroll in monthly Comp and Pen notification + now = datetime.now(timezone.utc) + cutoff_datetime = datetime(now.year, now.month, *COMP_AND_PEN_OPT_IN_CUTOFF_TIME_UTC, tzinfo=timezone.utc) + + if now < cutoff_datetime: + month_personalisation = calendar.month_name[now.month] + else: + next_month = (now.month % 12) + 1 + month_personalisation = calendar.month_name[next_month] + + sms_data = json.dumps( + { + 'template_id': COMP_AND_PEN_OPT_IN_TEMPLATE_ID, + 'recipient_identifier': {'id_type': 'VAPROFILEID', 'id_value': str(va_profile_id)}, + 'sms_sender_id': COMP_AND_PEN_SMS_SENDER_ID, + 'personalisation': {'month-name': month_personalisation}, + } + ) + + logger.debug('Sending Comp and Pen opt-in confirmation SMS notification vaProfileId %s', va_profile_id) + + conn = HTTPSConnection(VA_NOTIFY_DOMAIN, context=ssl_context) + encoded_header = generate_jwt() + + conn.request( + 'POST', + VA_NOTIFY_SEND_SMS_PATH, + body=sms_data, + headers={'Content-Type': 'application/json', 'Authorization': f'Bearer {encoded_header}'}, + ) + + response = conn.getresponse() + + if response.status != 201: + logger.error( + 'Failed to send Comp and Pen opt-in confirmation SMS notification. Response status: %s', + {response.status}, + ) + else: + return response + + except ValueError as ve: + logger.exception( + 'Configuration error while attempting to send Comp and Pen opt-in confirmation SMS notification: %s', {ve} + ) + + except Exception as e: + logger.exception( + 'An error occurred while attempting to send Comp and Pen opt-in confirmation SMS notification to vaProfileId %s: %s', + va_profile_id, + e, + ) + return None + + +def save_notification_id_to_cache(va_profile_id: int, notification_id: str, source_date: str): + """ + Update the VAProfileLocalCache table by inserting the notification_id for the given va_profile_id. + + Args: + va_profile_id (int): The VA profile ID of the user the notification was sent to. + notification_id (str): The notification UUID. + """ + try: + with db_connection.cursor() as cursor: + # Execute the SQL query with the provided parameters. + cursor.execute(OPT_IN_OUT_ADD_NOTIFICATION_ID_QUERY, (notification_id, va_profile_id, source_date)) + + db_connection.commit() + logger.info( + 'Successfully updated VAProfileLocalCache with notification_id %s for va_profile_id %s.', + notification_id, + va_profile_id, + ) + + except psycopg2.IntegrityError as e: + db_connection.rollback() + logger.error( + 'Failed to insert notification_id %s for va_profile_id %s. IntegrityError: %s', + notification_id, + va_profile_id, + str(e), + ) + except Exception as e: + db_connection.rollback() + logger.error( + 'An error occurred while attempting to update notification_id: %s for va_profile_id %s: %s', + notification_id, + va_profile_id, + str(e), + ) + finally: + cursor.close() + + +def generate_jwt() -> str: + """ + Generate a JWT token for authentication purposes. + + Args: + service_api_key (str): The API key used to sign the JWT. + service_id (str): The service identifier. + + Returns: + str: The generated JWT token. + """ + + headers = {'typ': 'JWT', 'alg': 'HS256'} + + current_timestamp = int(datetime.now().timestamp()) + payload = {'iss': COMP_AND_PEN_SERVICE_ID, 'iat': current_timestamp} + + # Generate and return the signed JWT token using the pyJWT library + return jwt.encode(payload, COMP_AND_PEN_OPT_IN_API_KEY, algorithm='HS256', headers=headers) + + def get_integration_testing_public_cert() -> Certificate: """ Load the integration testing public certificate used to verify JWT signatures for POST requests. diff --git a/tests/lambda_functions/va_profile/test_va_profile_integration.py b/tests/lambda_functions/va_profile/test_va_profile_integration.py index 0e012086cb..d26414a39a 100644 --- a/tests/lambda_functions/va_profile/test_va_profile_integration.py +++ b/tests/lambda_functions/va_profile/test_va_profile_integration.py @@ -6,18 +6,27 @@ created or updated; otherwise, False. """ +import importlib +import json +from datetime import datetime, timedelta, timezone +from json import dumps, loads +from random import randint +from unittest.mock import Mock, patch + import jwt import pytest -from app.models import VAProfileLocalCache -from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization from cryptography.x509 import Certificate, load_pem_x509_certificate -from datetime import datetime, timedelta, timezone -from json import dumps, loads -from lambda_functions.va_profile.va_profile_opt_in_out_lambda import jwt_is_valid, va_profile_opt_in_out_lambda_handler -from random import randint from sqlalchemy import delete, func, select, text +from app.models import VAProfileLocalCache +from lambda_functions.va_profile import va_profile_opt_in_out_lambda +from lambda_functions.va_profile.va_profile_opt_in_out_lambda import ( + generate_jwt, + jwt_is_valid, + va_profile_opt_in_out_lambda_handler, +) # Base path for mocks LAMBDA_MODULE = 'lambda_functions.va_profile.va_profile_opt_in_out_lambda' @@ -30,6 +39,18 @@ ) +@pytest.fixture +def mock_env_vars(monkeypatch): + monkeypatch.setenv('COMP_AND_PEN_OPT_IN_TEMPLATE_ID', 'mock_template_id') + monkeypatch.setenv('COMP_AND_PEN_SMS_SENDER_ID', 'mock_sms_sender_id') + monkeypatch.setenv('COMP_AND_PEN_OPT_IN_API_KEY_PARAM_PATH', 'mock_va_notify_api_key_param_path') + monkeypatch.setenv('COMP_AND_PEN_OPT_IN_API_KEY', 'mock_va_notify_api_key') + monkeypatch.setenv('COMP_AND_PEN_SERVICE_ID', 'mock_service_id') + monkeypatch.setenv('ALB_CERTIFICATE_ARN', 'mock_alb_certificate_arn') + monkeypatch.setenv('ALB_PRIVATE_KEY_PATH', 'mock_alb_private_key_path') + monkeypatch.setenv('VA_PROFILE_DOMAIN', 'mock_va_profile_domain') + + @pytest.fixture def put_mock(mocker): """ @@ -40,6 +61,13 @@ def put_mock(mocker): return mocker.patch(f'{LAMBDA_MODULE}.make_PUT_request') +@pytest.fixture +def post_opt_in_confirmation_mock_return(mocker): + mock = mocker.patch(f'{LAMBDA_MODULE}.send_comp_and_pen_opt_in_confirmation') + mock.return_value = None + return mock + + @pytest.fixture(scope='module') def private_key(): # This assumes tests are run from the project root directory. @@ -134,6 +162,41 @@ def event_bytes(event_str) -> dict: return event +def create_event( + master_tx_audit_id: str, + tx_audit_id: str, + source_date: str, + va_profile_id: int, + communication_channel_id: int, + communication_item_id: int, + is_allowed: bool, + jwt_value, +) -> dict: + """ + Return a dictionary in the format of the payload the lambda function expects to + receive from VA Profile via AWS API Gateway v2. + """ + + return { + 'headers': { + 'Authorization': f'Bearer {jwt_value}', + }, + 'body': { + 'txAuditId': master_tx_audit_id, + 'bios': [ + { + 'txAuditId': tx_audit_id, + 'sourceDate': source_date, + 'vaProfileId': va_profile_id, + 'communicationChannelId': communication_channel_id, + 'communicationItemId': communication_item_id, + 'allowed': is_allowed, + } + ], + }, + } + + def test_va_profile_cache_exists(notify_db_session): assert notify_db_session.engine.has_table('va_profile_local_cache') @@ -311,6 +374,7 @@ def test_va_profile_opt_in_out_lambda_handler_valid_dict( event_dict, put_mock, get_integration_testing_public_cert_mock, + post_opt_in_confirmation_mock_return, ): """ Test the VA Profile integration lambda by sending a valid request that should create @@ -344,7 +408,9 @@ def test_va_profile_opt_in_out_lambda_handler_valid_dict( notify_db_session.session.commit() -def test_va_profile_opt_in_out_lambda_handler_valid_str(notify_db_session, event_str, put_mock): +def test_va_profile_opt_in_out_lambda_handler_valid_str( + notify_db_session, event_str, put_mock, post_opt_in_confirmation_mock_return +): """ Test the VA Profile integration lambda by sending a valid request that should create a new row in the database. The AWS lambda function should be able to handle and event @@ -374,7 +440,9 @@ def test_va_profile_opt_in_out_lambda_handler_valid_str(notify_db_session, event notify_db_session.session.commit() -def test_va_profile_opt_in_out_lambda_handler_valid_bytes(notify_db_session, event_bytes, put_mock): +def test_va_profile_opt_in_out_lambda_handler_valid_bytes( + notify_db_session, event_bytes, put_mock, post_opt_in_confirmation_mock_return +): """ Test the VA Profile integration lambda by sending a valid request that should create a new row in the database. The AWS lambda function should be able to handle and event @@ -394,7 +462,7 @@ def test_va_profile_opt_in_out_lambda_handler_valid_bytes(notify_db_session, eve put_mock.assert_called_once_with('txAuditId', expected_put_body) # Verify one row was created using a delete statement that doubles as teardown. - event = loads(event_bytes['body'].decode())['bios'][0] + event = json.loads(event_bytes['body'].decode())['bios'][0] stmt = delete(VAProfileLocalCache).where( VAProfileLocalCache.va_profile_id == event['vaProfileId'], VAProfileLocalCache.communication_item_id == event['communicationItemId'], @@ -404,7 +472,9 @@ def test_va_profile_opt_in_out_lambda_handler_valid_bytes(notify_db_session, eve notify_db_session.session.commit() -def test_va_profile_opt_in_out_lambda_handler_new_row(notify_db_session, jwt_encoded, put_mock): +def test_va_profile_opt_in_out_lambda_handler_new_row( + notify_db_session, jwt_encoded, put_mock, post_opt_in_confirmation_mock_return +): """ Test the VA Profile integration lambda by sending a valid request that should create a new row in the database. @@ -448,10 +518,7 @@ def test_va_profile_opt_in_out_lambda_handler_new_row(notify_db_session, jwt_enc def test_va_profile_opt_in_out_lambda_handler_older_date( - notify_db_session, - jwt_encoded, - put_mock, - sample_va_profile_local_cache, + notify_db_session, jwt_encoded, put_mock, sample_va_profile_local_cache, post_opt_in_confirmation_mock_return ): """ Test the VA Profile integration lambda by sending a valid request with an older date. @@ -491,6 +558,7 @@ def test_va_profile_opt_in_out_lambda_handler_newer_date( jwt_encoded, put_mock, sample_va_profile_local_cache, + post_opt_in_confirmation_mock_return, ): """ Test the VA Profile integration lambda by sending a valid request with a newer date. @@ -525,7 +593,7 @@ def test_va_profile_opt_in_out_lambda_handler_newer_date( @pytest.mark.serial -def test_va_profile_opt_in_out_lambda_handler_KeyError1(jwt_encoded, put_mock): +def test_va_profile_opt_in_out_lambda_handler_KeyError1(jwt_encoded, put_mock, post_opt_in_confirmation_mock_return): """ Test the VA Profile integration lambda by inspecting the PUT request it initiates to VA Profile in response to a request. This test should generate a KeyError in the handler @@ -647,11 +715,19 @@ def test_va_profile_opt_in_out_lambda_handler_audit_id_mismatch(jwt_encoded, put @pytest.mark.serial -def test_va_profile_opt_in_out_lambda_handler_integration_testing( +@pytest.mark.parametrize( + 'mock_date,expected_month', + [ + (datetime(2024, 4, 11, 9, 59, tzinfo=timezone.utc), 'April'), # Before 11th 10:00 AM UTC + (datetime(2024, 4, 11, 10, 1, tzinfo=timezone.utc), 'May'), # After 11th 10:00 AM UTC + ], +) +def test_va_profile_opt_in_out_lambda_handler( notify_db_session, jwt_encoded, - put_mock, - get_integration_testing_public_cert_mock, + mock_env_vars, + mock_date, + expected_month, ): """ When the lambda handler is invoked with a path that includes the URL parameter "integration_test", @@ -662,8 +738,50 @@ def test_va_profile_opt_in_out_lambda_handler_integration_testing( This unit test verifies that the lambda code attempts to load this certificate. """ + # Must reload lambda module to properly mock ENV variables defined before running lambda handler + importlib.reload(va_profile_opt_in_out_lambda) + + # Mock datetime to ensure cutoff logic works as expected + mock_datetime = patch('lambda_functions.va_profile.va_profile_opt_in_out_lambda.datetime').start() + mock_datetime.now.return_value = mock_date + mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) + + # Create mock responses for PUT request to VAPROFILE + mock_put_instance = Mock() + mock_put_response = Mock() + mock_put_response.status = 200 + mock_put_response.headers = {'Content-Type': 'application/json'} + mock_put_response.read.return_value = b'{"dateTime": "2022-04-07T19:37:59.320Z","status": "COMPLETED_SUCCESS",}' + mock_put_instance.getresponse.return_value = mock_put_response + + # Create mock response for POST request to VANotify + mock_post_instance = Mock() + mock_post_response = Mock() + mock_post_response.status = 201 + mock_post_response.read.return_value = b'{"id":"e7b8cdda-858e-4b6f-a7df-93a71a2edb1e"}' + mock_post_instance.getresponse.return_value = mock_post_response + + # Use a list of mocks for side_effect to differentiate between calls + https_connection_side_effect = [mock_put_instance, mock_post_instance] + + # Patch HTTPSConnection with side_effect for PUT request and POST request + patch( + 'lambda_functions.va_profile.va_profile_opt_in_out_lambda.HTTPSConnection', + side_effect=https_connection_side_effect, + ).start() + + mock_ssm = patch('boto3.client').start() + mock_ssm_instance = mock_ssm.return_value + mock_ssm_instance.get_parameter.return_value = {'Parameter': {'Value': 'mock_va_notify_api_key'}} + + # Generate a dynamic JWT token using the mocked API key + # Use to compare against actual request sent + encoded_header = generate_jwt() + + # Setup new va_profile_id va_profile_id = randint(1000, 100000) + # Check initial state in DB (there should be no records) stmt = ( select(func.count()) .select_from(VAProfileLocalCache) @@ -673,68 +791,47 @@ def test_va_profile_opt_in_out_lambda_handler_integration_testing( VAProfileLocalCache.communication_channel_id == 1, ) ) - assert notify_db_session.session.scalar(stmt) == 0 + # Create the event with appropriate parameters for COMP and PEN Opt-In event = create_event('txAuditId', 'txAuditId', '2022-04-07T19:37:59.320Z', va_profile_id, 1, 5, True, jwt_encoded) - event['queryStringParameters'] = {'integration_test': "the value doesn't matter"} + + # Call the Lambda Handler with the test event response = va_profile_opt_in_out_lambda_handler(event, None) + # Validate the response from the lambda handler assert isinstance(response, dict) assert response['statusCode'] == 200 - assert response.get('headers', {}).get('Content-Type', '') == 'application/json' - response_body = loads(response.get('body', '{}')) - assert 'put_body' in response_body + # Assert PUT request to VAProfile was made with correct parameters expected_put_body = { 'dateTime': '2022-04-07T19:37:59.320Z', 'status': 'COMPLETED_SUCCESS', } + mock_put_instance.request_assert_called_once_with('txAuditId', expected_put_body) + mock_put_instance.request.assert_called_once() + + # Assert POST request to VANotify was made with correct parameters, including the expected month + mock_post_instance.request.assert_called_once_with( + 'POST', + '/v2/notifications/sms', + body=json.dumps( + { + 'template_id': 'mock_template_id', + 'recipient_identifier': {'id_type': 'VAPROFILEID', 'id_value': str(va_profile_id)}, + 'sms_sender_id': 'mock_sms_sender_id', + 'personalisation': {'month-name': expected_month}, + } + ), + headers={'Authorization': f'Bearer {encoded_header}', 'Content-Type': 'application/json'}, + ) - put_mock.assert_called_once_with('txAuditId', expected_put_body) - get_integration_testing_public_cert_mock.assert_called_once() - assert response_body['put_body'] == expected_put_body - - # Verify one row was created using a delete statement that doubles as teardown. + # Verify that one row was created in the DB stmt = delete(VAProfileLocalCache).where( VAProfileLocalCache.va_profile_id == va_profile_id, VAProfileLocalCache.communication_item_id == 5, VAProfileLocalCache.communication_channel_id == 1, + VAProfileLocalCache.notification_id == 'e7b8cdda-858e-4b6f-a7df-93a71a2edb1e', ) assert notify_db_session.session.execute(stmt).rowcount == 1 notify_db_session.session.commit() - - -def create_event( - master_tx_audit_id: str, - tx_audit_id: str, - source_date: str, - va_profile_id: int, - communication_channel_id: int, - communication_item_id: int, - is_allowed: bool, - jwt_value, -) -> dict: - """ - Return a dictionary in the format of the payload the lambda function expects to - receive from VA Profile via AWS API Gateway v2. - """ - - return { - 'headers': { - 'Authorization': f'Bearer {jwt_value}', - }, - 'body': { - 'txAuditId': master_tx_audit_id, - 'bios': [ - { - 'txAuditId': tx_audit_id, - 'sourceDate': source_date, - 'vaProfileId': va_profile_id, - 'communicationChannelId': communication_channel_id, - 'communicationItemId': communication_item_id, - 'allowed': is_allowed, - } - ], - }, - }