Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion infrastructure/instance/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ environment = "dev"
immunisation_account_id = "345594581768"
dspp_core_account_id = "603871901111"
pds_environment = "int"
enable_mns_test_queue = true
Comment thread
dlzhry2nhs marked this conversation as resolved.
Outdated
mns_environment = "int"
error_alarm_notifications_enabled = true
create_mesh_processor = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ environment = "dev"
immunisation_account_id = "345594581768"
dspp_core_account_id = "603871901111"
pds_environment = "int"
enable_mns_test_queue = true
mns_environment = "int"
error_alarm_notifications_enabled = false
mns_publisher_feature_enabled = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ environment = "dev"
immunisation_account_id = "345594581768"
dspp_core_account_id = "603871901111"
pds_environment = "int"
enable_mns_test_queue = true
mns_environment = "int"
error_alarm_notifications_enabled = false
mns_publisher_feature_enabled = true # Switch this off once tested fully e2e in Lambda branch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ environment = "dev"
immunisation_account_id = "345594581768"
dspp_core_account_id = "603871901111"
pds_environment = "ref"
enable_mns_test_queue = true
mns_environment = "int"
error_alarm_notifications_enabled = true
create_mesh_processor = false
Expand Down
2 changes: 2 additions & 0 deletions infrastructure/instance/mns_publisher.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ module "mns_publisher" {
imms_base_path = strcontains(var.sub_environment, "pr-") ? "immunisation-fhir-api/FHIR/R4-${var.sub_environment}" : "immunisation-fhir-api/FHIR/R4"
lambda_kms_encryption_key_arn = data.aws_kms_key.existing_lambda_encryption_key.arn
mns_publisher_resource_name_prefix = "${local.resource_scope}-mns-outbound-events"
mns_test_notification_name_prefix = "${local.resource_scope}-mns-test-notification"
secrets_manager_policy_path = "${local.policy_path}/secret_manager.json"
account_id = data.aws_caller_identity.current.account_id
enable_mns_test_queue = var.enable_mns_test_queue
Comment thread
dlzhry2nhs marked this conversation as resolved.
Outdated
pds_environment = var.pds_environment
mns_environment = var.mns_environment

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ resource "aws_lambda_function" "mns_publisher_lambda" {
environment {
variables = {
SPLUNK_FIREHOSE_NAME = var.splunk_firehose_stream_name
MNS_TEST_QUEUE_URL = aws_sqs_queue.mns_test_notification[0].url
Comment thread
dlzhry2nhs marked this conversation as resolved.
Outdated
IMMUNIZATION_ENV = var.resource_scope,
IMMUNIZATION_BASE_PATH = var.imms_base_path
PDS_ENV = var.pds_environment
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
resource "aws_sqs_queue" "mns_test_notification" {
Comment thread
dlzhry2nhs marked this conversation as resolved.
count = var.enable_mns_test_queue ? 1 : 0
Comment thread
Akol125 marked this conversation as resolved.
name = "${var.mns_test_notification_name_prefix}-queue"
fifo_queue = false
message_retention_seconds = 86400
visibility_timeout_seconds = 300
}


data "aws_iam_policy_document" "mns_test_notification_sqs_policy" {
statement {
sid = "mns-test-notification-allow-lambda-access"
effect = "Allow"

principals {
type = "AWS"
identifiers = [aws_iam_role.mns_publisher_lambda_exec_role.arn]
Comment thread
dlzhry2nhs marked this conversation as resolved.
}

actions = [
"sqs:SendMessage",
]

resources = [
aws_sqs_queue.mns_test_notification[0].arn
Comment thread
dlzhry2nhs marked this conversation as resolved.
]
}
}

resource "aws_sqs_queue_policy" "mns_test_notification_sqs" {
queue_url = aws_sqs_queue.mns_test_notification[0].id
policy = data.aws_iam_policy_document.mns_test_notification_sqs_policy.json
}

output "mns_test_queue_url" {
value = aws_sqs_queue.mns_test_notification[0].url
description = "URL of the MNS test notifications queue"
}

output "mns_test_queue_arn" {
value = aws_sqs_queue.mns_test_notification[0].arn
description = "ARN of the MNS test notifications queue"
}

11 changes: 11 additions & 0 deletions infrastructure/instance/modules/mns_publisher/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,15 @@ variable "account_id" {
variable "secrets_manager_policy_path" {
type = string
description = "Path to the IAM policy JSON template for Secrets Manager access (e.g., ./policies/secret_manager.json)."
}

variable "mns_test_notification_name_prefix" {
type = string
description = "The prefix for the name of resources for testing mns notification"
}

variable "enable_mns_test_queue" {
description = "Enable test SQS queue for MNS notifications (dev only)"
type = bool
default = false
}
12 changes: 12 additions & 0 deletions infrastructure/instance/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,15 @@ output "id_sync_queue_arn" {
description = "The ARN of the ID Sync (MNS NHS Number change) SQS queue"
value = aws_sqs_queue.id_sync_queue.arn
}

# TODO: Remove when MNS platform authorizes imms-vaccinations-1 event type
Comment thread
dlzhry2nhs marked this conversation as resolved.
Outdated
# Temporary SQS queue for testing MNS notifications until MNS HTTP endpoint is available
output "mns_test_queue_url" {
value = var.mns_publisher_feature_enabled ? module.mns_publisher[0].mns_test_queue_url : null
description = "URL of the MNS test notifications queue (from mns_publisher module)"
}

output "mns_test_queue_arn" {
value = var.mns_publisher_feature_enabled ? module.mns_publisher[0].mns_test_queue_arn : null
description = "ARN of the MNS test notifications queue (from mns_publisher module)"
}
6 changes: 6 additions & 0 deletions infrastructure/instance/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ variable "mns_publisher_feature_enabled" {
type = bool
}

variable "enable_mns_test_queue" {
description = "Enable test SQS queue for MNS notifications (dev only)"
type = bool
default = false
}

variable "has_sub_environment_scope" {
description = "True if the sub-environment is a standalone environment, e.g. internal-dev. False if it is part of a blue-green split, e.g. int-green."
type = bool
Expand Down
6 changes: 3 additions & 3 deletions lambdas/mns_publisher/src/process_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

from common.api_clients.mns_service import MnsService
from common.api_clients.mns_setup import get_mns_service
from common.api_clients.mock_mns_service import MockMnsService
from common.clients import logger
from create_notification import create_mns_notification

mns_env = os.getenv("MNS_ENV", "int")
MNS_TEST_QUEUE_URL = os.getenv("MNS_TEST_QUEUE_URL")


def process_records(records: list[SQSMessage]) -> dict[str, list]:
Expand Down Expand Up @@ -37,7 +39,7 @@ def process_records(records: list[SQSMessage]) -> dict[str, list]:
return {"batchItemFailures": batch_item_failures}


def process_record(record: SQSMessage, mns_service: MnsService) -> None:
def process_record(record: SQSMessage, mns_service: MnsService | MockMnsService) -> None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially even better if we implement an abstract class, or vanilla Python base class that defines the interface that includes a publish_notification(self, notification: MnsPayloadBla) -> None in the contract. Optional though, this is good enough for now...maybe.

"""
Process a single SQS record.
Args:
Expand Down Expand Up @@ -65,8 +67,6 @@ def process_record(record: SQSMessage, mns_service: MnsService) -> None:
mns_service.publish_notification(mns_notification_payload)
logger.info("Successfully created MNS notification", extra={"mns_notification_id": notification_id})

return None


def extract_trace_ids(record: SQSMessage) -> Tuple[str, str | None]:
"""
Expand Down
30 changes: 18 additions & 12 deletions lambdas/shared/src/common/api_clients/mns_setup.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import logging
import os

import boto3
from botocore.config import Config

from common.api_clients.authentication import AppRestrictedAuth, Service
from common.api_clients.mns_service import MnsService
from common.api_clients.mock_mns_service import MockMnsService
from common.cache import Cache

logging.basicConfig(level=logging.INFO)
MNS_TEST_QUEUE_URL = os.getenv("MNS_TEST_QUEUE_URL")


def get_mns_service(mns_env: str = "int"):
boto_config = Config(region_name="eu-west-2")
cache = Cache(directory="/tmp") # NOSONAR(S5443)
logging.info("Creating authenticator...")
authenticator = AppRestrictedAuth(
service=Service.PDS,
secret_manager_client=boto3.client("secretsmanager", config=boto_config),
environment=mns_env,
cache=cache,
)

logging.info("Authentication Initiated...")
return MnsService(authenticator)
if mns_env == "dev":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For flexibility, might it be better to have pass in a flag for use_mock? Could easily be derived from an env variable.

logging.info("Dev environment: Using MockMnsService")
return MockMnsService(MNS_TEST_QUEUE_URL)
else:
boto_config = Config(region_name="eu-west-2")
cache = Cache(directory="/tmp") # NOSONAR(S5443)
logging.info("Creating authenticator...")
authenticator = AppRestrictedAuth(
service=Service.PDS,
secret_manager_client=boto3.client("secretsmanager", config=boto_config),
environment=mns_env,
cache=cache,
)
logging.info("Authentication Initiated...")
return MnsService(authenticator)
36 changes: 36 additions & 0 deletions lambdas/shared/src/common/api_clients/mock_mns_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import json
import os

import boto3

from common.clients import logger

REGION_NAME = os.getenv("AWS_REGION", "eu-west-2")


class MockMnsService:
def __init__(self, queue_url):
self.queue_url = queue_url
self.sqs_client = self._get_sqs_client()
logger.info(f"MockMnsService initialized with queue: {queue_url}")

def _get_sqs_client(self):
Comment thread
dlzhry2nhs marked this conversation as resolved.
Outdated
return boto3.client("sqs", region_name=REGION_NAME)

def publish_notification(self, mns_payload: dict) -> None:
Comment thread
dlzhry2nhs marked this conversation as resolved.
Outdated
"""
Send MNS notification payload to test SQS queue as fallback.
Args: payload: MNS notification payload
"""
try:
response = self.sqs_client.send_message(
QueueUrl=self.queue_url,
MessageBody=json.dumps(mns_payload),
MessageAttributes={"source": {"StringValue": "mns-publisher-lambda", "DataType": "String"}},
)
logger.info(
"Mock MNS: Successfully sent notification to test queue", extra={"message_id": response["MessageId"]}
)
except Exception:
logger.exception("Mock MNS: Failed to send to test SQS queue")
raise
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import json
import unittest

import boto3
from moto import mock_aws

from common.api_clients.mock_mns_service import MockMnsService


@mock_aws
class TestMockMnsService(unittest.TestCase):
"""Tests for MockMnsService (dev environment)."""

def setUp(self):
"""Set up mocked SQS queue and test payload."""
# Create mock SQS queue
self.sqs = boto3.client("sqs", region_name="eu-west-2")
response = self.sqs.create_queue(QueueName="mns-test-notifications-dev")
self.queue_url = response["QueueUrl"]

self.mns_payload = {
"specversion": "1.0",
"id": "236a1d4a-5d69-4fa9-9c7f-e72bf505aa5b",
"source": "https://int.api.service.nhs.uk/immunisation-fhir-api",
"type": "imms-vaccinations-2",
"time": "20260212T174437+00:00",
"subject": "9481152782",
"dataref": "https://int.api.service.nhs.uk/immunisation-fhir-api/Immunization/d058014c-b0fd-4471-8db9-3316175eb825",
"filtering": {
"generalpractitioner": "Y12345",
"sourceorganisation": "B0C4P",
"sourceapplication": "TPP",
"subjectage": 21,
"immunisationtype": "HIB",
"action": "CREATE",
},
}

def test_publish_notification_success(self):
"""Test MockMnsService successfully publishes to SQS queue."""
# Create mock service with queue URL
mock_service = MockMnsService(queue_url=self.queue_url)

# Publish notification
mock_service.publish_notification(self.mns_payload)

# Verify message was sent to queue
messages = self.sqs.receive_message(
QueueUrl=self.queue_url, MaxNumberOfMessages=1, MessageAttributeNames=["All"]
)

# Assert message exists
self.assertIn("Messages", messages)
self.assertEqual(len(messages["Messages"]), 1)

# Verify message body
message_body = json.loads(messages["Messages"][0]["Body"])
self.assertEqual(message_body["id"], "236a1d4a-5d69-4fa9-9c7f-e72bf505aa5b")
self.assertEqual(message_body["subject"], "9481152782")
self.assertEqual(message_body["filtering"]["generalpractitioner"], "Y12345")
self.assertEqual(message_body["filtering"]["sourceorganisation"], "B0C4P")
self.assertEqual(message_body["filtering"]["sourceapplication"], "TPP")
self.assertEqual(message_body["filtering"]["immunisationtype"], "HIB")
self.assertEqual(message_body["filtering"]["action"], "CREATE")
self.assertEqual(message_body["filtering"]["subjectage"], 21)

# Verify message attributes
attributes = messages["Messages"][0]["MessageAttributes"]
self.assertEqual(attributes["source"]["StringValue"], "mns-publisher-lambda")

def test_publish_notification_multiple_messages(self):
"""Test MockMnsService handles multiple publications."""
mock_service = MockMnsService(queue_url=self.queue_url)

# Publish multiple notifications
payload1 = {**self.mns_payload, "id": "notification-1"}
payload2 = {**self.mns_payload, "id": "notification-2"}

mock_service.publish_notification(payload1)
mock_service.publish_notification(payload2)

# Verify both messages in queue
messages = self.sqs.receive_message(QueueUrl=self.queue_url, MaxNumberOfMessages=10)

self.assertEqual(len(messages["Messages"]), 2)

message_ids = [json.loads(msg["Body"])["id"] for msg in messages["Messages"]]
self.assertIn("notification-1", message_ids)
self.assertIn("notification-2", message_ids)

def test_publish_notification_sqs_failure(self):
"""Test MockMnsService raises exception on SQS failure."""
# Use invalid queue URL
mock_service = MockMnsService(queue_url="queue_url=invalid_queue_url")
with self.assertRaises(Exception):
mock_service.publish_notification(self.mns_payload)
Loading