From bc907aba888e7a9e66175220457c95be8ec46585 Mon Sep 17 00:00:00 2001 From: steph-torres-nhs <173282814+steph-torres-nhs@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:37:30 +0100 Subject: [PATCH 1/8] introduce conditional rendering of action url --- .../models/templates/slack_alert_blocks.json | 2 +- lambdas/services/im_alerting_service.py | 5 +++- ...ert.json => mock_slack_initial_alert.json} | 0 .../tests/unit/services/test_im_alerting.py | 26 ++++++++++++++++--- .../test_upload_document_reference_service.py | 3 ++- lambdas/tests/unit/utils/test_s3_utils.py | 7 ++--- 6 files changed, 34 insertions(+), 9 deletions(-) rename lambdas/tests/unit/helpers/data/{mock_slack_alert.json => mock_slack_initial_alert.json} (100%) diff --git a/lambdas/models/templates/slack_alert_blocks.json b/lambdas/models/templates/slack_alert_blocks.json index bfed9f8745..76592d3327 100644 --- a/lambdas/models/templates/slack_alert_blocks.json +++ b/lambdas/models/templates/slack_alert_blocks.json @@ -27,7 +27,7 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "*Info:*\n <{{ action_url }}>" + "text": "{% if is_initial_message %}*Info:*\n <{{ action_url }}>{% endif %}" } } ] \ No newline at end of file diff --git a/lambdas/services/im_alerting_service.py b/lambdas/services/im_alerting_service.py index fa96935423..c5abf63c8e 100644 --- a/lambdas/services/im_alerting_service.py +++ b/lambdas/services/im_alerting_service.py @@ -547,7 +547,9 @@ def update_original_slack_message(self, alarm_entry: AlarmEntry): f"Unexpected error updating original Slack message for alarm {alarm_entry.alarm_name_metric}: {e}" ) - def compose_slack_message_blocks(self, alarm_entry: AlarmEntry): + def compose_slack_message_blocks( + self, alarm_entry: AlarmEntry, is_initial_message: bool + ): with open(f"{os.getcwd()}/models/templates/slack_alert_blocks.json", "r") as f: template_content = f.read() @@ -560,6 +562,7 @@ def compose_slack_message_blocks(self, alarm_entry: AlarmEntry): "action_url": self.create_action_url( self.confluence_base_url, alarm_entry.alarm_name_metric ), + "is_initial_message": is_initial_message, } rendered_json = template.render(context) diff --git a/lambdas/tests/unit/helpers/data/mock_slack_alert.json b/lambdas/tests/unit/helpers/data/mock_slack_initial_alert.json similarity index 100% rename from lambdas/tests/unit/helpers/data/mock_slack_alert.json rename to lambdas/tests/unit/helpers/data/mock_slack_initial_alert.json diff --git a/lambdas/tests/unit/services/test_im_alerting.py b/lambdas/tests/unit/services/test_im_alerting.py index c63e60f555..9898a40927 100644 --- a/lambdas/tests/unit/services/test_im_alerting.py +++ b/lambdas/tests/unit/services/test_im_alerting.py @@ -473,7 +473,7 @@ def test_compose_teams_message(alerting_service): assert actual == expected -def test_compose_slack_message_blocks(alerting_service): +def test_compose_slack_message_blocks_initial_message(alerting_service): alarm_entry = AlarmEntry( alarm_name_metric=ALARM_METRIC_NAME, time_created=ALERT_TIMESTAMP, @@ -481,9 +481,29 @@ def test_compose_slack_message_blocks(alerting_service): channel_id=MOCK_ALERTING_SLACK_CHANNEL_ID, history=[AlarmSeverity.HIGH], ) - expected = read_json("../helpers/data/mock_slack_alert.json") + expected = read_json("../helpers/data/mock_slack_initial_alert.json") - actual = alerting_service.compose_slack_message_blocks(alarm_entry) + actual = alerting_service.compose_slack_message_blocks( + alarm_entry=alarm_entry, is_initial_message=True + ) + + assert actual == expected + + +def test_compose_slack_message_blocks_message_reply(alerting_service): + alarm_entry = AlarmEntry( + alarm_name_metric=ALARM_METRIC_NAME, + time_created=ALERT_TIMESTAMP, + last_updated=ALERT_TIMESTAMP, + channel_id=MOCK_ALERTING_SLACK_CHANNEL_ID, + history=[AlarmSeverity.HIGH], + ) + + expected = read_json("../helpers/data/mock_slack_reply.json") + + actual = alerting_service.compose_slack_message_blocks( + alarm_entry=alarm_entry, is_initial_message=False + ) assert actual == expected diff --git a/lambdas/tests/unit/services/test_upload_document_reference_service.py b/lambdas/tests/unit/services/test_upload_document_reference_service.py index 1c8e37473f..45238fcd38 100644 --- a/lambdas/tests/unit/services/test_upload_document_reference_service.py +++ b/lambdas/tests/unit/services/test_upload_document_reference_service.py @@ -3,7 +3,6 @@ import pytest from botocore.exceptions import ClientError from enums.virus_scan_result import VirusScanResult -from lambdas.enums.snomed_codes import SnomedCodes from models.document_reference import DocumentReference from services.mock_virus_scan_service import MockVirusScanService from services.upload_document_reference_service import UploadDocumentReferenceService @@ -17,6 +16,8 @@ from utils.common_query_filters import PreliminaryStatus from utils.exceptions import DocumentServiceException, FileProcessingException +from lambdas.enums.snomed_codes import SnomedCodes + @pytest.fixture def mock_document_reference(): diff --git a/lambdas/tests/unit/utils/test_s3_utils.py b/lambdas/tests/unit/utils/test_s3_utils.py index 0492017e97..d8b49f29cb 100644 --- a/lambdas/tests/unit/utils/test_s3_utils.py +++ b/lambdas/tests/unit/utils/test_s3_utils.py @@ -1,9 +1,10 @@ import pytest from enums.lambda_error import LambdaError -from lambdas.enums.snomed_codes import SnomedCodes -from tests.unit.conftest import MOCK_PDM_BUCKET, MOCK_LG_BUCKET -from utils.s3_utils import DocTypeS3BucketRouter +from tests.unit.conftest import MOCK_LG_BUCKET, MOCK_PDM_BUCKET from utils.lambda_exceptions import InvalidDocTypeException +from utils.s3_utils import DocTypeS3BucketRouter + +from lambdas.enums.snomed_codes import SnomedCodes @pytest.mark.parametrize( From c1d320f46295da7103e2affeecb3f3096472ff3a Mon Sep 17 00:00:00 2001 From: steph-torres-nhs <173282814+steph-torres-nhs@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:38:08 +0100 Subject: [PATCH 2/8] add mock slack reply message --- .../unit/helpers/data/mock_slack_reply.json | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 lambdas/tests/unit/helpers/data/mock_slack_reply.json diff --git a/lambdas/tests/unit/helpers/data/mock_slack_reply.json b/lambdas/tests/unit/helpers/data/mock_slack_reply.json new file mode 100644 index 0000000000..78b1a01da1 --- /dev/null +++ b/lambdas/tests/unit/helpers/data/mock_slack_reply.json @@ -0,0 +1,33 @@ +[ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "dev-test_bulk_upload_metadata_queue ApproximateAgeOfOldestMessage Alert: :red_circle:" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "plain_text", + "text": "History: :red_circle:" + } + }, + { + "type": "section", + "text": { + "type": "plain_text", + "text": "Last state change: 15:10:41 17-04-2025 UTC" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "" + } + } +] \ No newline at end of file From c5621616e3d7ebed28c91c3d8c7384b9814dc8c0 Mon Sep 17 00:00:00 2001 From: steph-torres-nhs <173282814+steph-torres-nhs@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:52:22 +0100 Subject: [PATCH 3/8] [PRMP-577] refactor alerting mock data file structure --- .../unit/handlers/test_im_alerting_handler.py | 11 ++++ .../mock_slack_initial_alert.json | 0 .../data/{ => alerting}/mock_slack_reply.json | 0 .../helpers/data/alerting/mock_sns_alerts.py | 62 +++++++++++++++++++ .../data/{ => alerting}/mock_teams_alert.json | 0 .../tests/unit/services/test_im_alerting.py | 52 ++-------------- 6 files changed, 78 insertions(+), 47 deletions(-) create mode 100644 lambdas/tests/unit/handlers/test_im_alerting_handler.py rename lambdas/tests/unit/helpers/data/{ => alerting}/mock_slack_initial_alert.json (100%) rename lambdas/tests/unit/helpers/data/{ => alerting}/mock_slack_reply.json (100%) create mode 100644 lambdas/tests/unit/helpers/data/alerting/mock_sns_alerts.py rename lambdas/tests/unit/helpers/data/{ => alerting}/mock_teams_alert.json (100%) diff --git a/lambdas/tests/unit/handlers/test_im_alerting_handler.py b/lambdas/tests/unit/handlers/test_im_alerting_handler.py new file mode 100644 index 0000000000..9b7b868f20 --- /dev/null +++ b/lambdas/tests/unit/handlers/test_im_alerting_handler.py @@ -0,0 +1,11 @@ +import pytest + + + +@pytest.fixture +def mock_service_with_alarm_alert(mocker): + mocked_class = mocker.patch("handlers.im_alerting_handler.IMAlertingService") + mocked_instance = mocked_class.return_value + mocked_class.return_value.message = MOCK_ALARM_SNS_ALERT + return mocked_instance + diff --git a/lambdas/tests/unit/helpers/data/mock_slack_initial_alert.json b/lambdas/tests/unit/helpers/data/alerting/mock_slack_initial_alert.json similarity index 100% rename from lambdas/tests/unit/helpers/data/mock_slack_initial_alert.json rename to lambdas/tests/unit/helpers/data/alerting/mock_slack_initial_alert.json diff --git a/lambdas/tests/unit/helpers/data/mock_slack_reply.json b/lambdas/tests/unit/helpers/data/alerting/mock_slack_reply.json similarity index 100% rename from lambdas/tests/unit/helpers/data/mock_slack_reply.json rename to lambdas/tests/unit/helpers/data/alerting/mock_slack_reply.json diff --git a/lambdas/tests/unit/helpers/data/alerting/mock_sns_alerts.py b/lambdas/tests/unit/helpers/data/alerting/mock_sns_alerts.py new file mode 100644 index 0000000000..658ff3090d --- /dev/null +++ b/lambdas/tests/unit/helpers/data/alerting/mock_sns_alerts.py @@ -0,0 +1,62 @@ +from tests.unit.conftest import MOCK_LG_METADATA_SQS_QUEUE + +ALERT_TIME = "2025-04-17T15:10:41.433+0000" + +QUEUE_ALERT_MESSAGE = { + "AlarmName": "dev_lg_bulk_main_oldest_message_alarm_6d", + "AlarmDescription": f"Alarm when a message in queue dev-{MOCK_LG_METADATA_SQS_QUEUE} is older than 6 days.", + "NewStateValue": "ALARM", + "StateChangeTime": ALERT_TIME, + "OldStateValue": "OK", + "Trigger": { + "MetricName": "ApproximateAgeOfOldestMessage", + "Namespace": "AWS/SQS", + "StatisticType": "Statistic", + "Statistic": "Maximum", + "Unit": None, + "Dimensions": [ + { + "QueueName": f"dev-{MOCK_LG_METADATA_SQS_QUEUE}", + } + ], + }, +} + +LAMBDA_ALERT_MESSAGE = { + "AlarmName": "dev-alarm_search_patient_details_handler_error", + "AlarmDescription": "Triggers when an error has occurred in dev_SearchPatientDetailsLambda.", + "AlarmConfigurationUpdatedTimestamp": "2025-04-17T15:08:51.604+0000", + "NewStateValue": "ALARM", + "StateChangeTime": ALERT_TIME, + "OldStateValue": "OK", + "Trigger": { + "MetricName": "Errors", + "Namespace": "AWS/Lambda", + "StatisticType": "Statistic", + "Statistic": "SUM", + "Unit": None, + "Dimensions": [ + { + "value": "dev_SearchPatientDetailsLambda", + "name": "FunctionName", + } + ], + }, +} + +MOCK_ALARM_SNS_ALERT = { + "Records": [ + { + "EventSource": "aws:sns", + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:eu-west-2:xxxxxx:ndra-sns-search_patient_details_alarms-topicxxxxx:xxxxxx", + "Sns": { + "Type": "Notification", + "MessageId": "xxxxxx", + "TopicArn": "arn:aws:sns:eu-west-2:xxxxxx:ndra-sns-search_patient_details_alarms-topicxxxxx", + "Subject": 'ALARM: "ndra-alarm_search_patient_details_handler_error" in EU (London)', + "Message": LAMBDA_ALERT_MESSAGE, + }, + } + ] + } \ No newline at end of file diff --git a/lambdas/tests/unit/helpers/data/mock_teams_alert.json b/lambdas/tests/unit/helpers/data/alerting/mock_teams_alert.json similarity index 100% rename from lambdas/tests/unit/helpers/data/mock_teams_alert.json rename to lambdas/tests/unit/helpers/data/alerting/mock_teams_alert.json diff --git a/lambdas/tests/unit/services/test_im_alerting.py b/lambdas/tests/unit/services/test_im_alerting.py index 9898a40927..5e9f8dfb0e 100644 --- a/lambdas/tests/unit/services/test_im_alerting.py +++ b/lambdas/tests/unit/services/test_im_alerting.py @@ -16,6 +16,8 @@ MOCK_LG_METADATA_SQS_QUEUE, ) +from tests.unit.helpers.data.alerting.mock_sns_alerts import QUEUE_ALERT_MESSAGE, LAMBDA_ALERT_MESSAGE + ALERT_TIME = "2025-04-17T15:10:41.433+0000" TTL_IN_SECONDS = 300 @@ -28,56 +30,12 @@ BASE_URL = MOCK_CONFLUENCE_URL ALERT_TIMESTAMP = int(datetime.fromisoformat(ALERT_TIME).timestamp()) -QUEUE_ALERT_MESSAGE = { - "AlarmName": "dev_lg_bulk_main_oldest_message_alarm_6d", - "AlarmDescription": f"Alarm when a message in queue dev-{MOCK_LG_METADATA_SQS_QUEUE} is older than 6 days.", - "NewStateValue": "ALARM", - "StateChangeTime": ALERT_TIME, - "OldStateValue": "OK", - "Trigger": { - "MetricName": "ApproximateAgeOfOldestMessage", - "Namespace": "AWS/SQS", - "StatisticType": "Statistic", - "Statistic": "Maximum", - "Unit": None, - "Dimensions": [ - { - "QueueName": f"dev-{MOCK_LG_METADATA_SQS_QUEUE}", - } - ], - }, -} - QUEUE_ALERT_TAGS = { "alarm_group": f"dev-{MOCK_LG_METADATA_SQS_QUEUE}", "alarm_metric": "ApproximateAgeOfOldestMessage", "severity": "medium", } - -LAMBDA_ALERT_MESSAGE = { - "AlarmName": "dev-alarm_search_patient_details_handler_error", - "AlarmDescription": "Triggers when an error has occurred in dev_SearchPatientDetailsLambda.", - "AlarmConfigurationUpdatedTimestamp": "2025-04-17T15:08:51.604+0000", - "NewStateValue": "ALARM", - "StateChangeTime": ALERT_TIME, - "OldStateValue": "OK", - "Trigger": { - "MetricName": "Errors", - "Namespace": "AWS/Lambda", - "StatisticType": "Statistic", - "Statistic": "SUM", - "Unit": None, - "Dimensions": [ - { - "value": "dev_SearchPatientDetailsLambda", - "name": "FunctionName", - } - ], - }, -} - - def read_json(filename: str) -> str: filepath = os.path.join(os.path.dirname(__file__), filename) with open(filepath, "r") as file: @@ -467,7 +425,7 @@ def test_compose_teams_message(alerting_service): channel_id=MOCK_ALERTING_SLACK_CHANNEL_ID, history=[AlarmSeverity.HIGH], ) - expected = read_json("../helpers/data/mock_teams_alert.json") + expected = read_json("../helpers/data/alerting/mock_teams_alert.json") actual = json.loads(alerting_service.compose_teams_message(alarm_entry)) assert actual == expected @@ -481,7 +439,7 @@ def test_compose_slack_message_blocks_initial_message(alerting_service): channel_id=MOCK_ALERTING_SLACK_CHANNEL_ID, history=[AlarmSeverity.HIGH], ) - expected = read_json("../helpers/data/mock_slack_initial_alert.json") + expected = read_json("../helpers/data/alerting/mock_slack_initial_alert.json") actual = alerting_service.compose_slack_message_blocks( alarm_entry=alarm_entry, is_initial_message=True @@ -499,7 +457,7 @@ def test_compose_slack_message_blocks_message_reply(alerting_service): history=[AlarmSeverity.HIGH], ) - expected = read_json("../helpers/data/mock_slack_reply.json") + expected = read_json("../helpers/data/alerting/mock_slack_reply.json") actual = alerting_service.compose_slack_message_blocks( alarm_entry=alarm_entry, is_initial_message=False From 2798e81699ea4e480ccddb133bc1be0e4488a909 Mon Sep 17 00:00:00 2001 From: steph-torres-nhs <173282814+steph-torres-nhs@users.noreply.github.com> Date: Fri, 17 Oct 2025 11:05:48 +0100 Subject: [PATCH 4/8] [PRMP-557] introduce handler testing --- lambdas/enums/mtls.py | 1 + lambdas/tests/unit/enums/test_mtls.py | 1 - .../unit/handlers/test_im_alerting_handler.py | 44 ++++++++++++++++++- .../helpers/data/alerting/mock_sns_alerts.py | 38 +++++++++------- ...est_get_fhir_document_reference_service.py | 2 +- .../tests/unit/services/test_im_alerting.py | 7 ++- lambdas/tests/unit/utils/test_dynamo_utils.py | 2 +- 7 files changed, 71 insertions(+), 24 deletions(-) diff --git a/lambdas/enums/mtls.py b/lambdas/enums/mtls.py index f61c9784e4..f39806783c 100644 --- a/lambdas/enums/mtls.py +++ b/lambdas/enums/mtls.py @@ -1,4 +1,5 @@ from enum import StrEnum, auto + from enums.lambda_error import LambdaError from utils.audit_logging_setup import LoggingService from utils.lambda_exceptions import InvalidDocTypeException diff --git a/lambdas/tests/unit/enums/test_mtls.py b/lambdas/tests/unit/enums/test_mtls.py index 48eadfb712..cc0c565661 100644 --- a/lambdas/tests/unit/enums/test_mtls.py +++ b/lambdas/tests/unit/enums/test_mtls.py @@ -1,5 +1,4 @@ import pytest - from enums.lambda_error import LambdaError from enums.mtls import MtlsCommonNames from utils.lambda_exceptions import InvalidDocTypeException diff --git a/lambdas/tests/unit/handlers/test_im_alerting_handler.py b/lambdas/tests/unit/handlers/test_im_alerting_handler.py index 9b7b868f20..88d1ce1ccd 100644 --- a/lambdas/tests/unit/handlers/test_im_alerting_handler.py +++ b/lambdas/tests/unit/handlers/test_im_alerting_handler.py @@ -1,11 +1,51 @@ -import pytest +import json +import pytest +from handlers.im_alerting_handler import lambda_handler +from tests.unit.helpers.data.alerting.mock_sns_alerts import ( + LAMBDA_ALERT_MESSAGE, + MOCK_VIRUS_SCANNER_ALERT_SNS_MESSAGE, +) @pytest.fixture def mock_service_with_alarm_alert(mocker): mocked_class = mocker.patch("handlers.im_alerting_handler.IMAlertingService") mocked_instance = mocked_class.return_value - mocked_class.return_value.message = MOCK_ALARM_SNS_ALERT + mocker.patch.object(mocked_instance, "dynamo_service") + mocked_class.return_value.message = LAMBDA_ALERT_MESSAGE + return mocked_instance + + +@pytest.fixture +def mock_service_with_virus_scanner_alert(mocker): + mocked_class = mocker.patch("handlers.im_alerting_handler.IMAlertingService") + mocked_instance = mocked_class.return_value + mocker.patch.object(mocked_instance, "dynamo_service") + mocked_class.return_value.message = MOCK_VIRUS_SCANNER_ALERT_SNS_MESSAGE return mocked_instance + +def test_handler_calls_handle_alarm_message_lambda_triggered_by_alarm_message( + mock_service_with_alarm_alert, context, set_env +): + + event = {"Records": [{"Sns": {"Message": json.dumps(LAMBDA_ALERT_MESSAGE)}}]} + lambda_handler(event, context) + + mock_service_with_alarm_alert.handle_virus_scanner_alert.assert_not_called() + mock_service_with_alarm_alert.handle_alarm_alert.assert_called() + + +def test_handler_calls_handle_virus_scanner_alert_lambda_triggered_by_virus_scanner_sns( + mock_service_with_virus_scanner_alert, context, set_env +): + + event = { + "Records": [ + {"Sns": {"Message": json.dumps(MOCK_VIRUS_SCANNER_ALERT_SNS_MESSAGE)}} + ] + } + lambda_handler(event, context) + pass + # mock_service_with_virus_scanner_alert.handle_virus_scanner_alert.assert_called() diff --git a/lambdas/tests/unit/helpers/data/alerting/mock_sns_alerts.py b/lambdas/tests/unit/helpers/data/alerting/mock_sns_alerts.py index 658ff3090d..bbd55d7b7c 100644 --- a/lambdas/tests/unit/helpers/data/alerting/mock_sns_alerts.py +++ b/lambdas/tests/unit/helpers/data/alerting/mock_sns_alerts.py @@ -1,4 +1,4 @@ -from tests.unit.conftest import MOCK_LG_METADATA_SQS_QUEUE +from tests.unit.conftest import MOCK_LG_METADATA_SQS_QUEUE, TEST_UUID ALERT_TIME = "2025-04-17T15:10:41.433+0000" @@ -44,19 +44,23 @@ }, } -MOCK_ALARM_SNS_ALERT = { - "Records": [ - { - "EventSource": "aws:sns", - "EventVersion": "1.0", - "EventSubscriptionArn": "arn:aws:sns:eu-west-2:xxxxxx:ndra-sns-search_patient_details_alarms-topicxxxxx:xxxxxx", - "Sns": { - "Type": "Notification", - "MessageId": "xxxxxx", - "TopicArn": "arn:aws:sns:eu-west-2:xxxxxx:ndra-sns-search_patient_details_alarms-topicxxxxx", - "Subject": 'ALARM: "ndra-alarm_search_patient_details_handler_error" in EU (London)', - "Message": LAMBDA_ALERT_MESSAGE, - }, - } - ] - } \ No newline at end of file +MOCK_LAMBDA_ALARM_SNS_ALERT = { + "EventSource": "aws:sns", + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:region:xxxxxx:dev-sns-search_patient_details_alarms-topicxxxxx:xxxxxx", + "Sns": { + "Type": "Notification", + "MessageId": "xxxxxx", + "TopicArn": "arn:aws:sns:region:xxxxxx:dev-sns-search_patient_details_alarms-topicxxxxx", + "Subject": 'ALARM: "dev-alarm_search_patient_details_handler_error"', + "Message": LAMBDA_ALERT_MESSAGE, + }, +} + +MOCK_VIRUS_SCANNER_ALERT_SNS_MESSAGE = { + "Type": "Notification", + "MessageId": "xxxxxx", + "TopicArn": "", + "Subject": "", + "Message": {"id": TEST_UUID, "dateScanned": ALERT_TIME, "result": "Error"}, +} diff --git a/lambdas/tests/unit/services/test_get_fhir_document_reference_service.py b/lambdas/tests/unit/services/test_get_fhir_document_reference_service.py index 93433202c2..1246808a3a 100644 --- a/lambdas/tests/unit/services/test_get_fhir_document_reference_service.py +++ b/lambdas/tests/unit/services/test_get_fhir_document_reference_service.py @@ -5,7 +5,7 @@ import pytest from enums.lambda_error import LambdaError -from enums.snomed_codes import SnomedCodes, SnomedCode +from enums.snomed_codes import SnomedCode, SnomedCodes from services.get_fhir_document_reference_service import GetFhirDocumentReferenceService from tests.unit.conftest import MOCK_LG_TABLE_NAME, MOCK_PDM_TABLE_NAME from tests.unit.helpers.data.test_documents import create_test_doc_store_refs diff --git a/lambdas/tests/unit/services/test_im_alerting.py b/lambdas/tests/unit/services/test_im_alerting.py index 5e9f8dfb0e..644b699bb0 100644 --- a/lambdas/tests/unit/services/test_im_alerting.py +++ b/lambdas/tests/unit/services/test_im_alerting.py @@ -15,8 +15,10 @@ MOCK_CONFLUENCE_URL, MOCK_LG_METADATA_SQS_QUEUE, ) - -from tests.unit.helpers.data.alerting.mock_sns_alerts import QUEUE_ALERT_MESSAGE, LAMBDA_ALERT_MESSAGE +from tests.unit.helpers.data.alerting.mock_sns_alerts import ( + LAMBDA_ALERT_MESSAGE, + QUEUE_ALERT_MESSAGE, +) ALERT_TIME = "2025-04-17T15:10:41.433+0000" TTL_IN_SECONDS = 300 @@ -36,6 +38,7 @@ "severity": "medium", } + def read_json(filename: str) -> str: filepath = os.path.join(os.path.dirname(__file__), filename) with open(filepath, "r") as file: diff --git a/lambdas/tests/unit/utils/test_dynamo_utils.py b/lambdas/tests/unit/utils/test_dynamo_utils.py index c9ca316af3..a38399b33c 100644 --- a/lambdas/tests/unit/utils/test_dynamo_utils.py +++ b/lambdas/tests/unit/utils/test_dynamo_utils.py @@ -25,7 +25,7 @@ create_update_expression, parse_dynamo_record, ) -from utils.lambda_exceptions import CreateDocumentRefException, InvalidDocTypeException +from utils.lambda_exceptions import InvalidDocTypeException from lambdas.enums.snomed_codes import SnomedCodes From 38652b9bea355fe4335fb2edea3bd001bd96248e Mon Sep 17 00:00:00 2001 From: steph-torres-nhs <173282814+steph-torres-nhs@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:09:24 +0100 Subject: [PATCH 5/8] [PRM-557] add check against virus scanner topic arn --- lambdas/handlers/im_alerting_handler.py | 8 ++++++++ lambdas/models/fhir/R4/fhir_document_reference.py | 1 - lambdas/services/bulk_upload_metadata_service.py | 7 ++++--- lambdas/services/bulk_upload_service.py | 1 - .../post_fhir_document_reference_service.py | 2 -- lambdas/tests/unit/conftest.py | 1 + .../unit/handlers/test_im_alerting_handler.py | 15 ++++++++++----- .../unit/helpers/data/alerting/mock_sns_alerts.py | 6 +++--- .../unit/helpers/data/bulk_upload/test_data.py | 6 +++--- .../services/test_bulk_upload_metadata_service.py | 1 - .../test_get_fhir_document_reference_service.py | 2 +- lambdas/tests/unit/services/test_im_alerting.py | 6 +++--- .../test_post_fhir_document_reference_service.py | 5 +---- 13 files changed, 34 insertions(+), 27 deletions(-) diff --git a/lambdas/handlers/im_alerting_handler.py b/lambdas/handlers/im_alerting_handler.py index c49e6419d3..94a25c7a96 100644 --- a/lambdas/handlers/im_alerting_handler.py +++ b/lambdas/handlers/im_alerting_handler.py @@ -1,4 +1,5 @@ import json +import os from services.im_alerting_service import IMAlertingService from utils.audit_logging_setup import LoggingService @@ -21,6 +22,7 @@ "SLACK_CHANNEL_ID", "SLACK_BOT_TOKEN", "WORKSPACE", + "VIRUS_SCANNER_TOPIC_ARN", ] ) def lambda_handler(event, context): @@ -33,3 +35,9 @@ def lambda_handler(event, context): message_service = IMAlertingService(message) message_service.handle_alarm_alert() + + +def is_virus_scanner_topic(message): + + topic_arn = message.get("TopicArn", "") + return topic_arn == os.environ["VIRUS_SCANNER_TOPIC_ARN"] diff --git a/lambdas/models/fhir/R4/fhir_document_reference.py b/lambdas/models/fhir/R4/fhir_document_reference.py index f0dd5a3229..07109261e7 100644 --- a/lambdas/models/fhir/R4/fhir_document_reference.py +++ b/lambdas/models/fhir/R4/fhir_document_reference.py @@ -12,7 +12,6 @@ Reference, ) from pydantic import BaseModel, Field - from utils.ods_utils import PCSE_ODS_CODE # Constants diff --git a/lambdas/services/bulk_upload_metadata_service.py b/lambdas/services/bulk_upload_metadata_service.py index 38d2d6c53d..1eb60f1e8c 100644 --- a/lambdas/services/bulk_upload_metadata_service.py +++ b/lambdas/services/bulk_upload_metadata_service.py @@ -8,12 +8,12 @@ import pydantic from botocore.exceptions import ClientError - from models.staging_metadata import ( NHS_NUMBER_FIELD_NAME, ODS_CODE, + BulkUploadQueueMetadata, MetadataFile, - StagingSqsMetadata, BulkUploadQueueMetadata, + StagingSqsMetadata, ) from services.base.s3_service import S3Service from services.base.sqs_service import SQSService @@ -114,7 +114,8 @@ def csv_to_staging_sqs_metadata(csv_file_path: str) -> list[StagingSqsMetadata]: nhs_number=nhs_number, files=[ BulkUploadQueueMetadata( - **metadata_file.model_dump(), stored_file_name=metadata_file.file_path + **metadata_file.model_dump(), + stored_file_name=metadata_file.file_path, ) for metadata_file in patients[nhs_number, ods_code] ], diff --git a/lambdas/services/bulk_upload_service.py b/lambdas/services/bulk_upload_service.py index 8e5fa804cc..2d58bdf81d 100644 --- a/lambdas/services/bulk_upload_service.py +++ b/lambdas/services/bulk_upload_service.py @@ -5,7 +5,6 @@ import pydantic from botocore.exceptions import ClientError - from enums.patient_ods_inactive_status import PatientOdsInactiveStatus from enums.snomed_codes import SnomedCodes from enums.upload_status import UploadStatus diff --git a/lambdas/services/post_fhir_document_reference_service.py b/lambdas/services/post_fhir_document_reference_service.py index aad2d8adde..9e8537c336 100644 --- a/lambdas/services/post_fhir_document_reference_service.py +++ b/lambdas/services/post_fhir_document_reference_service.py @@ -6,7 +6,6 @@ from botocore.exceptions import ClientError from enums.lambda_error import LambdaError from enums.mtls import MtlsCommonNames -from enums.patient_ods_inactive_status import PatientOdsInactiveStatus from enums.snomed_codes import SnomedCode, SnomedCodes from models.document_reference import DocumentReference from models.fhir.R4.fhir_document_reference import SNOMED_URL, Attachment @@ -28,7 +27,6 @@ ) from utils.lambda_exceptions import CreateDocumentRefException from utils.lambda_header_utils import validate_common_name_in_mtls -from utils.ods_utils import PCSE_ODS_CODE from utils.utilities import create_reference_id, get_pds_service, validate_nhs_number logger = LoggingService(__name__) diff --git a/lambdas/tests/unit/conftest.py b/lambdas/tests/unit/conftest.py index 03cc926e3e..3b05bd4d4f 100644 --- a/lambdas/tests/unit/conftest.py +++ b/lambdas/tests/unit/conftest.py @@ -225,6 +225,7 @@ def set_env(monkeypatch): monkeypatch.setenv("SLACK_BOT_TOKEN", MOCK_SLACK_BOT_TOKEN) monkeypatch.setenv("SLACK_CHANNEL_ID", MOCK_ALERTING_SLACK_CHANNEL_ID) monkeypatch.setenv("ITOC_TESTING_ODS_CODES", MOCK_ITOC_ODS_CODES) + monkeypatch.setenv("VIRUS_SCANNER_TOPIC_ARN", "virus_scanner_topic_arn") EXPECTED_PARSED_PATIENT_BASE_CASE = PatientDetails( diff --git a/lambdas/tests/unit/handlers/test_im_alerting_handler.py b/lambdas/tests/unit/handlers/test_im_alerting_handler.py index 88d1ce1ccd..48f1e3aef8 100644 --- a/lambdas/tests/unit/handlers/test_im_alerting_handler.py +++ b/lambdas/tests/unit/handlers/test_im_alerting_handler.py @@ -1,9 +1,9 @@ import json import pytest -from handlers.im_alerting_handler import lambda_handler +from handlers.im_alerting_handler import is_virus_scanner_topic, lambda_handler from tests.unit.helpers.data.alerting.mock_sns_alerts import ( - LAMBDA_ALERT_MESSAGE, + MOCK_LAMBDA_ALERT_MESSAGE, MOCK_VIRUS_SCANNER_ALERT_SNS_MESSAGE, ) @@ -13,7 +13,7 @@ def mock_service_with_alarm_alert(mocker): mocked_class = mocker.patch("handlers.im_alerting_handler.IMAlertingService") mocked_instance = mocked_class.return_value mocker.patch.object(mocked_instance, "dynamo_service") - mocked_class.return_value.message = LAMBDA_ALERT_MESSAGE + mocked_class.return_value.message = MOCK_LAMBDA_ALERT_MESSAGE return mocked_instance @@ -30,7 +30,7 @@ def test_handler_calls_handle_alarm_message_lambda_triggered_by_alarm_message( mock_service_with_alarm_alert, context, set_env ): - event = {"Records": [{"Sns": {"Message": json.dumps(LAMBDA_ALERT_MESSAGE)}}]} + event = {"Records": [{"Sns": {"Message": json.dumps(MOCK_LAMBDA_ALERT_MESSAGE)}}]} lambda_handler(event, context) mock_service_with_alarm_alert.handle_virus_scanner_alert.assert_not_called() @@ -47,5 +47,10 @@ def test_handler_calls_handle_virus_scanner_alert_lambda_triggered_by_virus_scan ] } lambda_handler(event, context) - pass # mock_service_with_virus_scanner_alert.handle_virus_scanner_alert.assert_called() + + +def test_is_virus_scanner_topic(set_env): + + assert is_virus_scanner_topic(MOCK_VIRUS_SCANNER_ALERT_SNS_MESSAGE) + assert not is_virus_scanner_topic(MOCK_LAMBDA_ALERT_MESSAGE) diff --git a/lambdas/tests/unit/helpers/data/alerting/mock_sns_alerts.py b/lambdas/tests/unit/helpers/data/alerting/mock_sns_alerts.py index bbd55d7b7c..f68117bcb5 100644 --- a/lambdas/tests/unit/helpers/data/alerting/mock_sns_alerts.py +++ b/lambdas/tests/unit/helpers/data/alerting/mock_sns_alerts.py @@ -22,7 +22,7 @@ }, } -LAMBDA_ALERT_MESSAGE = { +MOCK_LAMBDA_ALERT_MESSAGE = { "AlarmName": "dev-alarm_search_patient_details_handler_error", "AlarmDescription": "Triggers when an error has occurred in dev_SearchPatientDetailsLambda.", "AlarmConfigurationUpdatedTimestamp": "2025-04-17T15:08:51.604+0000", @@ -53,14 +53,14 @@ "MessageId": "xxxxxx", "TopicArn": "arn:aws:sns:region:xxxxxx:dev-sns-search_patient_details_alarms-topicxxxxx", "Subject": 'ALARM: "dev-alarm_search_patient_details_handler_error"', - "Message": LAMBDA_ALERT_MESSAGE, + "Message": MOCK_LAMBDA_ALERT_MESSAGE, }, } MOCK_VIRUS_SCANNER_ALERT_SNS_MESSAGE = { "Type": "Notification", "MessageId": "xxxxxx", - "TopicArn": "", + "TopicArn": "virus_scanner_topic_arn", "Subject": "", "Message": {"id": TEST_UUID, "dateScanned": ALERT_TIME, "result": "Error"}, } diff --git a/lambdas/tests/unit/helpers/data/bulk_upload/test_data.py b/lambdas/tests/unit/helpers/data/bulk_upload/test_data.py index dd0c3e4fe3..c8aa9328fc 100644 --- a/lambdas/tests/unit/helpers/data/bulk_upload/test_data.py +++ b/lambdas/tests/unit/helpers/data/bulk_upload/test_data.py @@ -1,10 +1,8 @@ import os -from freezegun import freeze_time - from enums.snomed_codes import SnomedCodes from enums.virus_scan_result import VirusScanResult -from lambdas.enums.nrl_sqs_upload import NrlActionTypes +from freezegun import freeze_time from models.document_reference import DocumentReference from models.sqs.nrl_sqs_message import NrlSqsMessage from models.sqs.pdf_stitching_sqs_message import PdfStitchingSqsMessage @@ -18,6 +16,8 @@ ) from tests.unit.conftest import MOCK_LG_BUCKET, TEST_CURRENT_GP_ODS, TEST_UUID +from lambdas.enums.nrl_sqs_upload import NrlActionTypes + convert_to_sqs_metadata = BulkUploadMetadataProcessorService.convert_to_sqs_metadata sample_metadata_model = MetadataFile( diff --git a/lambdas/tests/unit/services/test_bulk_upload_metadata_service.py b/lambdas/tests/unit/services/test_bulk_upload_metadata_service.py index a0dc2bff97..f16c0193d2 100644 --- a/lambdas/tests/unit/services/test_bulk_upload_metadata_service.py +++ b/lambdas/tests/unit/services/test_bulk_upload_metadata_service.py @@ -5,7 +5,6 @@ import pytest from botocore.exceptions import ClientError from freezegun import freeze_time - from models.staging_metadata import METADATA_FILENAME from services.bulk_upload_metadata_service import BulkUploadMetadataService from tests.unit.conftest import MOCK_LG_METADATA_SQS_QUEUE, MOCK_STAGING_STORE_BUCKET diff --git a/lambdas/tests/unit/services/test_get_fhir_document_reference_service.py b/lambdas/tests/unit/services/test_get_fhir_document_reference_service.py index 2c5c4fd763..955de9d707 100644 --- a/lambdas/tests/unit/services/test_get_fhir_document_reference_service.py +++ b/lambdas/tests/unit/services/test_get_fhir_document_reference_service.py @@ -13,7 +13,6 @@ GetFhirDocumentReferenceException, InvalidDocTypeException, ) -from utils.lambda_exceptions import GetFhirDocumentReferenceException from utils.ods_utils import PCSE_ODS_CODE @@ -194,6 +193,7 @@ def test_create_document_reference_fhir_response_non_final_status( patched_service.s3_service.get_binary_file.assert_not_called() patched_service.get_presigned_url.assert_not_called() + def test_create_document_reference_fhir_response_when_patient_is_deceased( patched_service, mocker ): diff --git a/lambdas/tests/unit/services/test_im_alerting.py b/lambdas/tests/unit/services/test_im_alerting.py index 644b699bb0..1dad3facae 100644 --- a/lambdas/tests/unit/services/test_im_alerting.py +++ b/lambdas/tests/unit/services/test_im_alerting.py @@ -16,7 +16,7 @@ MOCK_LG_METADATA_SQS_QUEUE, ) from tests.unit.helpers.data.alerting.mock_sns_alerts import ( - LAMBDA_ALERT_MESSAGE, + MOCK_LAMBDA_ALERT_MESSAGE, QUEUE_ALERT_MESSAGE, ) @@ -375,8 +375,8 @@ def test_create_action_url_with_lambda_alert(alerting_service): "https://confluence.example.com#:~:text=SearchPatientDetailsLambda%20Errors" ) alarm_metric_name = ( - f'{LAMBDA_ALERT_MESSAGE["Trigger"]["Dimensions"][0]["value"]}' - f' {LAMBDA_ALERT_MESSAGE["Trigger"]["MetricName"]}' + f'{MOCK_LAMBDA_ALERT_MESSAGE["Trigger"]["Dimensions"][0]["value"]}' + f' {MOCK_LAMBDA_ALERT_MESSAGE["Trigger"]["MetricName"]}' ) actual = alerting_service.create_action_url(BASE_URL, alarm_metric_name) diff --git a/lambdas/tests/unit/services/test_post_fhir_document_reference_service.py b/lambdas/tests/unit/services/test_post_fhir_document_reference_service.py index 5e088076d1..edbde332f7 100644 --- a/lambdas/tests/unit/services/test_post_fhir_document_reference_service.py +++ b/lambdas/tests/unit/services/test_post_fhir_document_reference_service.py @@ -2,7 +2,6 @@ import pytest from botocore.exceptions import ClientError - from enums.lambda_error import LambdaError from enums.mtls import MtlsCommonNames from enums.snomed_codes import SnomedCode, SnomedCodes @@ -542,9 +541,7 @@ def test_create_document_reference_without_custodian(mock_service, mocker): current_gp_ods=current_gp_ods, ) - assert ( - result.custodian == current_gp_ods - ) + assert result.custodian == current_gp_ods def test_create_fhir_response_with_presigned_url(mock_service, mocker): From 1fab1e6f587db4cec04889ae97da6615c7295d7e Mon Sep 17 00:00:00 2001 From: steph-torres-nhs <173282814+steph-torres-nhs@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:16:30 +0100 Subject: [PATCH 6/8] [PRM-557] virus scanner alerting handled if message is from virus scanner topic --- lambdas/handlers/im_alerting_handler.py | 5 +++++ lambdas/services/im_alerting_service.py | 3 +++ lambdas/tests/unit/handlers/test_im_alerting_handler.py | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lambdas/handlers/im_alerting_handler.py b/lambdas/handlers/im_alerting_handler.py index 94a25c7a96..b22d8b017f 100644 --- a/lambdas/handlers/im_alerting_handler.py +++ b/lambdas/handlers/im_alerting_handler.py @@ -34,6 +34,11 @@ def lambda_handler(event, context): logger.info(f"Processing message: {message}") message_service = IMAlertingService(message) + + if is_virus_scanner_topic(message): + message_service.handle_virus_scanner_alert() + return + message_service.handle_alarm_alert() diff --git a/lambdas/services/im_alerting_service.py b/lambdas/services/im_alerting_service.py index c5abf63c8e..4741a97091 100644 --- a/lambdas/services/im_alerting_service.py +++ b/lambdas/services/im_alerting_service.py @@ -40,6 +40,9 @@ def __init__(self, message): "Content-type": "application/json; charset=utf-8", } + def handle_virus_scanner_alert(self): + pass + def handle_alarm_alert(self): alarm_state = self.message["NewStateValue"] alarm_time = self.message["StateChangeTime"] diff --git a/lambdas/tests/unit/handlers/test_im_alerting_handler.py b/lambdas/tests/unit/handlers/test_im_alerting_handler.py index 48f1e3aef8..d9c194bfb7 100644 --- a/lambdas/tests/unit/handlers/test_im_alerting_handler.py +++ b/lambdas/tests/unit/handlers/test_im_alerting_handler.py @@ -47,7 +47,8 @@ def test_handler_calls_handle_virus_scanner_alert_lambda_triggered_by_virus_scan ] } lambda_handler(event, context) - # mock_service_with_virus_scanner_alert.handle_virus_scanner_alert.assert_called() + mock_service_with_virus_scanner_alert.handle_virus_scanner_alert.assert_called() + mock_service_with_virus_scanner_alert.handle_alarm_alert.assert_not_called() def test_is_virus_scanner_topic(set_env): From 6f329ed045702308241d28bf295b7bb0db934e37 Mon Sep 17 00:00:00 2001 From: steph-torres-nhs <173282814+steph-torres-nhs@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:15:26 +0100 Subject: [PATCH 7/8] [PRM-557] add sending of virus scanner alert functionality --- .../virus_scanner_alert_slack_blocks.json | 33 ++++++++++ lambdas/services/im_alerting_service.py | 66 +++++++++++++++++-- .../alerting/mock_virus_scanner_alert.json | 33 ++++++++++ .../tests/unit/services/test_im_alerting.py | 64 ++++++++++++++++-- 4 files changed, 184 insertions(+), 12 deletions(-) create mode 100644 lambdas/models/templates/virus_scanner_alert_slack_blocks.json create mode 100644 lambdas/tests/unit/helpers/data/alerting/mock_virus_scanner_alert.json diff --git a/lambdas/models/templates/virus_scanner_alert_slack_blocks.json b/lambdas/models/templates/virus_scanner_alert_slack_blocks.json new file mode 100644 index 0000000000..e539bce16a --- /dev/null +++ b/lambdas/models/templates/virus_scanner_alert_slack_blocks.json @@ -0,0 +1,33 @@ +[ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "{{ topic }}: {{ scan_result }} {{ severity }}" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "plain_text", + "text": "Scan date and time: {{ scan_date }}" + } + }, + { + "type": "section", + "text": { + "type": "plain_text", + "text": "Scan ID: {{ scan_id }}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Info:*\n <{{ action_url }}>" + } + } +] \ No newline at end of file diff --git a/lambdas/services/im_alerting_service.py b/lambdas/services/im_alerting_service.py index 4741a97091..b2cb664f2a 100644 --- a/lambdas/services/im_alerting_service.py +++ b/lambdas/services/im_alerting_service.py @@ -40,9 +40,6 @@ def __init__(self, message): "Content-type": "application/json; charset=utf-8", } - def handle_virus_scanner_alert(self): - pass - def handle_alarm_alert(self): alarm_state = self.message["NewStateValue"] alarm_time = self.message["StateChangeTime"] @@ -197,6 +194,19 @@ def handle_ok_action_trigger(self, tags: dict, alarm_entry: AlarmEntry): f"Alarm entry for {alarm_entry.alarm_name_metric} has been updated since reaching OK state" ) + def handle_virus_scanner_alert(self): + + slack_blocks = { + "blocks": self.compose_virus_scanner_slack_blocks(), + "channel": os.environ["SLACK_CHANNEL_ID"], + } + + requests.post( + url=self.SLACK_POST_CHAT_API, + headers=self.slack_headers, + data=json.dumps(slack_blocks), + ) + """ We want to wait for a set time (ALARM_OK_WAIT_SECONDS) to allow the alarm's OK state to stabilise before updating the teams & slack alerts to display OK. This will prevent a situation where an alarm temporarily reaches an OK @@ -386,6 +396,10 @@ def extract_alarm_names_from_arns(self, arn_list: list) -> list: alarm_names.append(match.group(1)) return alarm_names + def extract_topic_name_from_arn(self, arn: str) -> str: + components = arn.split(":") + return components[-1] + def add_ttl_to_alarm_entry(self, alarm_entry: AlarmEntry): alarm_entry.time_to_exist = int( ( @@ -474,7 +488,9 @@ def compose_teams_message(self, alarm_entry: AlarmEntry): def send_initial_slack_alert(self, alarm_entry: AlarmEntry): slack_message = { "channel": alarm_entry.channel_id, - "blocks": self.compose_slack_message_blocks(alarm_entry), + "blocks": self.compose_slack_message_blocks( + alarm_entry=alarm_entry, is_initial_message=True + ), } try: @@ -508,7 +524,9 @@ def send_slack_response(self, alarm_entry: AlarmEntry): slack_message = { "channel": alarm_entry.channel_id, "thread_ts": alarm_entry.slack_timestamp, - "blocks": self.compose_slack_message_blocks(alarm_entry), + "blocks": self.compose_slack_message_blocks( + alarm_entry, is_initial_message=False + ), } try: @@ -532,7 +550,9 @@ def update_original_slack_message(self, alarm_entry: AlarmEntry): slack_message = { "channel": alarm_entry.channel_id, "ts": alarm_entry.slack_timestamp, - "blocks": self.compose_slack_message_blocks(alarm_entry), + "blocks": self.compose_slack_message_blocks( + alarm_entry=alarm_entry, is_initial_message=True + ), } requests.post( @@ -551,7 +571,9 @@ def update_original_slack_message(self, alarm_entry: AlarmEntry): ) def compose_slack_message_blocks( - self, alarm_entry: AlarmEntry, is_initial_message: bool + self, + alarm_entry: AlarmEntry, + is_initial_message: bool, ): with open(f"{os.getcwd()}/models/templates/slack_alert_blocks.json", "r") as f: template_content = f.read() @@ -571,6 +593,36 @@ def compose_slack_message_blocks( rendered_json = template.render(context) return json.loads(rendered_json) + def compose_virus_scanner_slack_blocks(self): + with open( + f"{os.getcwd()}/models/templates/virus_scanner_alert_slack_blocks.json", "r" + ) as f: + template_content = f.read() + + template = Template(template_content) + + topic = self.extract_topic_name_from_arn(self.message["TopicArn"]) + result = self.message["Message"].get("result", "") + + timestamp = self.create_alarm_timestamp( + self.message["Message"].get("dateScanned", "") + ) + scan_date = self.format_time_string(timestamp) + + context = { + "topic": topic, + "scan_result": result, + "scan_date": scan_date, + "severity": f":{AlarmSeverity.HIGH.additional_value}:", + "scan_id": self.message["Message"].get("id", ""), + "action_url": self.create_action_url( + self.confluence_base_url, f"{topic} {result}" + ), + } + + rendered_json = template.render(context) + return json.loads(rendered_json) + # To be used when implementing adding reaction to slack message on dynamo entry deletion def change_reaction(self, alarm_entry: AlarmEntry, action: str): logger.info( diff --git a/lambdas/tests/unit/helpers/data/alerting/mock_virus_scanner_alert.json b/lambdas/tests/unit/helpers/data/alerting/mock_virus_scanner_alert.json new file mode 100644 index 0000000000..71076f8bad --- /dev/null +++ b/lambdas/tests/unit/helpers/data/alerting/mock_virus_scanner_alert.json @@ -0,0 +1,33 @@ +[ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "virus_scanner_topic_arn: Error :red_circle:" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "plain_text", + "text": "Scan date and time: 15:10:41 17-04-2025 UTC" + } + }, + { + "type": "section", + "text": { + "type": "plain_text", + "text": "Scan ID: 1234-4567-8912-HSDF-TEST" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Info:*\n " + } + } +] \ No newline at end of file diff --git a/lambdas/tests/unit/services/test_im_alerting.py b/lambdas/tests/unit/services/test_im_alerting.py index 1dad3facae..0609ef6a66 100644 --- a/lambdas/tests/unit/services/test_im_alerting.py +++ b/lambdas/tests/unit/services/test_im_alerting.py @@ -14,9 +14,11 @@ MOCK_ALERTING_SLACK_CHANNEL_ID, MOCK_CONFLUENCE_URL, MOCK_LG_METADATA_SQS_QUEUE, + MOCK_SLACK_BOT_TOKEN, ) from tests.unit.helpers.data.alerting.mock_sns_alerts import ( MOCK_LAMBDA_ALERT_MESSAGE, + MOCK_VIRUS_SCANNER_ALERT_SNS_MESSAGE, QUEUE_ALERT_MESSAGE, ) @@ -86,6 +88,14 @@ def existing_alarm_alerting_service(alerting_service, mocker): yield alerting_service +@pytest.fixture +def virus_scanner_alerting_service(mocker, set_env): + service = IMAlertingService(MOCK_VIRUS_SCANNER_ALERT_SNS_MESSAGE) + mocker.patch.object(service, "dynamo_service") + mocker.patch.object(service, "send_initial_slack_alert") + yield service + + ALARM_METRIC_NAME = ( f'{QUEUE_ALERT_MESSAGE["Trigger"]["Dimensions"][0]["QueueName"]}' f' {QUEUE_ALERT_MESSAGE["Trigger"]["MetricName"]}' @@ -104,7 +114,7 @@ def existing_alarm_entry(): @freeze_time(ALERT_TIME) -def test_handle_new_alert_happy_path(alerting_service): +def test_handle_new_alarm_alert_happy_path(alerting_service): alerting_service.get_all_alarm_tags.return_value = QUEUE_ALERT_TAGS alerting_service.get_alarm_history.return_value = [] @@ -163,7 +173,7 @@ def test_handle_existing_alarm_entry_happy_path(alerting_service, existing_alarm @freeze_time(ALERT_TIME) -def test_handle_ok_action_happy_path(ok_alerting_service, existing_alarm_entry): +def test_handle_alarm_ok_action_happy_path(ok_alerting_service, existing_alarm_entry): ok_alerting_service.all_alarm_state_ok.return_value = True ok_alerting_service.is_last_updated.return_value = True ok_alerting_service.get_all_alarm_tags.return_value = QUEUE_ALERT_TAGS @@ -206,7 +216,7 @@ def test_handle_ok_action_happy_path(ok_alerting_service, existing_alarm_entry): @freeze_time(ALERT_TIME) -def test_handle_ok_action_not_all_alarms_ok( +def test_handle_alarm_ok_action_not_all_alarms_ok( mocker, ok_alerting_service, existing_alarm_entry ): ok_alerting_service.all_alarm_state_ok.return_value = False @@ -227,7 +237,7 @@ def test_handle_ok_action_not_all_alarms_ok( @freeze_time(ALERT_TIME) -def test_handle_ok_action_not_last_updated( +def test_handle_alarm_ok_action_not_last_updated( mocker, ok_alerting_service, existing_alarm_entry ): ok_alerting_service.all_alarm_state_ok.return_value = True @@ -296,7 +306,7 @@ def test_handle_existing_alarm_history_no_active_alarm_new_episode_created( existing_alarm_alerting_service.handle_new_alarm_episode.assert_called() -def test_handle_existing_alarm_history_ok_action_trigger_alert_ignored( +def test_handle_existing_alarm_history_alarm_ok_action_trigger_alert_ignored( existing_alarm_alerting_service, existing_alarm_entry ): alarm_history = [existing_alarm_entry] @@ -503,6 +513,15 @@ def test_extract_alarm_names_from_arns(alerting_service): assert actual == expected +def test_extract_topic_name_from_arns(alerting_service): + arn = "arn:aws:sns:region:xxxxxx:dev-sns-search_patient_details_alarms-topicxxxxx" + + expected = "dev-sns-search_patient_details_alarms-topicxxxxx" + actual = alerting_service.extract_topic_name_from_arn(arn) + + assert actual == expected + + @freeze_time(ALERT_TIME) def test_is_last_updated(alerting_service, existing_alarm_entry): alerting_service.dynamo_service.get_item.return_value = { @@ -589,3 +608,38 @@ def test_is_episode_expired_TTL_past_returns_false(alerting_service): ) assert alerting_service.is_episode_expired(alarm_entry) is False + + +def test_compose_virus_scanner_slack_blocks(virus_scanner_alerting_service, set_env): + + expected_blocks = read_json( + "../helpers/data/alerting/mock_virus_scanner_alert.json" + ) + + actual_blocks = virus_scanner_alerting_service.compose_virus_scanner_slack_blocks() + + assert actual_blocks == expected_blocks + + +def test_handle_virus_scanner_alert(virus_scanner_alerting_service, mocker): + mock_post = mocker.patch("lambdas.services.im_alerting_service.requests.post") + + expected_blocks = read_json( + "../helpers/data/alerting/mock_virus_scanner_alert.json" + ) + + expected_slack_message = { + "blocks": expected_blocks, + "channel": MOCK_ALERTING_SLACK_CHANNEL_ID, + } + + virus_scanner_alerting_service.handle_virus_scanner_alert() + + mock_post.assert_called_with( + url="https://slack.com/api/chat.postMessage", + headers={ + "Content-type": "application/json; charset=utf-8", + "Authorization": f"Bearer {MOCK_SLACK_BOT_TOKEN}", + }, + data=json.dumps(expected_slack_message), + ) From e79b9ed6141fd6b6b492ee2efde8393c3dd21ebf Mon Sep 17 00:00:00 2001 From: steph-torres-nhs <173282814+steph-torres-nhs@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:13:50 +0100 Subject: [PATCH 8/8] [PRMP-557] restructure how arn is checked from sns notification --- lambdas/handlers/im_alerting_handler.py | 2 +- lambdas/tests/unit/handlers/test_im_alerting_handler.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lambdas/handlers/im_alerting_handler.py b/lambdas/handlers/im_alerting_handler.py index b22d8b017f..0178f60745 100644 --- a/lambdas/handlers/im_alerting_handler.py +++ b/lambdas/handlers/im_alerting_handler.py @@ -35,7 +35,7 @@ def lambda_handler(event, context): message_service = IMAlertingService(message) - if is_virus_scanner_topic(message): + if is_virus_scanner_topic(sns_message["Sns"]): message_service.handle_virus_scanner_alert() return diff --git a/lambdas/tests/unit/handlers/test_im_alerting_handler.py b/lambdas/tests/unit/handlers/test_im_alerting_handler.py index d9c194bfb7..dd16bca396 100644 --- a/lambdas/tests/unit/handlers/test_im_alerting_handler.py +++ b/lambdas/tests/unit/handlers/test_im_alerting_handler.py @@ -43,7 +43,14 @@ def test_handler_calls_handle_virus_scanner_alert_lambda_triggered_by_virus_scan event = { "Records": [ - {"Sns": {"Message": json.dumps(MOCK_VIRUS_SCANNER_ALERT_SNS_MESSAGE)}} + { + "Sns": { + "TopicArn": MOCK_VIRUS_SCANNER_ALERT_SNS_MESSAGE["TopicArn"], + "Message": json.dumps( + MOCK_VIRUS_SCANNER_ALERT_SNS_MESSAGE["Message"] + ), + } + } ] } lambda_handler(event, context)