Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
116 commits
Select commit Hold shift + click to select a range
2562437
CCM-12616: Added py-mock-mesh module
gareth-allan Nov 5, 2025
0da3abd
CCM-12616: Replaced mesh-poll Lambda with a skeleton Python version
gareth-allan Nov 5, 2025
b141b21
CCM-12616: test
lapenna-bjss Nov 11, 2025
9876b1d
CCM-12616: add false positive to .gitleaksignore
lapenna-bjss Nov 11, 2025
331d6c4
CCM-12616: fix sonarcloud issues
lapenna-bjss Nov 12, 2025
2ed8cf1
CCM-12616: fix sonarcloud issues
lapenna-bjss Nov 12, 2025
73fbd53
CCM-12616: Exclude mesh poll lambda from SonarCloud coverage temporarily
lapenna-bjss Nov 12, 2025
ce9e182
CCM-12616: add poetry install to pre.sh
lapenna-bjss Nov 13, 2025
cc83432
Merge branch 'main' into feature/CCM-12616_mesh_poll_retrieve
lapenna-bjss Nov 17, 2025
e02647d
CCM-12616: update .gitignore
lapenna-bjss Nov 18, 2025
dc0f7cf
Merge branch 'main' into feature/CCM-12616_mesh_poll_retrieve
lapenna-bjss Nov 18, 2025
19b16de
CCM-12616: mesh-poll lambda
lapenna-bjss Nov 20, 2025
29fa28c
Merge branch 'main' into feature/CCM-12616_mesh_poll_retrieve
lapenna-bjss Nov 20, 2025
092fd3f
CCM-12616: terraform code
lapenna-bjss Nov 21, 2025
94a2f08
CCM-12616: metric publisher
lapenna-bjss Nov 21, 2025
8631401
CCM-12616: mock mesh only when enabled
lapenna-bjss Nov 26, 2025
bc10540
CCM-12616: add senderId to CloudEvent data
lapenna-bjss Nov 26, 2025
d1c9cd7
CCM-12616: mesh-download lambda
lapenna-bjss Nov 27, 2025
51181ca
CCM-12616: add document_store tests
lapenna-bjss Nov 27, 2025
c045cc1
CCM-12616: update mesh download tests
lapenna-bjss Nov 28, 2025
0e51120
CCM-12616: add S3 putObject statement for storing messages when mock …
lapenna-bjss Nov 28, 2025
1054505
CCM-12616: add messageReference to the event data
lapenna-bjss Nov 28, 2025
d6dc913
CCM-12616: rename client to sender
lapenna-bjss Nov 28, 2025
f068287
CCM-12616: fix sonar issue
lapenna-bjss Nov 28, 2025
3059592
CCM-12616: move docs dependencies to a separate make command
lapenna-bjss Nov 28, 2025
b43fed5
Merge branch 'main' into feature/CCM-12616_mesh_poll_retrieve
lapenna-bjss Nov 28, 2025
0624e52
CCM-12616: update python runtime
lapenna-bjss Dec 1, 2025
92ce87f
CCM-12616: update ssm prefixes with the correct paths
lapenna-bjss Dec 1, 2025
904756a
CCM-12616: add shared MESH config
lapenna-bjss Dec 1, 2025
e024971
CCM-12616: update pipeline
lapenna-bjss Dec 2, 2025
9a401da
CCM-12616: update config and tests
lapenna-bjss Dec 2, 2025
7691746
CCM-12616: resolve comments
lapenna-bjss Dec 3, 2025
0498402
CCM-12616: resolve comments
lapenna-bjss Dec 5, 2025
b0cf0c8
CCM-12616: fix pipeline issues
lapenna-bjss Dec 5, 2025
360cdfb
CCM-12616: fix pipeline issues
lapenna-bjss Dec 5, 2025
3589952
CCM-12616: test
lapenna-bjss Dec 5, 2025
ae8b458
CCM-12616: test
lapenna-bjss Dec 5, 2025
4fd7343
CCM-12616: test
lapenna-bjss Dec 5, 2025
a912dae
CCM-12616: test
lapenna-bjss Dec 8, 2025
0ace208
CCM-12616: test
lapenna-bjss Dec 8, 2025
36b94ec
CCM-12616: revert changes
lapenna-bjss Dec 8, 2025
ff2e5b7
CCM-12616: test
lapenna-bjss Dec 8, 2025
eae4795
CCM-12616: test
lapenna-bjss Dec 8, 2025
e07f431
CCM-12616: test
lapenna-bjss Dec 8, 2025
1ed06b2
CCM-12616: fix sonarcloud coverage issues
lapenna-bjss Dec 8, 2025
aa80734
CCM-12616: fix sonarcloud coverage issues
lapenna-bjss Dec 8, 2025
e8d06a0
CCM-12616: convert poetry to pip
lapenna-bjss Dec 9, 2025
62035fb
CCM-12616: test
lapenna-bjss Dec 9, 2025
8baf979
CCM-12616: test
lapenna-bjss Dec 9, 2025
21e3c2e
CCM-12616: test
lapenna-bjss Dec 9, 2025
d312464
CCM-12616: add setup.py files
lapenna-bjss Dec 11, 2025
abcee60
CCM-12616: update coverage.xml paths
lapenna-bjss Dec 11, 2025
5f559b8
CCM-12616: update coverage.xml paths
lapenna-bjss Dec 11, 2025
9000b50
CCM-12616: update sonar-scanner.properties
lapenna-bjss Dec 11, 2025
b79e3d4
CCM-12616: update sonar-scanner.properties
lapenna-bjss Dec 11, 2025
9c8741c
CCM-12616: remove poetry from pre.sh
lapenna-bjss Dec 11, 2025
4656623
CCM-12616: fix Unsupported attribute error
lapenna-bjss Dec 11, 2025
f2061ab
CCM-12616: fix lambda env vars
lapenna-bjss Dec 11, 2025
8a0b984
Merge branch 'main' into feature/CCM-12616_mesh_poll_retrieve
lapenna-bjss Dec 11, 2025
590d3df
CCM-12616: skip build docs step
lapenna-bjss Dec 11, 2025
ff76bbc
CCM-12616: test deployment
lapenna-bjss Dec 15, 2025
bb0454e
CCM-12616: test deployment
lapenna-bjss Dec 15, 2025
367c5e9
CCM-12616: Restore previously disabled pipeline steps
lapenna-bjss Dec 16, 2025
e3cfa1d
CCM-12616: Update Readme
lapenna-bjss Dec 17, 2025
bd975fb
CCM-12616: Update Makefile
lapenna-bjss Dec 17, 2025
73278ca
CCM-12616: Update sonar properties
lapenna-bjss Dec 17, 2025
ca56d6e
Merge branch 'main' into feature/CCM-12616_mesh_poll_retrieve
lapenna-bjss Dec 17, 2025
295dfe8
Merge branch 'main' into feature/CCM-12616_mesh_poll_retrieve
gareth-allan Dec 18, 2025
e2a1485
CCM-12616: Remove mesh-poll lambda from workspace Jest folders
gareth-allan Dec 18, 2025
5b7ea7a
CCM-12616: Update events published to match latest schemas
gareth-allan Dec 18, 2025
b066e55
CCM-12616: Updated base CloudEvent model to match latest schemas
gareth-allan Dec 18, 2025
c7076d1
CCM-12616: Attempt to fix mesh-poll lambda's packaging
gareth-allan Dec 19, 2025
ebfb170
CCM-12616: Attempt to fix mesh-poll lambda entrypoint
gareth-allan Dec 19, 2025
02e1566
CCM-12616: Another mesh-poll packaging tweak
gareth-allan Dec 19, 2025
242e7c0
CCM-12616: Fix mesh-poll SSM parameters
gareth-allan Dec 19, 2025
a39f0f7
CCM-12616: Fix import module error
lapenna-bjss Dec 29, 2025
ee8fc7d
Merge branch 'feature/CCM-12616_mesh_poll_retrieve' of https://github…
lapenna-bjss Dec 29, 2025
7bcdbb6
CCM-12616: Update runtime version
lapenna-bjss Dec 31, 2025
fe52cd2
CCM-12616: Test
lapenna-bjss Dec 31, 2025
309c771
CCM-12616: Test
lapenna-bjss Jan 2, 2026
e666aca
CCM-12616: Don't zip Python lambda output when packaging
gareth-allan Jan 5, 2026
d991073
CCM-12616: Allow mesh_poll lambda to call ssm:GetParameter
gareth-allan Jan 5, 2026
6dbd587
CCM-12616: Fix SSM parameter format in mesh_poll IAM policy
gareth-allan Jan 5, 2026
e317569
CCM-12616: Update mesh download package script
lapenna-bjss Jan 5, 2026
0ec4b00
CCM-12616: Add setup file to mesh download
lapenna-bjss Jan 6, 2026
9a3afe3
CCM-12616: Fix mesh download tests
lapenna-bjss Jan 6, 2026
e26a815
CCM-12616: Add SSM statement to mesh download
lapenna-bjss Jan 6, 2026
f0eebf1
CCM-12616: Add MESH SSM parameter configuration
lapenna-bjss Jan 7, 2026
857a9b8
CCM-12616: Add false positive to .gitleaksignore
lapenna-bjss Jan 7, 2026
9c6663b
CCM-12616: Increase mesh poll timeout
lapenna-bjss Jan 8, 2026
da8ad2c
CCM-12616: Update mesh poll S3 terraform statement
lapenna-bjss Jan 8, 2026
cf5b80e
CCM-12616: Fix pydantic model
lapenna-bjss Jan 9, 2026
cb465f3
Merge branch 'main' into feature/CCM-12616_mesh_poll_retrieve
lapenna-bjss Jan 12, 2026
662ee02
CCM-12616: Update the Eventbridge rule
lapenna-bjss Jan 12, 2026
8e68276
CCM-12616: Update package-lock file
lapenna-bjss Jan 12, 2026
779ba5b
CCM-12616: Fix linting error
lapenna-bjss Jan 12, 2026
0941047
Merge branch 'main' into feature/CCM-12616_mesh_poll_retrieve
lapenna-bjss Jan 12, 2026
4b55ec4
CCM-12616: Fix linting error
lapenna-bjss Jan 12, 2026
dc9058b
CCM-12616: Add a retrieve_message method
lapenna-bjss Jan 12, 2026
80e2506
CCM-12616: 100
lapenna-bjss Jan 12, 2026
4bc5589
CCM-12616: Component tests
lapenna-bjss Jan 13, 2026
7bd4a78
CCM-12616: Fix linting errors
lapenna-bjss Jan 13, 2026
d43848e
CCM-12616: Update tests
lapenna-bjss Jan 14, 2026
68847d3
CCM-12616: Update ssm prefix path
lapenna-bjss Jan 14, 2026
832f32e
Merge branch 'feature/CCM-12616_mesh_poll_retrieve' into feature/CCM-…
lapenna-bjss Jan 14, 2026
790876a
Merge branch 'main' into feature/CCM-12616_mesh_poll_retrieve_compone…
simonlabarere Jan 19, 2026
5b224e4
CCM-12616: Split MESH/Senders config
simonlabarere Jan 20, 2026
c137d99
CCM-12616: Split MESH/Senders config
simonlabarere Jan 20, 2026
ec89ccd
CCM-12616: Split MESH/Senders config
simonlabarere Jan 20, 2026
bf613ce
CCM-12616: Split MESH/Senders config
simonlabarere Jan 20, 2026
709cab7
CCM-12616: Adjust component test
simonlabarere Jan 20, 2026
44f3682
CCM-12616: Adjust component test
simonlabarere Jan 20, 2026
99ceb79
CCM-12616: Remove mesh cert expiry metric from MESH Download
simonlabarere Jan 21, 2026
c78c8ae
CCM-12616: Fix event subject + fix component tests
simonlabarere Jan 21, 2026
775291f
CCM-12616: More test fixing
simonlabarere Jan 21, 2026
3f47a03
Merge branch 'main' into feature/CCM-12616_mesh_poll_retrieve_compone…
simonlabarere Jan 21, 2026
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
1 change: 1 addition & 0 deletions infrastructure/terraform/components/dl/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ locals {
apim_private_key_ssm_parameter_name = "/${var.component}/${var.environment}/apim/private_key"
apim_keystore_s3_bucket = "nhs-${var.aws_account_id}-${var.region}-${var.environment}-${var.component}-static-assets"
ssm_mesh_prefix = "/${var.component}/${var.environment}/mesh"
ssm_senders_prefix = "/${var.component}/${var.environment}/senders"
mock_mesh_endpoint = "s3://${module.s3bucket_non_pii_data.bucket}/mock-mesh"
root_domain_name = "${var.environment}.${local.acct.route53_zone_names["digital-letters"]}"
root_domain_id = local.acct.route53_zone_ids["digital-letters"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,14 @@ module "mesh_download" {
log_subscription_role_arn = local.acct.log_subscription_role_arn

lambda_env_vars = {
CERTIFICATE_EXPIRY_METRIC_NAME = "mesh-download-client-certificate-near-expiry"
CERTIFICATE_EXPIRY_METRIC_NAMESPACE = "dl-mesh-download"
DOWNLOAD_METRIC_NAME = "mesh-download-successful-downloads"
DOWNLOAD_METRIC_NAMESPACE = "dl-mesh-download"
ENVIRONMENT = var.environment
EVENT_PUBLISHER_DLQ_URL = module.sqs_event_publisher_errors.sqs_queue_url
EVENT_PUBLISHER_EVENT_BUS_ARN = aws_cloudwatch_event_bus.main.arn
PII_BUCKET = module.s3bucket_pii_data.bucket
SSM_PREFIX = "${local.ssm_mesh_prefix}"
SSM_MESH_PREFIX = "${local.ssm_mesh_prefix}"
SSM_SENDERS_PREFIX = "${local.ssm_senders_prefix}"
USE_MESH_MOCK = var.enable_mock_mesh ? "true" : "false"
}

Expand Down Expand Up @@ -172,7 +171,8 @@ data "aws_iam_policy_document" "mesh_download_lambda" {
]

resources = [
"arn:aws:ssm:${var.region}:${var.aws_account_id}:parameter${local.ssm_mesh_prefix}/*"
"arn:aws:ssm:${var.region}:${var.aws_account_id}:parameter${local.ssm_mesh_prefix}/*",
"arn:aws:ssm:${var.region}:${var.aws_account_id}:parameter${local.ssm_senders_prefix}/*"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ module "mesh_poll" {
MAXIMUM_RUNTIME_MILLISECONDS = "240000" # 4 minutes (Lambda has 5 min timeout)
POLLING_METRIC_NAME = "mesh-poll-successful-polls"
POLLING_METRIC_NAMESPACE = "dl-mesh-poll"
SSM_PREFIX = "${local.ssm_mesh_prefix}"
SSM_MESH_PREFIX = "${local.ssm_mesh_prefix}"
SSM_SENDERS_PREFIX = "${local.ssm_senders_prefix}"
USE_MESH_MOCK = var.enable_mock_mesh ? "true" : "false"
}

Expand Down Expand Up @@ -144,7 +145,8 @@ data "aws_iam_policy_document" "mesh_poll_lambda" {
]

resources = [
"arn:aws:ssm:${var.region}:${var.aws_account_id}:parameter${local.ssm_mesh_prefix}/*"
"arn:aws:ssm:${var.region}:${var.aws_account_id}:parameter${local.ssm_mesh_prefix}/*",
"arn:aws:ssm:${var.region}:${var.aws_account_id}:parameter${local.ssm_senders_prefix}/*"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from unittest.mock import Mock, patch
from datetime import datetime, timezone
from pydantic import ValidationError
from mesh_download.errors import MeshMessageNotFound


def setup_mocks():
Expand Down Expand Up @@ -238,9 +239,11 @@ def test_download_and_store_message_not_found(self):

config.mesh_client.retrieve_message.return_value = None
sqs_record = create_sqs_record()
processor.process_sqs_message(sqs_record)
config.mesh_client.retrieve_message.assert_called_once_with('test_message_123')

with pytest.raises(MeshMessageNotFound, match="MESH message with ID test_message_123 not found"):
processor.process_sqs_message(sqs_record)

config.mesh_client.retrieve_message.assert_called_once_with('test_message_123')
document_store.store_document.assert_not_called()
event_publisher.send_events.assert_not_called()
config.download_metric.record.assert_not_called()
Expand Down
5 changes: 2 additions & 3 deletions lambdas/mesh-download/mesh_download/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@


_REQUIRED_ENV_VAR_MAP = {
"ssm_prefix": "SSM_PREFIX",
"ssm_senders_prefix": "SSM_SENDERS_PREFIX",
"ssm_mesh_prefix": "SSM_MESH_PREFIX",
"environment": "ENVIRONMENT",
"certificate_expiry_metric_name": "CERTIFICATE_EXPIRY_METRIC_NAME",
"certificate_expiry_metric_namespace": "CERTIFICATE_EXPIRY_METRIC_NAMESPACE",
"download_metric_name": "DOWNLOAD_METRIC_NAME",
"download_metric_namespace": "DOWNLOAD_METRIC_NAMESPACE",
"event_publisher_event_bus_arn": "EVENT_PUBLISHER_EVENT_BUS_ARN",
Expand Down
5 changes: 5 additions & 0 deletions lambdas/mesh-download/mesh_download/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ def format_exception(exception):
"""
return ''.join(traceback.format_exception(
type(exception), exception, exception.__traceback__))

class MeshMessageNotFound(Exception):
"""
Indicates an invalid MESH message could not be retrieved
"""
3 changes: 2 additions & 1 deletion lambdas/mesh-download/mesh_download/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pydantic import ValidationError
from event_publisher.models import MeshInboxMessageEvent, MeshDownloadMessageEvent
from mesh_download.errors import MeshMessageNotFound


class MeshDownloadProcessor:
Expand Down Expand Up @@ -57,7 +58,7 @@ def _handle_download(self, event, logger):
message = self.__mesh_client.retrieve_message(data.meshMessageId)
if not message:
logger.error("Message not found in MESH inbox")
return
raise MeshMessageNotFound(f"MESH message with ID {data.meshMessageId} not found")

logger.info(
"Retrieved MESH message",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ def setup_mocks():
ssm = Mock()

config = Mock()
config.ssm_prefix = "/dl/test"
config.ssm_mesh_prefix = "/dl/test/mesh"
config.ssm_senders_prefix = "/dl/test/senders"

logger = Mock()

Expand Down
3 changes: 2 additions & 1 deletion lambdas/mesh-poll/mesh_poll/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@


_REQUIRED_ENV_VAR_MAP = {
"ssm_prefix": "SSM_PREFIX",
"ssm_senders_prefix": "SSM_SENDERS_PREFIX",
"ssm_mesh_prefix": "SSM_MESH_PREFIX",
"maximum_runtime_milliseconds": "MAXIMUM_RUNTIME_MILLISECONDS",
"environment": "ENVIRONMENT",
"event_bus_arn": "EVENT_PUBLISHER_EVENT_BUS_ARN",
Expand Down
3 changes: 2 additions & 1 deletion lambdas/mesh-poll/mesh_poll/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,15 @@ def _publish_mesh_inbox_message_received_event(self, event_detail):
'id': str(uuid4()),
'specversion': '1.0',
'source': self.__cloud_event_source,
'subject': 'customer/00000000-0000-0000-0000-000000000000/recipient/00000000-0000-0000-0000-000000000000',
'subject': f'customer/{event_detail["data"]["senderId"]}/recipient/{event_detail["data"]["messageReference"]}',
'type': 'uk.nhs.notify.digital.letters.mesh.inbox.message.received.v1',
'time': now,
'recordedtime': now,
'severitynumber': 2,
'severitytext': 'INFO',
'traceparent': '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
'dataschema': 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-mesh-inbox-message-received-data.schema.json',
'datacontenttype': 'application/json',
'data': event_detail.get('data', {}),
}

Expand Down
2 changes: 1 addition & 1 deletion lambdas/mesh-poll/mesh_poll/sender_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def __get_page(self, next_token=""):
"""
Loads a page of sender data and extracts mailbox IDs and sender IDs
"""
senders_path = f"{self.__config.ssm_prefix.rstrip('/')}/senders/"
senders_path = f"{self.__config.ssm_senders_prefix.rstrip('/')}/"

if len(next_token) == 0:
response = self.__ssm.get_parameters_by_path(
Expand Down
3 changes: 3 additions & 0 deletions tests/playwright/constants/backend-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const CORE_NOTIFIER_LAMBDA_NAME = `${CSI}-core-notifier`;
export const TTL_QUEUE_NAME = `${CSI}-ttl-queue`;
export const TTL_DLQ_NAME = `${CSI}-ttl-dlq`;
export const PDM_UPLOADER_DLQ_NAME = `${CSI}-pdm-uploader-dlq`;
export const MESH_DOWNLOAD_DLQ_NAME = `${CSI}-mesh-download-dlq`;
export const PDM_POLL_DLQ_NAME = `${CSI}-pdm-poll-dlq`;
export const CORE_NOTIFIER_DLQ_NAME = `${CSI}-core-notifier-dlq`;

Expand All @@ -33,6 +34,8 @@ export const TTL_TABLE_NAME = `${CSI}-ttl`;

// S3
export const LETTERS_S3_BUCKET_NAME = `nhs-${process.env.AWS_ACCOUNT_ID}-${REGION}-${ENV}-dl-letters`;
export const NON_PII_S3_BUCKET_NAME = `nhs-${process.env.AWS_ACCOUNT_ID}-${REGION}-${ENV}-dl-non-pii-data`;
export const PII_S3_BUCKET_NAME = `nhs-${process.env.AWS_ACCOUNT_ID}-${REGION}-${ENV}-dl-pii-data`;

// Cloudwatch
export const PDM_UPLOADER_LAMBDA_LOG_GROUP_NAME = `/aws/lambda/${CSI}-pdm-uploader`;
Expand Down
6 changes: 3 additions & 3 deletions tests/playwright/constants/tests-constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// senderIds
export const SENDER_ID_VALID_FOR_NOTIFY_SANDBOX =
'componentTestSender_RoutingConfig';
'2b8ebb33-8b33-49bd-949e-c12e22d25320';
export const SENDER_ID_THAT_TRIGGERS_ERROR_IN_NOTIFY_SANDBOX =
'componentTestSender_RoutingConfigInvalid';
export const SENDER_ID_SKIPS_NOTIFY = 'test-sender-1';
'f017669b-6da4-4576-9d59-3d2b7f005ae2';
export const SENDER_ID_SKIPS_NOTIFY = '67403568-166e-41d0-900a-1f31fe93a091';
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { expect, test } from '@playwright/test';
import {
ENV,
MESH_DOWNLOAD_DLQ_NAME,
MESH_POLL_LAMBDA_NAME,
NON_PII_S3_BUCKET_NAME,
PII_S3_BUCKET_NAME,
} from 'constants/backend-constants';
import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers';
import eventPublisher from 'helpers/event-bus-helpers';
import expectToPassEventually from 'helpers/expectations';
import { invokeLambda } from 'helpers/lambda-helpers';
import { downloadFromS3, uploadToS3 } from 'helpers/s3-helpers';
import { expectMessageContainingString } from 'helpers/sqs-helpers';
import { v4 as uuidv4 } from 'uuid';
import messageMessageReceived from 'digital-letters-events/MESHInboxMessageReceived.js';
import { SENDER_ID_SKIPS_NOTIFY } from 'constants/tests-constants';

test.describe('Digital Letters - MESH Poll and Download', () => {
const senderId = SENDER_ID_SKIPS_NOTIFY;
const sendersMeshMailboxId = 'test-mesh-sender-1';
const meshMailboxId = 'mock-mailbox';

async function uploadMeshMessage(
meshMessageId: string,
messageReference: string,
messageContent: string,
metadata: Record<string, string> = {},
): Promise<void> {
const key = `mock-mesh/${meshMailboxId}/in/${meshMessageId}`;
const meshMetadata = {
sender: sendersMeshMailboxId,
subject: '201',
workflow_id: 'NHS_NOTIFY_SEND_REQUEST',
local_id: messageReference,
...metadata,
};

await uploadToS3(messageContent, NON_PII_S3_BUCKET_NAME, key, meshMetadata);
}

async function expectMeshInboxMessageReceivedEvent(
meshMessageId: string,
): Promise<void> {
await expectToPassEventually(async () => {
const eventLogEntry = await getLogsFromCloudwatch(
`/aws/vendedlogs/events/event-bus/nhs-${ENV}-dl`,
[
'$.message_type = "EVENT_RECEIPT"',
'$.details.detail_type = "uk.nhs.notify.digital.letters.mesh.inbox.message.received.v1"',
`$.details.event_detail = "*\\"meshMessageId\\":\\"${meshMessageId}\\"*"`,
`$.details.event_detail = "*\\"senderId\\":\\"${senderId}\\"*"`,
],
);

expect(eventLogEntry.length).toBeGreaterThanOrEqual(1);
}, 120_000);
}

async function expectMeshInboxMessageDownloadedEvent(
messageReference: string,
): Promise<void> {
await expectToPassEventually(async () => {
const eventLogEntry = await getLogsFromCloudwatch(
`/aws/vendedlogs/events/event-bus/nhs-${ENV}-dl`,
[
'$.message_type = "EVENT_RECEIPT"',
'$.details.detail_type = "uk.nhs.notify.digital.letters.mesh.inbox.message.downloaded.v1"',
`$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`,
`$.details.event_detail = "*\\"senderId\\":\\"${senderId}\\"*"`,
],
);

expect(eventLogEntry.length).toBeGreaterThanOrEqual(1);
}, 180_000);
}

test('should poll message from MESH inbox, publish received event, download message, and publish downloaded event', async () => {
const meshMessageId = `${Date.now()}_TEST_${uuidv4().slice(0, 8)}`;
const messageReference = uuidv4();
const messageContent = JSON.stringify({
senderId,
messageReference,
testData: 'This is a test letter content',
timestamp: new Date().toISOString(),
});

await uploadMeshMessage(meshMessageId, messageReference, messageContent);

await invokeLambda(MESH_POLL_LAMBDA_NAME);

await expectMeshInboxMessageReceivedEvent(meshMessageId);
await expectMeshInboxMessageDownloadedEvent(messageReference);

await expectToPassEventually(async () => {
const storedMessage = await downloadFromS3(
PII_S3_BUCKET_NAME,
`document-reference/${senderId}_${messageReference}`,
);

expect(storedMessage.body).toContain(messageContent);
}, 60_000);

await expectToPassEventually(async () => {
await expect(async () => {
await downloadFromS3(
NON_PII_S3_BUCKET_NAME,
`mock-mesh/${meshMailboxId}/in/${meshMessageId}`,
);
}).rejects.toThrow('No objects found');
}, 60_000);
});

test('should send message to mesh-download DLQ when download fails', async () => {
test.setTimeout(400_000);

const invalidMeshMessageId = `${Date.now()}_DLQ_${uuidv4().slice(0, 8)}`;
const messageReference = uuidv4();

await eventPublisher.sendEvents(
[
{
id: uuidv4(),
specversion: '1.0',
source:
'/nhs/england/notify/development/primary/data-plane/digitalletters/mesh',
subject:
'customer/00000000-0000-0000-0000-000000000000/recipient/00000000-0000-0000-0000-000000000000',
type: 'uk.nhs.notify.digital.letters.mesh.inbox.message.received.v1',
time: '2026-01-20T15:48:21.636284+00:00',
recordedtime: '2026-01-20T15:48:21.636284+00:00',
severitynumber: 2,
severitytext: 'INFO',
traceparent:
'00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
dataschema:
'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-mesh-inbox-message-received-data.schema.json',
data: {
meshMessageId: invalidMeshMessageId,
senderId,
messageReference,
},
},
],
messageMessageReceived,
);

await expectMessageContainingString(
MESH_DOWNLOAD_DLQ_NAME,
invalidMeshMessageId,
300,
);
});

test('should handle multiple messages in inbox', async () => {
test.setTimeout(300_000);

const messages = Array.from({ length: 3 }, (_, i) => ({
meshMessageId: `${Date.now()}_MULTI_${i}_${uuidv4().slice(0, 8)}`,
messageReference: uuidv4(),
messageContent: JSON.stringify({
senderId,
messageReference: uuidv4(),
testData: `Test message ${i}`,
}),
}));

for (const msg of messages) {
await uploadMeshMessage(
msg.meshMessageId,
msg.messageReference,
msg.messageContent,
);
}

await invokeLambda(MESH_POLL_LAMBDA_NAME);

for (const msg of messages) {
await expectMeshInboxMessageReceivedEvent(msg.meshMessageId);
}
});
});
Loading
Loading