diff --git a/.github/actions/acceptance-tests/action.yaml b/.github/actions/acceptance-tests/action.yaml index 3b76f9bb8..f1d5d9c1b 100644 --- a/.github/actions/acceptance-tests/action.yaml +++ b/.github/actions/acceptance-tests/action.yaml @@ -21,6 +21,11 @@ inputs: description: Name of the component under test default: dl + shard: + description: "Playwright shard index in the format N/total e.g. 1/4 (optional, omit to run all tests)" + required: false + default: "" + runs: using: "composite" @@ -34,11 +39,47 @@ runs: with: node-version: ${{ steps.nodejs_version.outputs.nodejs_version }} GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }} + - name: Cache node modules + id: cache-node-modules + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + node_modules + tests/playwright/node_modules + key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json', 'tests/playwright/package-lock.json') }} + restore-keys: | + node-modules-${{ runner.os }}- - name: "Install dependencies" + if: steps.cache-node-modules.outputs.cache-hit != 'true' shell: bash run: | npm ci + - name: "Cache Playwright browsers" + id: cache-playwright + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('tests/playwright/package-lock.json') }} + restore-keys: | + playwright-${{ runner.os }}- + - name: "Install Playwright browsers" + if: steps.cache-playwright.outputs.cache-hit != 'true' + shell: bash + run: npx playwright install --with-deps + - name: "Cache generated dependencies" + id: cache-generated-deps + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + schemas/digital-letters/ + output/digital-letters/ + src/digital-letters-events/types/ + src/digital-letters-events/validators/ + src/digital-letters-events/digital_letters_events/models/ + src/digital-letters-events/guard-functions/ + key: generated-deps-${{ runner.os }}-${{ hashFiles('src/cloudevents/**', 'src/typescript-schema-generator/**', 'src/python-schema-generator/**') }} - name: "Generate dependencies" + if: steps.cache-generated-deps.outputs.cache-hit != 'true' shell: bash run: | npm run generate-dependencies @@ -58,6 +99,7 @@ runs: env: TEST_TYPE: ${{ inputs.testType }} ENVIRONMENT: ${{ inputs.targetEnvironment }} + PLAYWRIGHT_SHARD: ${{ inputs.shard }} - name: Archive integration test results if: ${{ inputs.testType == 'integration' }} uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 diff --git a/.github/scripts/dispatch_internal_repo_workflow.sh b/.github/scripts/dispatch_internal_repo_workflow.sh index a52c1bbee..1e0e221b5 100755 --- a/.github/scripts/dispatch_internal_repo_workflow.sh +++ b/.github/scripts/dispatch_internal_repo_workflow.sh @@ -80,6 +80,10 @@ while [[ $# -gt 0 ]]; do overrideRoleName="$2" shift 2 ;; + --enableSharding) # Enable test sharding across 4 parallel runners (optional) + enableSharding="$2" + shift 2 + ;; *) echo "[ERROR] Unknown argument: $1" exit 1 @@ -167,6 +171,7 @@ echo " overrides: $overrides" echo " overrideProjectName: $overrideProjectName" echo " overrideRoleName: $overrideRoleName" echo " targetProject: $targetProject" +echo " enableSharding: ${enableSharding:-}" DISPATCH_EVENT=$(jq -ncM \ --arg infraRepoName "$infraRepoName" \ @@ -180,6 +185,7 @@ DISPATCH_EVENT=$(jq -ncM \ --arg overrideProjectName "$overrideProjectName" \ --arg overrideRoleName "$overrideRoleName" \ --arg targetProject "$targetProject" \ + --argjson enableSharding "${enableSharding:-false}" \ '{ "ref": "'"$internalRef"'", "inputs": ( @@ -188,6 +194,7 @@ DISPATCH_EVENT=$(jq -ncM \ (if $overrideProjectName != "" then { "overrideProjectName": $overrideProjectName } else {} end) + (if $overrideRoleName != "" then { "overrideRoleName": $overrideRoleName } else {} end) + (if $targetProject != "" then { "targetProject": $targetProject } else {} end) + + (if $enableSharding then { "enableSharding": $enableSharding } else {} end) + { "releaseVersion": $releaseVersion, "targetEnvironment": $targetEnvironment, diff --git a/.github/workflows/cicd-1-pull-request.yaml b/.github/workflows/cicd-1-pull-request.yaml index 9309aec55..063c800b4 100644 --- a/.github/workflows/cicd-1-pull-request.yaml +++ b/.github/workflows/cicd-1-pull-request.yaml @@ -180,6 +180,7 @@ jobs: --terraformAction "apply" \ --overrideProjectName "nhs" \ --overrideRoleName "nhs-main-acct-digital-letters-github-deploy" \ + --internalRef "feature/add-shard-input-to-dynamic-env-tests" \ --overrides "branch_name=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" acceptance-stage: # Recommended maximum execution time is 10 minutes name: "Acceptance stage" @@ -189,6 +190,7 @@ jobs: with: target_environment: "pr${{ needs.metadata.outputs.pr_number }}" target_account_group: nhs-notify-digital-letters-dev + internal_ref: "feature/add-shard-input-to-dynamic-env-tests" secrets: APP_PEM_FILE: ${{ secrets.APP_PEM_FILE }} APP_CLIENT_ID: ${{ secrets.APP_CLIENT_ID }} diff --git a/.github/workflows/stage-4-acceptance.yaml b/.github/workflows/stage-4-acceptance.yaml index cbd3b1551..c68c3d60f 100644 --- a/.github/workflows/stage-4-acceptance.yaml +++ b/.github/workflows/stage-4-acceptance.yaml @@ -20,6 +20,10 @@ on: description: "Account for the environment being tested" required: true type: string + internal_ref: + description: "Branch or ref to use (defaults to main)" + required: false + type: string jobs: test-security: @@ -84,7 +88,9 @@ jobs: --targetWorkflow "dispatch-contextual-tests-dynamic-env.yaml" \ --targetEnvironment "$TARGET_ENVIRONMENT" \ --targetAccountGroup "$TARGET_ACCOUNT_GROUP" \ - --targetComponent "dl" + --targetComponent "dl" \ + --internalRef "${{ inputs.internal_ref || 'main' }}" \ + --enableSharding "true" test-accessibility: name: "Accessibility test" runs-on: ubuntu-latest diff --git a/infrastructure/terraform/components/dl/README.md b/infrastructure/terraform/components/dl/README.md index 43435531b..1149a210c 100644 --- a/infrastructure/terraform/components/dl/README.md +++ b/infrastructure/terraform/components/dl/README.md @@ -34,6 +34,8 @@ No requirements. | [event\_anomaly\_period](#input\_event\_anomaly\_period) | The period in seconds over which the specified statistic is applied for anomaly detection. Minimum 300 seconds (5 minutes). Recommended: 300-600. | `number` | `300` | no | | [eventpub\_control\_plane\_bus\_arn](#input\_eventpub\_control\_plane\_bus\_arn) | Event publisher control plane | `string` | n/a | yes | | [eventpub\_data\_plane\_bus\_arn](#input\_eventpub\_data\_plane\_bus\_arn) | Event publisher data plane | `string` | n/a | yes | +| [firehose\_destination\_buffer\_interval](#input\_firehose\_destination\_buffer\_interval) | The Firehose destination buffer interval in seconds. Lower values reduce latency for tests but increase costs. Minimum is 60, default (Terraform) is 300. | `number` | `300` | no | +| [firehose\_processor\_buffer\_interval](#input\_firehose\_processor\_buffer\_interval) | The Firehose Lambda processor buffer interval in seconds. Should be 1 more than firehose\_destination\_buffer\_interval to ensure destination flushes first. Minimum is 0. | `number` | `301` | no | | [force\_destroy](#input\_force\_destroy) | Flag to force deletion of S3 buckets | `bool` | `false` | no | | [force\_lambda\_code\_deploy](#input\_force\_lambda\_code\_deploy) | If the lambda package in s3 has the same commit id tag as the terraform build branch, the lambda will not update automatically. Set to True if making changes to Lambda code from on the same commit for example during development | `bool` | `false` | no | | [group](#input\_group) | The group variables are being inherited from (often synonmous with account short-name) | `string` | n/a | yes | @@ -103,6 +105,8 @@ No requirements. | [sqs\_report\_generator](#module\_sqs\_report\_generator) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-sqs.zip | n/a | | [sqs\_report\_sender](#module\_sqs\_report\_sender) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-sqs.zip | n/a | | [sqs\_scanner](#module\_sqs\_scanner) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-sqs.zip | n/a | +| [sqs\_test\_observer](#module\_sqs\_test\_observer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-sqs.zip | n/a | +| [sqs\_test\_observer\_queues](#module\_sqs\_test\_observer\_queues) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-sqs.zip | n/a | | [sqs\_ttl](#module\_sqs\_ttl) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-sqs.zip | n/a | | [sqs\_ttl\_handle\_expiry\_errors](#module\_sqs\_ttl\_handle\_expiry\_errors) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-sqs.zip | n/a | | [ttl\_create](#module\_ttl\_create) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-lambda.zip | n/a | diff --git a/infrastructure/terraform/components/dl/cloudwatch_event_rule_all_events.tf b/infrastructure/terraform/components/dl/cloudwatch_event_rule_all_events.tf index f27e48de9..136e6881a 100644 --- a/infrastructure/terraform/components/dl/cloudwatch_event_rule_all_events.tf +++ b/infrastructure/terraform/components/dl/cloudwatch_event_rule_all_events.tf @@ -18,3 +18,10 @@ resource "aws_cloudwatch_event_target" "reporting_firehose" { role_arn = aws_iam_role.eventbridge_firehose.arn event_bus_name = aws_cloudwatch_event_bus.main.name } + +resource "aws_cloudwatch_event_target" "test_observer_sqs" { + rule = aws_cloudwatch_event_rule.all_events.name + target_id = "test-observer-sqs-target" + arn = module.sqs_test_observer.sqs_queue_arn + event_bus_name = aws_cloudwatch_event_bus.main.name +} diff --git a/infrastructure/terraform/components/dl/kinesis_firehose_delivery_stream_to_s3_reporting.tf b/infrastructure/terraform/components/dl/kinesis_firehose_delivery_stream_to_s3_reporting.tf index 86efee7f4..0be07006e 100644 --- a/infrastructure/terraform/components/dl/kinesis_firehose_delivery_stream_to_s3_reporting.tf +++ b/infrastructure/terraform/components/dl/kinesis_firehose_delivery_stream_to_s3_reporting.tf @@ -11,7 +11,7 @@ resource "aws_kinesis_firehose_delivery_stream" "to_s3_reporting" { error_output_prefix = "${local.firehose_output_path_prefix}/errors/!{timestamp:yyyy}-!{timestamp:MM}-!{timestamp:dd}-!{timestamp:HH}/!{firehose:error-output-type}/" buffering_size = 128 - buffering_interval = 300 + buffering_interval = var.firehose_destination_buffer_interval dynamic_partitioning_configuration { enabled = true @@ -37,7 +37,7 @@ resource "aws_kinesis_firehose_delivery_stream" "to_s3_reporting" { } parameters { parameter_name = "BufferIntervalInSeconds" - parameter_value = 301 + parameter_value = var.firehose_processor_buffer_interval } } } diff --git a/infrastructure/terraform/components/dl/module_sqs_test_observer.tf b/infrastructure/terraform/components/dl/module_sqs_test_observer.tf new file mode 100644 index 000000000..f2be9f4ac --- /dev/null +++ b/infrastructure/terraform/components/dl/module_sqs_test_observer.tf @@ -0,0 +1,41 @@ +module "sqs_test_observer" { + source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-sqs.zip" + + aws_account_id = var.aws_account_id + component = local.component + environment = var.environment + project = var.project + region = var.region + name = "test-observer" + sqs_kms_key_arn = module.kms.key_arn + visibility_timeout_seconds = var.sqs_visibility_timeout_seconds + create_dlq = false + max_receive_count = var.sqs_max_receive_count + sqs_policy_overload = data.aws_iam_policy_document.sqs_test_observer.json +} + +data "aws_iam_policy_document" "sqs_test_observer" { + statement { + sid = "AllowEventBridgeToSendMessage" + effect = "Allow" + + principals { + type = "Service" + identifiers = ["events.amazonaws.com"] + } + + actions = [ + "sqs:SendMessage" + ] + + resources = [ + "arn:aws:sqs:${var.region}:${var.aws_account_id}:${local.csi}-test-observer-queue" + ] + + condition { + test = "ArnLike" + variable = "aws:SourceArn" + values = [aws_cloudwatch_event_rule.all_events.arn] + } + } +} diff --git a/infrastructure/terraform/components/dl/module_sqs_test_observer_queues.tf b/infrastructure/terraform/components/dl/module_sqs_test_observer_queues.tf new file mode 100644 index 000000000..34ff9f77f --- /dev/null +++ b/infrastructure/terraform/components/dl/module_sqs_test_observer_queues.tf @@ -0,0 +1,84 @@ + +# Per-component test observer queues +# Each queue subscribes to a filtered subset of Digital Letters events, +# reducing queue depth per test and eliminating contention between spec files. + +locals { + test_observer_queues = { + mesh = "uk.nhs.notify.digital.letters.mesh.inbox.message." + pdm = "uk.nhs.notify.digital.letters.pdm.resource." + messages = "uk.nhs.notify.digital.letters.messages.request." + print = "uk.nhs.notify.digital.letters.print." + queue-items = "uk.nhs.notify.digital.letters.queue.item." + reporting = "uk.nhs.notify.digital.letters.reporting." + } +} + +module "sqs_test_observer_queues" { + for_each = local.test_observer_queues + + source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-sqs.zip" + + aws_account_id = var.aws_account_id + component = local.component + environment = var.environment + project = var.project + region = var.region + name = "test-observer-${each.key}" + sqs_kms_key_arn = module.kms.key_arn + visibility_timeout_seconds = var.sqs_visibility_timeout_seconds + create_dlq = false + max_receive_count = var.sqs_max_receive_count + sqs_policy_overload = data.aws_iam_policy_document.sqs_test_observer_queues[each.key].json +} + +data "aws_iam_policy_document" "sqs_test_observer_queues" { + for_each = local.test_observer_queues + + statement { + sid = "AllowEventBridgeToSendMessage" + effect = "Allow" + + principals { + type = "Service" + identifiers = ["events.amazonaws.com"] + } + + actions = ["sqs:SendMessage"] + + resources = [ + "arn:aws:sqs:${var.region}:${var.aws_account_id}:${local.csi}-test-observer-${each.key}-queue" + ] + + condition { + test = "ArnLike" + variable = "aws:SourceArn" + values = [aws_cloudwatch_event_rule.test_observer[each.key].arn] + } + } +} + +resource "aws_cloudwatch_event_rule" "test_observer" { + for_each = local.test_observer_queues + + name = "${local.csi}-test-observer-${each.key}" + description = "Event rule for test observer queue: ${each.key}" + event_bus_name = aws_cloudwatch_event_bus.main.name + + event_pattern = jsonencode({ + "detail" : { + "type" : [{ + "prefix" : each.value + }] + } + }) +} + +resource "aws_cloudwatch_event_target" "test_observer_sqs_queues" { + for_each = local.test_observer_queues + + rule = aws_cloudwatch_event_rule.test_observer[each.key].name + target_id = "test-observer-${each.key}-sqs-target" + arn = module.sqs_test_observer_queues[each.key].sqs_queue_arn + event_bus_name = aws_cloudwatch_event_bus.main.name +} diff --git a/infrastructure/terraform/components/dl/variables.tf b/infrastructure/terraform/components/dl/variables.tf index 96a186827..10760f568 100644 --- a/infrastructure/terraform/components/dl/variables.tf +++ b/infrastructure/terraform/components/dl/variables.tf @@ -261,6 +261,18 @@ variable "sqs_visibility_timeout_seconds" { default = "270" } +variable "firehose_destination_buffer_interval" { + type = number + description = "The Firehose destination buffer interval in seconds. Lower values reduce latency for tests but increase costs. Minimum is 60, default (Terraform) is 300." + default = 300 +} + +variable "firehose_processor_buffer_interval" { + type = number + description = "The Firehose Lambda processor buffer interval in seconds. Should be 1 more than firehose_destination_buffer_interval to ensure destination flushes first. Minimum is 0." + default = 301 +} + variable "lambda_timeout_seconds" { type = string description = "The timeout of the lambdas that are triggered by SQS. " diff --git a/scripts/tests/integration.sh b/scripts/tests/integration.sh index 893c4efc2..22a824b1d 100755 --- a/scripts/tests/integration.sh +++ b/scripts/tests/integration.sh @@ -5,8 +5,21 @@ set -euo pipefail cd "$(git rev-parse --show-toplevel)" npm install -npx playwright install --with-deps > /dev/null cd tests/playwright -npm run test:component +case "${INTEGRATION_MODE:-run}" in + setup) + npm run test:component:setup + ;; + teardown) + npm run test:component:teardown + ;; + run) + npm run test:component -- ${PLAYWRIGHT_SHARD:+--shard="$PLAYWRIGHT_SHARD"} + ;; + *) + echo "[ERROR] Unknown INTEGRATION_MODE: ${INTEGRATION_MODE}" >&2 + exit 1 + ;; +esac diff --git a/tests/playwright/config/component/component.config.ts b/tests/playwright/config/component/component.config.ts index 87ca74faa..2b39dacb2 100644 --- a/tests/playwright/config/component/component.config.ts +++ b/tests/playwright/config/component/component.config.ts @@ -14,19 +14,12 @@ export default defineConfig({ testMatch: 'senders.setup.ts', }, { - name: 'firehose:setup', - testMatch: 'firehose.setup.ts', - teardown: 'firehose:teardown', - }, - { - name: 'firehose:teardown', - testMatch: 'firehose.teardown.ts', + name: 'component:setup', + testMatch: 'component.setup.ts', }, { name: 'component', testMatch: '*.component.spec.ts', - dependencies: ['senders:setup', 'firehose:setup'], - teardown: 'component:teardown', }, { name: 'component:teardown', diff --git a/tests/playwright/config/component/component.setup.ts b/tests/playwright/config/component/component.setup.ts new file mode 100644 index 000000000..39a48816d --- /dev/null +++ b/tests/playwright/config/component/component.setup.ts @@ -0,0 +1,31 @@ +import { PurgeQueueCommand } from '@aws-sdk/client-sqs'; +import { test as setup } from '@playwright/test'; +import { + SQS_URL_PREFIX, + TEST_OBSERVER_MESH_QUEUE_NAME, + TEST_OBSERVER_MESSAGES_QUEUE_NAME, + TEST_OBSERVER_PDM_QUEUE_NAME, + TEST_OBSERVER_PRINT_QUEUE_NAME, + TEST_OBSERVER_QUEUE_ITEMS_QUEUE_NAME, + TEST_OBSERVER_REPORTING_QUEUE_NAME, +} from 'constants/backend-constants'; +import { sqsClient } from 'utils'; + +const queues = [ + TEST_OBSERVER_MESH_QUEUE_NAME, + TEST_OBSERVER_MESSAGES_QUEUE_NAME, + TEST_OBSERVER_PDM_QUEUE_NAME, + TEST_OBSERVER_PRINT_QUEUE_NAME, + TEST_OBSERVER_QUEUE_ITEMS_QUEUE_NAME, + TEST_OBSERVER_REPORTING_QUEUE_NAME, +]; + +setup('Purge test observer queues', async () => { + await Promise.all( + queues.map((name) => + sqsClient.send( + new PurgeQueueCommand({ QueueUrl: `${SQS_URL_PREFIX}${name}` }), + ), + ), + ); +}); diff --git a/tests/playwright/config/component/firehose.setup.ts b/tests/playwright/config/component/firehose.setup.ts deleted file mode 100644 index f7ecf537e..000000000 --- a/tests/playwright/config/component/firehose.setup.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { test as setup } from '@playwright/test'; -import { - MINIMUM_DESTINATION_BUFFER_INTERVAL, - MINIMUM_PROCESSOR_BUFFER_INTERVAL, - TERRAFORM_DESTINATION_BUFFER_INTERVAL, - TERRAFORM_PROCESSOR_BUFFER_INTERVAL, -} from 'constants/backend-constants'; -import { alterFirehoseBufferIntervals } from 'helpers/data-firehose-helpers'; - -setup('Reduce Firehose buffer intervals', async () => { - await alterFirehoseBufferIntervals({ - expected: { - destination: TERRAFORM_DESTINATION_BUFFER_INTERVAL, - processor: TERRAFORM_PROCESSOR_BUFFER_INTERVAL, - }, - update: { - destination: MINIMUM_DESTINATION_BUFFER_INTERVAL, - processor: MINIMUM_PROCESSOR_BUFFER_INTERVAL, - }, - }); -}); diff --git a/tests/playwright/config/component/firehose.teardown.ts b/tests/playwright/config/component/firehose.teardown.ts deleted file mode 100644 index 11844458c..000000000 --- a/tests/playwright/config/component/firehose.teardown.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { test as teardown } from '@playwright/test'; -import { - MINIMUM_DESTINATION_BUFFER_INTERVAL, - MINIMUM_PROCESSOR_BUFFER_INTERVAL, - TERRAFORM_DESTINATION_BUFFER_INTERVAL, - TERRAFORM_PROCESSOR_BUFFER_INTERVAL, -} from 'constants/backend-constants'; -import { alterFirehoseBufferIntervals } from 'helpers/data-firehose-helpers'; - -teardown('Restore Firehose buffer intervals', async () => { - await alterFirehoseBufferIntervals({ - expected: { - destination: MINIMUM_DESTINATION_BUFFER_INTERVAL, - processor: MINIMUM_PROCESSOR_BUFFER_INTERVAL, - }, - update: { - destination: TERRAFORM_DESTINATION_BUFFER_INTERVAL, - processor: TERRAFORM_PROCESSOR_BUFFER_INTERVAL, - }, - }); -}); diff --git a/tests/playwright/constants/backend-constants.ts b/tests/playwright/constants/backend-constants.ts index 0ca126e33..3adacdc27 100644 --- a/tests/playwright/constants/backend-constants.ts +++ b/tests/playwright/constants/backend-constants.ts @@ -32,6 +32,15 @@ export const PRINT_SENDER_DLQ_NAME = `${CSI}-print-sender-dlq`; export const MOVE_SCANNED_FILES_NAME = `${CSI}-move-scanned-files-queue`; export const MOVE_SCANNED_FILES_DLQ_NAME = `${CSI}-move-scanned-files-dlq`; export const REPORT_SENDER_DLQ_NAME = `${CSI}-report-sender-dlq`; +export const TEST_OBSERVER_QUEUE_NAME = `${CSI}-test-observer-queue`; + +// Per-component test observer queues +export const TEST_OBSERVER_MESH_QUEUE_NAME = `${CSI}-test-observer-mesh-queue`; +export const TEST_OBSERVER_PDM_QUEUE_NAME = `${CSI}-test-observer-pdm-queue`; +export const TEST_OBSERVER_MESSAGES_QUEUE_NAME = `${CSI}-test-observer-messages-queue`; +export const TEST_OBSERVER_PRINT_QUEUE_NAME = `${CSI}-test-observer-print-queue`; +export const TEST_OBSERVER_QUEUE_ITEMS_QUEUE_NAME = `${CSI}-test-observer-queue-items-queue`; +export const TEST_OBSERVER_REPORTING_QUEUE_NAME = `${CSI}-test-observer-reporting-queue`; // Queue Url Prefix export const SQS_URL_PREFIX = `https://sqs.${REGION}.amazonaws.com/${AWS_ACCOUNT_ID}/`; @@ -73,11 +82,12 @@ export const MESH_DOWNLOAD_LAMBDA_LOG_GROUP_NAME = `/aws/lambda/${CSI}-mesh-down // Data Firehose export const FIREHOSE_STREAM_NAME = `${CSI}-to-s3-reporting`; -export const TERRAFORM_DESTINATION_BUFFER_INTERVAL = 300; -export const TERRAFORM_PROCESSOR_BUFFER_INTERVAL = 301; +export const TERRAFORM_DESTINATION_BUFFER_INTERVAL = 60; +export const TERRAFORM_PROCESSOR_BUFFER_INTERVAL = 61; export const MINIMUM_DESTINATION_BUFFER_INTERVAL = 60; export const MINIMUM_PROCESSOR_BUFFER_INTERVAL = 0; // Athena export const ATHENA_WORKGROUP_NAME = CSI; export const CREATE_TTL_LAMBDA_LOG_GROUP_NAME = `/aws/lambda/${CSI}-ttl-create`; +export const TTL_HANDLE_EXPIRY_LAMBDA_LOG_GROUP_NAME = `/aws/lambda/${CSI}-ttl-handle-expiry`; diff --git a/tests/playwright/digital-letters-component-tests/core-notify.component.spec.ts b/tests/playwright/digital-letters-component-tests/core-notify.component.spec.ts index 96ad87362..c41b94ffa 100644 --- a/tests/playwright/digital-letters-component-tests/core-notify.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/core-notify.component.spec.ts @@ -1,9 +1,5 @@ import { expect, test } from '@playwright/test'; -import { - CORE_NOTIFIER_DLQ_NAME, - CORE_NOTIFIER_LAMBDA_LOG_GROUP_NAME, - EVENT_BUS_LOG_GROUP_NAME, -} from 'constants/backend-constants'; +import { CORE_NOTIFIER_DLQ_NAME } from 'constants/backend-constants'; import { SENDER_ID_SKIPS_NOTIFY, SENDER_ID_THAT_TRIGGERS_ERROR_IN_NOTIFY_SANDBOX, @@ -13,10 +9,12 @@ import { PDMResourceAvailable, validatePDMResourceAvailable, } from 'digital-letters-events'; -import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; -import expectToPassEventually from 'helpers/expectations'; import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers'; +import { + MESSAGES_OBSERVER_QUEUE_URL, + expectEventOnTestObserverQueue, +} from 'helpers/test-observer-helpers'; import { v4 as uuidv4 } from 'uuid'; const baseEvent: Omit = { @@ -69,35 +67,23 @@ test.describe('Digital Letters - Core Notify', () => { validatePDMResourceAvailable, ); - // Verify the event is processed and a message appears in the Lambda logs - await expectToPassEventually(async () => { - const filteredLogs = await getLogsFromCloudwatch( - CORE_NOTIFIER_LAMBDA_LOG_GROUP_NAME, - [ - '$.message.description = "Successfully processed request and sent to Notify"', - `$.message.messageReference = "${messageReference}"`, - ], - ); - - expect(filteredLogs.length).toEqual(1); - }, 240); - - // Verify the event is published in the event bus - await expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - EVENT_BUS_LOG_GROUP_NAME, - [ - '$.message_type = "EVENT_RECEIPT"', - '$.details.detail_type = "uk.nhs.notify.digital.letters.messages.request.submitted.v1"', - `$.details.event_detail = "*\\"notifyId\\":\\"*\\"*"`, - `$.details.event_detail = "*\\"messageUri\\":\\"https://www.nhsapp.service.nhs.uk/digital-letters?letterid=${resourceId}\\"*"`, - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - `$.details.event_detail = "*\\"senderId\\":\\"${SENDER_ID_VALID_FOR_NOTIFY_SANDBOX}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 240); + const submittedDetail = await expectEventOnTestObserverQueue( + MESSAGES_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.messages.request.submitted.v1', + (d) => { + const { data } = d as any; + return ( + data.messageReference === messageReference && + data.senderId === SENDER_ID_VALID_FOR_NOTIFY_SANDBOX + ); + }, + 80_000, + ); + const submittedData = (submittedDetail as any).data; + expect(submittedData.notifyId).toBeTruthy(); + expect(submittedData.messageUri).toBe( + `https://www.nhsapp.service.nhs.uk/digital-letters?letterid=${resourceId}`, + ); }); test('given PDMResourceAvailable event with a client configured with a Routing plan not recognized by the Core Notify sandbox, when the sandbox receives the event then it replies with an error', async () => { @@ -122,35 +108,23 @@ test.describe('Digital Letters - Core Notify', () => { validatePDMResourceAvailable, ); - // Verify the event is processed and a message appears in the Lambda logs - await expectToPassEventually(async () => { - const filteredLogs = await getLogsFromCloudwatch( - CORE_NOTIFIER_LAMBDA_LOG_GROUP_NAME, - [ - '$.message.description = "Failed sending request to Notify API"', - `$.message.messageReference = "${messageReference}"`, - ], - ); - - expect(filteredLogs.length).toEqual(1); - }, 240); - - // Verify the event is published in the event bus - await expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - EVENT_BUS_LOG_GROUP_NAME, - [ - '$.message_type = "EVENT_RECEIPT"', - '$.details.detail_type = "uk.nhs.notify.digital.letters.messages.request.rejected.v1"', - `$.details.event_detail = "*\\"failureCode\\":\\"CM_INVALID_VALUE\\"*"`, - `$.details.event_detail = "*\\"messageUri\\":\\"https://www.nhsapp.service.nhs.uk/digital-letters?letterid=${resourceId}\\"*"`, - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - `$.details.event_detail = "*\\"senderId\\":\\"${SENDER_ID_THAT_TRIGGERS_ERROR_IN_NOTIFY_SANDBOX}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 240); + const rejectedDetail = await expectEventOnTestObserverQueue( + MESSAGES_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.messages.request.rejected.v1', + (d) => { + const { data } = d as any; + return ( + data.messageReference === messageReference && + data.senderId === SENDER_ID_THAT_TRIGGERS_ERROR_IN_NOTIFY_SANDBOX + ); + }, + 80_000, + ); + const rejectedData = (rejectedDetail as any).data; + expect(rejectedData.failureCode).toBe('CM_INVALID_VALUE'); + expect(rejectedData.messageUri).toBe( + `https://www.nhsapp.service.nhs.uk/digital-letters?letterid=${resourceId}`, + ); }); test('given PDMResourceAvailable event, when client does NOT have routingConfigId then a message is NOT sent to core Notify', async () => { @@ -175,19 +149,18 @@ test.describe('Digital Letters - Core Notify', () => { ); // Verify the event is published in the event bus - await expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - EVENT_BUS_LOG_GROUP_NAME, - [ - '$.message_type = "EVENT_RECEIPT"', - '$.details.detail_type = "uk.nhs.notify.digital.letters.messages.request.skipped.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - `$.details.event_detail = "*\\"senderId\\":\\"${SENDER_ID_SKIPS_NOTIFY}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 240); + await expectEventOnTestObserverQueue( + MESSAGES_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.messages.request.skipped.v1', + (d) => { + const { data } = d as any; + return ( + data.messageReference === messageReference && + data.senderId === SENDER_ID_SKIPS_NOTIFY + ); + }, + 80_000, + ); }); test('given PDMResourceAvailable event, when client does NOT exist then it goes to DLQ', async () => { @@ -212,18 +185,7 @@ test.describe('Digital Letters - Core Notify', () => { validatePDMResourceAvailable, ); - await Promise.all([ - // Verify the event is processed and a message appears in the Lambda logs - expectToPassEventually(async () => { - const filteredLogs = await getLogsFromCloudwatch( - CORE_NOTIFIER_LAMBDA_LOG_GROUP_NAME, - ['$.message.description = "0 of 1 records processed successfully"'], - ); - - expect(filteredLogs.length).toBeGreaterThanOrEqual(1); - }, 240), - // Verify there is a message in the DLQ - expectMessageContainingString(CORE_NOTIFIER_DLQ_NAME, eventId, 240), - ]); + // Verify there is a message in the DLQ + expectMessageContainingString(CORE_NOTIFIER_DLQ_NAME, eventId, 240); }); }); diff --git a/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts b/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts index c65a1ed37..7adacb1f3 100644 --- a/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts @@ -1,6 +1,5 @@ import { expect, test } from '@playwright/test'; import { - ENV, MESH_ACKNOWLEDGE_DLQ_NAME, NON_PII_S3_BUCKET_NAME, } from 'constants/backend-constants'; @@ -11,11 +10,14 @@ import { validateMESHInboxMessageDownloaded, validateMESHInboxMessageInvalid, } from 'digital-letters-events'; -import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; import { downloadFromS3 } from 'helpers/s3-helpers'; import { expectMessageContainingString } from 'helpers/sqs-helpers'; +import { + MESH_OBSERVER_QUEUE_URL, + expectEventOnTestObserverQueue, +} from 'helpers/test-observer-helpers'; import { v4 as uuidv4 } from 'uuid'; test.describe('Digital Letters - Mesh Acknowledger', () => { @@ -76,33 +78,26 @@ test.describe('Digital Letters - Mesh Acknowledger', () => { validateMESHInboxMessageDownloaded, ); - // The mailbox ID matches the Mock MESH config in SSM. const meshMailboxId = 'mock-mailbox'; // Verify message acknowledged event was published, // and extract sentMeshMessageId to use for the S3 lookup. - let sentMeshMessageId: string; - 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.acknowledged.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - `$.details.event_detail = "*\\"senderId\\":\\"${senderId}\\"*"`, - `$.details.event_detail = "*\\"meshMailboxId\\":\\"${sendersMeshMailboxId}\\"*"`, - `$.details.event_detail = "*\\"receivedMeshMessageId\\":\\"${meshMessageId}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - - const eventDetail = JSON.parse( - (eventLogEntry[0] as any).details.event_detail, - ); - sentMeshMessageId = eventDetail.data.sentMeshMessageId; - expect(sentMeshMessageId).toBeTruthy(); - }); + const acknowledgedDetail = await expectEventOnTestObserverQueue( + MESH_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.mesh.inbox.message.acknowledged.v1', + (d) => { + const { data } = d as any; + return ( + data.messageReference === messageReference && + data.senderId === senderId && + data.meshMailboxId === sendersMeshMailboxId && + data.receivedMeshMessageId === meshMessageId + ); + }, + 80_000, + ); + const { sentMeshMessageId } = (acknowledgedDetail as any).data; + expect(sentMeshMessageId).toBeTruthy(); // Verify MESH acknowledgement message was sent. await expectToPassEventually(async () => { @@ -208,29 +203,23 @@ test.describe('Digital Letters - Mesh Acknowledger', () => { // Verify message acknowledged event was published with statusCode 400, // and extract sentMeshMessageId to use for the S3 lookup. - let sentMeshMessageId: string; - 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.acknowledged.v1"', - `$.details.event_detail = "*\\"senderId\\":\\"${senderId}\\"*"`, - `$.details.event_detail = "*\\"meshMailboxId\\":\\"${sendersMeshMailboxId}\\"*"`, - `$.details.event_detail = "*\\"statusCode\\":400*"`, - `$.details.event_detail = "*\\"failureCode\\":\\"${failureCode}\\"*"`, - `$.details.event_detail = "*\\"receivedMeshMessageId\\":\\"${meshMessageId}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - - const eventDetail = JSON.parse( - (eventLogEntry[0] as any).details.event_detail, - ); - sentMeshMessageId = eventDetail.data.sentMeshMessageId; - expect(sentMeshMessageId).toBeTruthy(); - }, 120_000); + const acknowledgedDetail2 = await expectEventOnTestObserverQueue( + MESH_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.mesh.inbox.message.acknowledged.v1', + (d) => { + const { data } = d as any; + return ( + data.senderId === senderId && + data.meshMailboxId === sendersMeshMailboxId && + data.statusCode === 400 && + data.failureCode === failureCode && + data.receivedMeshMessageId === meshMessageId + ); + }, + 80_000, + ); + const { sentMeshMessageId } = (acknowledgedDetail2 as any).data; + expect(sentMeshMessageId).toBeTruthy(); // Verify MESH negative acknowledgement message was sent. await expectToPassEventually(async () => { diff --git a/tests/playwright/digital-letters-component-tests/mesh-poll-download.component.spec.ts b/tests/playwright/digital-letters-component-tests/mesh-poll-download.component.spec.ts index 0ff3ce0ee..3c75ac075 100644 --- a/tests/playwright/digital-letters-component-tests/mesh-poll-download.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/mesh-poll-download.component.spec.ts @@ -1,6 +1,5 @@ import { expect, test } from '@playwright/test'; import { - ENV, MESH_DOWNLOAD_DLQ_NAME, MESH_DOWNLOAD_LAMBDA_LOG_GROUP_NAME, MESH_POLL_LAMBDA_NAME, @@ -12,7 +11,11 @@ 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 { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers'; +import { + MESH_OBSERVER_QUEUE_URL, + expectEventOnTestObserverQueue, +} from 'helpers/test-observer-helpers'; import { v4 as uuidv4 } from 'uuid'; import { SENDER_ID_SKIPS_NOTIFY } from 'constants/tests-constants'; import { validateMESHInboxMessageReceived } from 'digital-letters-events'; @@ -72,6 +75,10 @@ test.describe('Digital Letters - MESH Poll and Download', () => { const sendersMeshMailboxId = 'test-mesh-sender-1'; const meshMailboxId = 'mock-mailbox'; + test.beforeAll(async () => { + await purgeQueue(MESH_DOWNLOAD_DLQ_NAME); + }); + async function uploadMeshMessage( meshMessageId: string, messageReference: string, @@ -93,58 +100,70 @@ test.describe('Digital Letters - MESH Poll and Download', () => { async function expectMeshInboxMessageReceivedEvent( meshMessageId: string, ): Promise { - 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); + await expectEventOnTestObserverQueue( + MESH_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.mesh.inbox.message.received.v1', + (detail) => { + const { data } = detail as { + data: { meshMessageId?: string; senderId?: string }; + }; + return ( + data.meshMessageId === meshMessageId && data.senderId === senderId + ); + }, + 80_000, + ); } async function expectMeshInboxMessageDownloadedEvent( messageReference: string, ): Promise { - 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); + await expectEventOnTestObserverQueue( + MESH_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.mesh.inbox.message.downloaded.v1', + (detail) => { + const { data } = detail as { + data: { messageReference?: string; senderId?: string }; + }; + return ( + data.messageReference === messageReference && + data.senderId === senderId + ); + }, + 80_000, + ); } async function expectMeshInboxMessageInvalidEvent( meshMessageId: string, - messageReference: string, + messageReference: string | undefined, + failureCode = 'DL_CLIV_005', ): Promise { - 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.invalid.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - `$.details.event_detail = "*\\"senderId\\":\\"${senderId}\\"*"`, - `$.details.event_detail = "*\\"meshMessageId\\":\\"${meshMessageId}\\"*"`, - `$.details.event_detail = "*\\"failureCode\\":\\"DL_CLIV_005\\"*"`, - ], - ); - - expect(eventLogEntry.length).toBeGreaterThanOrEqual(1); - }, 180_000); + await expectEventOnTestObserverQueue( + MESH_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.mesh.inbox.message.invalid.v1', + (detail) => { + const { data } = detail as { + data: { + meshMessageId: string; + messageReference?: string; + senderId: string; + failureCode: string; + }; + }; + const messageReferenceMatches = + messageReference === undefined || + messageReference === '' || + data?.messageReference === messageReference; + return ( + data.meshMessageId === meshMessageId && + messageReferenceMatches && + data.senderId === senderId && + data.failureCode === failureCode + ); + }, + 120_000, + ); } test('should poll message from MESH inbox, publish received event, download message, and publish downloaded event', async () => { @@ -166,7 +185,7 @@ test.describe('Digital Letters - MESH Poll and Download', () => { ); expect(storedMessage.body).toContain(messageContent); - }, 60_000); + }, 80_000); await expectToPassEventually(async () => { await expect(async () => { @@ -175,10 +194,11 @@ test.describe('Digital Letters - MESH Poll and Download', () => { `mock-mesh/${meshMailboxId}/in/${meshMessageId}`, ); }).rejects.toThrow('No objects found'); - }, 60_000); + }, 80_000); }); test('given invalid PDM request should publish invalid event, log an error, acknowledge message', async () => { + test.setTimeout(340_000); const meshMessageId = `${Date.now()}_TEST_${uuidv4().slice(0, 8)}`; const messageReference = uuidv4(); const invalidPdmRequest = { ...validPdmRequest, id: undefined }; @@ -212,7 +232,7 @@ test.describe('Digital Letters - MESH Poll and Download', () => { `mock-mesh/${meshMailboxId}/in/${meshMessageId}`, ); }).rejects.toThrow('No objects found'); - }, 60_000); + }, 80_000); }); test('should send message to mesh-download DLQ when download fails', async () => { @@ -287,6 +307,8 @@ test.describe('Digital Letters - MESH Poll and Download', () => { }); test('should publish MESHInboxMessageInvalid event when local_id is missing', async () => { + test.setTimeout(200_000); + const meshMessageId = `${Date.now()}_INVALID_${uuidv4().slice(0, 8)}`; const messageContent = JSON.stringify({ senderId, @@ -300,20 +322,7 @@ test.describe('Digital Letters - MESH Poll and Download', () => { await invokeLambda(MESH_POLL_LAMBDA_NAME); - 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.invalid.v1"', - String.raw`$.details.event_detail = "*\"meshMessageId\":\"${meshMessageId}\"*"`, - String.raw`$.details.event_detail = "*\"senderId\":\"${senderId}\"*"`, - String.raw`$.details.event_detail = "*\"failureCode\":\"DL_CLIV_006\"*"`, - ], - ); - - expect(eventLogEntry.length).toBeGreaterThanOrEqual(1); - }, 120_000); + await expectMeshInboxMessageInvalidEvent(meshMessageId, '', 'DL_CLIV_006'); await expectToPassEventually(async () => { await expect(async () => { @@ -322,19 +331,7 @@ test.describe('Digital Letters - MESH Poll and Download', () => { `mock-mesh/${meshMailboxId}/in/${meshMessageId}`, ); }).rejects.toThrow('No objects found'); - }, 60_000); - - await expectToPassEventually(async () => { - const receivedEvents = 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}\\"*"`, - ], - ); - expect(receivedEvents.length).toBe(0); - }, 15_000); + }, 80_000); }); test('should skip publishing downloaded event and acknowledge message when document already exists in S3', async () => { @@ -392,19 +389,6 @@ test.describe('Digital Letters - MESH Poll and Download', () => { expect(warnLogEntry.length).toBeGreaterThanOrEqual(1); }, 120_000); - // Assert that no MESHInboxMessageDownloaded event was published - await expectToPassEventually(async () => { - const downloadedEvents = 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}\\"*"`, - ], - ); - expect(downloadedEvents.length).toBe(0); - }, 15_000); - // Assert the MESH message was still acknowledged (deleted from mock inbox) await expectToPassEventually(async () => { await expect(async () => { @@ -413,6 +397,6 @@ test.describe('Digital Letters - MESH Poll and Download', () => { `mock-mesh/${meshMailboxId}/in/${meshMessageId}`, ); }).rejects.toThrow('No objects found'); - }, 60_000); + }, 80_000); }); }); diff --git a/tests/playwright/digital-letters-component-tests/move-scanned-files.component.spec.ts b/tests/playwright/digital-letters-component-tests/move-scanned-files.component.spec.ts index c04ac2bf6..43459db31 100644 --- a/tests/playwright/digital-letters-component-tests/move-scanned-files.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/move-scanned-files.component.spec.ts @@ -1,17 +1,18 @@ import { expect, test } from '@playwright/test'; import { - EVENT_BUS_LOG_GROUP_NAME, FILE_QUARANTINE_S3_BUCKET_NAME, FILE_SAFE_S3_BUCKET_NAME, MOVE_SCANNED_FILES_DLQ_NAME, - MOVE_SCANNED_FILES_LAMBDA_LOG_GROUP_NAME, PREFIX_DL_FILES, UNSCANNED_FILES_S3_BUCKET_NAME, } from 'constants/backend-constants'; import { SENDER_ID_SKIPS_NOTIFY } from 'constants/tests-constants'; -import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import expectToPassEventually from 'helpers/expectations'; import { expectMessageContainingString } from 'helpers/sqs-helpers'; +import { + PRINT_OBSERVER_QUEUE_URL, + expectEventOnTestObserverQueue, +} from 'helpers/test-observer-helpers'; import { v4 as uuidv4 } from 'uuid'; import { PutObjectCommand } from '@aws-sdk/client-s3'; import { getS3ObjectMetadata, s3Client } from 'utils'; @@ -39,38 +40,21 @@ test.describe('Digital Letters - Move Scanned Files', () => { await s3Client.send(new PutObjectCommand(params)); - // Verify the event is processed and a message appears in the Lambda logs - await expectToPassEventually(async () => { - const filteredLogs = await getLogsFromCloudwatch( - MOVE_SCANNED_FILES_LAMBDA_LOG_GROUP_NAME, - [ - '$.message.description = "Moved file to destination bucket"', - '$.message.scanStatus = "COMPLETED"', - `$.message.messageReference = "${messageReference}"`, - `$.message.senderId = "${SENDER_ID_SKIPS_NOTIFY}"`, - ], - ); - - expect(filteredLogs.length).toEqual(1); - }, 240); - // Verify the event is published in the event bus - await expectToPassEventually(async () => { - const expectedLetterUri = `s3://${FILE_SAFE_S3_BUCKET_NAME}/${objectKey}`; - const eventLogEntry = await getLogsFromCloudwatch( - EVENT_BUS_LOG_GROUP_NAME, - [ - '$.message_type = "EVENT_RECEIPT"', - '$.details.detail_type = "uk.nhs.notify.digital.letters.print.file.safe.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - `$.details.event_detail = "*\\"senderId\\":\\"${SENDER_ID_SKIPS_NOTIFY}\\"*"`, - `$.details.event_detail = "*\\"createdAt\\":\\"${createdAt}\\"*"`, - `$.details.event_detail = "*\\"letterUri\\":\\"${expectedLetterUri}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 240); + await expectEventOnTestObserverQueue( + PRINT_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.print.file.safe.v1', + (detail) => { + const { data } = detail as { + data: { messageReference?: string; senderId?: string }; + }; + return ( + data.messageReference === messageReference && + data.senderId === SENDER_ID_SKIPS_NOTIFY + ); + }, + 80_000, + ); await expectToPassEventually(async () => { const metadata = await getS3ObjectMetadata({ @@ -121,38 +105,21 @@ test.describe('Digital Letters - Move Scanned Files', () => { await s3Client.send(new PutObjectCommand(params)); - // Verify the event is processed and a message appears in the Lambda logs - await expectToPassEventually(async () => { - const filteredLogs = await getLogsFromCloudwatch( - MOVE_SCANNED_FILES_LAMBDA_LOG_GROUP_NAME, - [ - '$.message.description = "Moved file to destination bucket"', - '$.message.scanStatus = "COMPLETED"', - `$.message.messageReference = "${messageReference}"`, - `$.message.senderId = "${SENDER_ID_SKIPS_NOTIFY}"`, - ], - ); - - expect(filteredLogs.length).toEqual(1); - }, 240); - // Verify the event is published in the event bus - await expectToPassEventually(async () => { - const expectedLetterUri = `s3://${FILE_QUARANTINE_S3_BUCKET_NAME}/${objectKey}`; - const eventLogEntry = await getLogsFromCloudwatch( - EVENT_BUS_LOG_GROUP_NAME, - [ - '$.message_type = "EVENT_RECEIPT"', - '$.details.detail_type = "uk.nhs.notify.digital.letters.print.file.quarantined.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - `$.details.event_detail = "*\\"senderId\\":\\"${SENDER_ID_SKIPS_NOTIFY}\\"*"`, - `$.details.event_detail = "*\\"createdAt\\":\\"${createdAt}\\"*"`, - `$.details.event_detail = "*\\"letterUri\\":\\"${expectedLetterUri}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 240); + await expectEventOnTestObserverQueue( + PRINT_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.print.file.quarantined.v1', + (detail) => { + const { data } = detail as { + data: { messageReference?: string; senderId?: string }; + }; + return ( + data.messageReference === messageReference && + data.senderId === SENDER_ID_SKIPS_NOTIFY + ); + }, + 80_000, + ); await expectToPassEventually(async () => { const metadata = await getS3ObjectMetadata({ diff --git a/tests/playwright/digital-letters-component-tests/pdm-poll.component.spec.ts b/tests/playwright/digital-letters-component-tests/pdm-poll.component.spec.ts index fca7aff57..4f43f9c78 100644 --- a/tests/playwright/digital-letters-component-tests/pdm-poll.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/pdm-poll.component.spec.ts @@ -1,17 +1,15 @@ import { expect, test } from '@playwright/test'; -import { - EVENT_BUS_LOG_GROUP_NAME, - PDM_POLL_DLQ_NAME, - PDM_POLL_LAMBDA_LOG_GROUP_NAME, -} from 'constants/backend-constants'; +import { PDM_POLL_DLQ_NAME } from 'constants/backend-constants'; import { validatePDMResourceSubmitted, validatePDMResourceUnavailable, } from 'digital-letters-events'; -import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; -import expectToPassEventually from 'helpers/expectations'; import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers'; +import { + PDM_OBSERVER_QUEUE_URL, + expectEventOnTestObserverQueue, +} from 'helpers/test-observer-helpers'; import { v4 as uuidv4 } from 'uuid'; const baseEvent = { @@ -71,20 +69,14 @@ test.describe('PDM Poll', () => { validatePDMResourceSubmitted, ); - await expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - EVENT_BUS_LOG_GROUP_NAME, - [ - '$.message_type = "EVENT_RECEIPT"', - '$.details.detail_type = "uk.nhs.notify.digital.letters.pdm.resource.available.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - `$.details.event_detail = "*\\"odsCode\\":\\"Y05868\\"*"`, - `$.details.event_detail = "*\\"nhsNumber\\":\\"9912003071\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 120); + const availableDetail = await expectEventOnTestObserverQueue( + PDM_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.pdm.resource.available.v1', + (d) => (d as any).data.messageReference === messageReference, + 80_000, + ); + expect((availableDetail as any).data.odsCode).toBe('Y05868'); + expect((availableDetail as any).data.nhsNumber).toBe('9912003071'); }); test('should send a pdm.resource.unavailable event when unavailable in PDM', async () => { @@ -108,19 +100,13 @@ test.describe('PDM Poll', () => { validatePDMResourceSubmitted, ); - await expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - EVENT_BUS_LOG_GROUP_NAME, - [ - '$.message_type = "EVENT_RECEIPT"', - '$.details.detail_type = "uk.nhs.notify.digital.letters.pdm.resource.unavailable.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - `$.details.event_detail = "*\\"retryCount\\":0*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 120); + const unavailableDetail = await expectEventOnTestObserverQueue( + PDM_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.pdm.resource.unavailable.v1', + (d) => (d as any).data.messageReference === messageReference, + 80_000, + ); + expect((unavailableDetail as any).data.retryCount).toBe(0); }); }); @@ -147,20 +133,14 @@ test.describe('PDM Poll', () => { validatePDMResourceUnavailable, ); - await expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - EVENT_BUS_LOG_GROUP_NAME, - [ - '$.message_type = "EVENT_RECEIPT"', - '$.details.detail_type = "uk.nhs.notify.digital.letters.pdm.resource.available.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - `$.details.event_detail = "*\\"odsCode\\":\\"Y05868\\"*"`, - `$.details.event_detail = "*\\"nhsNumber\\":\\"9912003071\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 120); + const availableDetail2 = await expectEventOnTestObserverQueue( + PDM_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.pdm.resource.available.v1', + (d) => (d as any).data.messageReference === messageReference, + 80_000, + ); + expect((availableDetail2 as any).data.odsCode).toBe('Y05868'); + expect((availableDetail2 as any).data.nhsNumber).toBe('9912003071'); }); test('should send a pdm.resource.unavailable event when still unavailable in PDM', async () => { @@ -185,19 +165,18 @@ test.describe('PDM Poll', () => { validatePDMResourceUnavailable, ); - await expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - EVENT_BUS_LOG_GROUP_NAME, - [ - '$.message_type = "EVENT_RECEIPT"', - '$.details.detail_type = "uk.nhs.notify.digital.letters.pdm.resource.unavailable.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - `$.details.event_detail = "*\\"retryCount\\":1*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 120); + const unavailableDetail2 = await expectEventOnTestObserverQueue( + PDM_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.pdm.resource.unavailable.v1', + (d) => { + const { data } = d as any; + return ( + data.messageReference === messageReference && data.retryCount === 1 + ); + }, + 80_000, + ); + expect((unavailableDetail2 as any).data.retryCount).toBe(1); }); test('should send a pdm.resource.retries.exceeded event when unavailable in PDM after 10 retries', async () => { @@ -222,19 +201,13 @@ test.describe('PDM Poll', () => { validatePDMResourceUnavailable, ); - await expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - EVENT_BUS_LOG_GROUP_NAME, - [ - '$.message_type = "EVENT_RECEIPT"', - '$.details.detail_type = "uk.nhs.notify.digital.letters.pdm.resource.retries.exceeded.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - `$.details.event_detail = "*\\"retryCount\\":10*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 120); + const exceededDetail = await expectEventOnTestObserverQueue( + PDM_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.pdm.resource.retries.exceeded.v1', + (d) => (d as any).data.messageReference === messageReference, + 80_000, + ); + expect((exceededDetail as any).data.retryCount).toBe(10); }); }); @@ -265,19 +238,6 @@ test.describe('PDM Poll', () => { () => true, ); - await Promise.all([ - expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - PDM_POLL_LAMBDA_LOG_GROUP_NAME, - [ - `$.message.err[0].message = "must have required property 'retryCount'"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 150), - - expectMessageContainingString(PDM_POLL_DLQ_NAME, eventId, 150), - ]); + expectMessageContainingString(PDM_POLL_DLQ_NAME, eventId, 150); }); }); diff --git a/tests/playwright/digital-letters-component-tests/pdm-uploader.component.spec.ts b/tests/playwright/digital-letters-component-tests/pdm-uploader.component.spec.ts index 6dfebc614..8eb724861 100644 --- a/tests/playwright/digital-letters-component-tests/pdm-uploader.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/pdm-uploader.component.spec.ts @@ -1,6 +1,5 @@ import { expect, test } from '@playwright/test'; import { - EVENT_BUS_LOG_GROUP_NAME, LETTERS_S3_BUCKET_NAME, PDM_UPLOADER_DLQ_NAME, PDM_UPLOADER_LAMBDA_LOG_GROUP_NAME, @@ -9,6 +8,10 @@ import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers'; +import { + PDM_OBSERVER_QUEUE_URL, + expectEventOnTestObserverQueue, +} from 'helpers/test-observer-helpers'; import { v4 as uuidv4 } from 'uuid'; import { SENDER_ID_SKIPS_NOTIFY } from 'constants/tests-constants'; import { putDataS3 } from 'utils'; @@ -100,18 +103,12 @@ test.describe('Digital Letters - Upload to PDM', () => { expect(filteredLogs.length).toEqual(1); }, 120); - await expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - EVENT_BUS_LOG_GROUP_NAME, - [ - '$.message_type = "EVENT_RECEIPT"', - '$.details.detail_type = "uk.nhs.notify.digital.letters.pdm.resource.submitted.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 120); + await expectEventOnTestObserverQueue( + PDM_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.pdm.resource.submitted.v1', + (d) => (d as any).data.messageReference === messageReference, + 80_000, + ); }); test('should send a pdm.resource.submission.rejected event following an error from PDM', async () => { @@ -166,18 +163,12 @@ test.describe('Digital Letters - Upload to PDM', () => { expect(filteredLogs.length).toEqual(1); }, 120); - await expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - EVENT_BUS_LOG_GROUP_NAME, - [ - '$.message_type = "EVENT_RECEIPT"', - '$.details.detail_type = "uk.nhs.notify.digital.letters.pdm.resource.submission.rejected.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 120); + await expectEventOnTestObserverQueue( + PDM_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.pdm.resource.submission.rejected.v1', + (d) => (d as any).data.messageReference === messageReference, + 80_000, + ); }); test('should send invalid event to uploader dlq', async () => { @@ -205,21 +196,6 @@ test.describe('Digital Letters - Upload to PDM', () => { () => true, ); - await Promise.all([ - expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - EVENT_BUS_LOG_GROUP_NAME, - [ - '$.message_type = "EVENT_RECEIPT"', - '$.details.detail_type = "uk.nhs.notify.digital.letters.pdm.resource.submission.rejected.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 150), - - expectMessageContainingString(PDM_UPLOADER_DLQ_NAME, eventId, 150), - ]); + await expectMessageContainingString(PDM_UPLOADER_DLQ_NAME, eventId, 150); }); }); diff --git a/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts b/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts index befb25d11..81de74f91 100644 --- a/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts @@ -1,17 +1,17 @@ import { expect, test } from '@playwright/test'; import { - ENV, FILE_SAFE_S3_BUCKET_NAME, PRINT_ANALYSER_DLQ_NAME, - PRINT_ANALYSER_LAMBDA_LOG_GROUP_NAME, } from 'constants/backend-constants'; -import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; -import expectToPassEventually from 'helpers/expectations'; import { fivePagePdf } from 'helpers/pdf-helpers'; import { v4 as uuidv4 } from 'uuid'; import { FileSafe, validateFileSafe } from 'digital-letters-events'; import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers'; +import { + PRINT_OBSERVER_QUEUE_URL, + expectEventOnTestObserverQueue, +} from 'helpers/test-observer-helpers'; import { putFileS3 } from 'utils'; export const fileSafeEvent: FileSafe = { @@ -66,23 +66,25 @@ test.describe('Print analyser', () => { await eventPublisher.sendEvents([event], validateFileSafe); - 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.print.pdf.analysed.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - `$.details.event_detail = "*\\"senderId\\":\\"${event.data.senderId}\\"*"`, - `$.details.event_detail = "*\\"letterUri\\":\\"${event.data.letterUri}\\"*"`, - `$.details.event_detail = "*\\"pageCount\\":5*"`, - `$.details.event_detail = "*\\"sha256Hash\\":\\"631b6ef1a936e62277d55a80deb850babdde861152d476489d75b0c9161bd326\\"*"`, - `$.details.event_detail = "*\\"createdAt\\":\\"${event.data.createdAt}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 120); + const analysedDetail = await expectEventOnTestObserverQueue( + PRINT_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.print.pdf.analysed.v1', + (d) => { + const { data } = d as any; + return ( + data.messageReference === messageReference && + data.senderId === event.data.senderId + ); + }, + 80_000, + ); + const analysedData = (analysedDetail as any).data; + expect(analysedData.letterUri).toBe(event.data.letterUri); + expect(analysedData.pageCount).toBe(5); + expect(analysedData.sha256Hash).toBe( + '631b6ef1a936e62277d55a80deb850babdde861152d476489d75b0c9161bd326', + ); + expect(analysedData.createdAt).toBe(event.data.createdAt); }); test('should send invalid event to print analyser dlq', async () => { @@ -99,25 +101,10 @@ test.describe('Print analyser', () => { await eventPublisher.sendEvents([event], () => true); - await Promise.all([ - expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - PRINT_ANALYSER_LAMBDA_LOG_GROUP_NAME, - [ - '$.message.description = "Error parsing FileSafe event"', - `$.message.err[0].message = "must have required property 'senderId'"`, - `$.messageReference = "${messageReference}"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 150), - - expectMessageContainingString( - PRINT_ANALYSER_DLQ_NAME, - messageReference, - 150, - ), - ]); + await expectMessageContainingString( + PRINT_ANALYSER_DLQ_NAME, + messageReference, + 150, + ); }); }); diff --git a/tests/playwright/digital-letters-component-tests/print-status-handler.component.spec.ts b/tests/playwright/digital-letters-component-tests/print-status-handler.component.spec.ts index d6b013389..108912264 100644 --- a/tests/playwright/digital-letters-component-tests/print-status-handler.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/print-status-handler.component.spec.ts @@ -1,15 +1,13 @@ -import { expect, test } from '@playwright/test'; +import { test } from '@playwright/test'; import { LetterEvent } from '@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events'; -import { - ENV, - PRINT_STATUS_HANDLER_DLQ_NAME, - PRINT_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME, -} from 'constants/backend-constants'; -import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; +import { PRINT_STATUS_HANDLER_DLQ_NAME } from 'constants/backend-constants'; import eventPublisher from 'helpers/event-bus-helpers'; -import expectToPassEventually from 'helpers/expectations'; import { v4 as uuidv4 } from 'uuid'; import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers'; +import { + PRINT_OBSERVER_QUEUE_URL, + expectEventOnTestObserverQueue, +} from 'helpers/test-observer-helpers'; const baseLetterEvent = { id: '550e8400-e29b-41d4-a716-446655440001', @@ -78,19 +76,17 @@ test.describe('Print status handler', () => { await eventPublisher.sendEvents([letterEvent], () => true); - 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.print.letter.transitioned.v1"', - `$.details.event_detail = "*\\"messageReference\\":\\"${messageReference}\\"*"`, - `$.details.event_detail = "*\\"status\\":\\"${status}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 120); + await expectEventOnTestObserverQueue( + PRINT_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.print.letter.transitioned.v1', + (d) => { + const { data } = d as any; + return ( + data.messageReference === messageReference && data.status === status + ); + }, + 120_000, + ); }); } @@ -119,24 +115,10 @@ test.describe('Print status handler', () => { () => true, ); - await Promise.all([ - expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - PRINT_STATUS_HANDLER_LAMBDA_LOG_GROUP_NAME, - [ - String.raw`$.message.err.message = "*Invalid option: expected one of \\\"PENDING\\\"*"`, - '$.message.description = "Error parsing queue item"', - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 150), - - expectMessageContainingString( - PRINT_STATUS_HANDLER_DLQ_NAME, - messageReference, - 150, - ), - ]); + await expectMessageContainingString( + PRINT_STATUS_HANDLER_DLQ_NAME, + messageReference, + 150, + ); }); }); diff --git a/tests/playwright/digital-letters-component-tests/report-scheduler.component.spec.ts b/tests/playwright/digital-letters-component-tests/report-scheduler.component.spec.ts index 945c5e5cd..8f06df627 100644 --- a/tests/playwright/digital-letters-component-tests/report-scheduler.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/report-scheduler.component.spec.ts @@ -1,57 +1,38 @@ import { expect, test } from '@playwright/test'; +import { REPORT_SCHEDULER_LAMBDA_NAME } from 'constants/backend-constants'; import { - EVENT_BUS_LOG_GROUP_NAME, - REPORT_SCHEDULER_LAMBDA_NAME, -} from 'constants/backend-constants'; -import { - EXISTING_SENDER_IDS, SENDER_ID_SKIPS_NOTIFY, SENDER_ID_THAT_TRIGGERS_ERROR_IN_NOTIFY_SANDBOX, SENDER_ID_VALID_FOR_NOTIFY_SANDBOX, } from 'constants/tests-constants'; -import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; -import expectToPassEventually from 'helpers/expectations'; import { invokeLambda } from 'helpers/lambda-helpers'; +import { + REPORTING_OBSERVER_QUEUE_URL, + expectEventOnTestObserverQueue, +} from 'helpers/test-observer-helpers'; test.describe('Digital Letters - Report Scheduler', () => { test('should send reporting.generate.report for all senders', async () => { - invokeLambda(REPORT_SCHEDULER_LAMBDA_NAME); + test.setTimeout(120_000); - await expectToPassEventually(async () => { - const eventLogEntries = await getLogsFromCloudwatch( - EVENT_BUS_LOG_GROUP_NAME, - [ - '$.message_type = "EVENT_RECEIPT"', - '$.details.detail_type = "uk.nhs.notify.digital.letters.reporting.generate.report.v1"', - ], - ); - // to avoid conflicts with other tests, we filter the events to only include those with senderIds we know exist in the system, which will produce generation day of yesterday. - const parsedEvents = eventLogEntries - .map((entry: any) => JSON.parse(entry.details.event_detail)) - .filter( - (event: any) => - event.data && EXISTING_SENDER_IDS.includes(event.data.senderId), - ); - - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - const yesterdayString = yesterday.toISOString().split('T')[0]; + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + const yesterdayString = yesterday.toISOString().split('T')[0]; - for (const event of parsedEvents) { - expect(event.type).toBe( - 'uk.nhs.notify.digital.letters.reporting.generate.report.v1', - ); - expect(event.data).toBeDefined(); - expect(event.data.senderId).toBeDefined(); - expect(event.data.reportDate).toBe(yesterdayString); - } + invokeLambda(REPORT_SCHEDULER_LAMBDA_NAME); - const senderIds = parsedEvents.map((event) => event.data.senderId); - expect(senderIds).toContain(SENDER_ID_VALID_FOR_NOTIFY_SANDBOX); - expect(senderIds).toContain( - SENDER_ID_THAT_TRIGGERS_ERROR_IN_NOTIFY_SANDBOX, + for (const senderId of [ + SENDER_ID_VALID_FOR_NOTIFY_SANDBOX, + SENDER_ID_THAT_TRIGGERS_ERROR_IN_NOTIFY_SANDBOX, + SENDER_ID_SKIPS_NOTIFY, + ]) { + const detail = await expectEventOnTestObserverQueue( + REPORTING_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.reporting.generate.report.v1', + (d) => (d as any).data.senderId === senderId, + 80_000, ); - expect(senderIds).toContain(SENDER_ID_SKIPS_NOTIFY); - }, 120); + expect((detail as any).data.reportDate).toBe(yesterdayString); + } }); }); diff --git a/tests/playwright/digital-letters-component-tests/send-reports-trust.component.spec.ts b/tests/playwright/digital-letters-component-tests/send-reports-trust.component.spec.ts index eb14754da..1b1b0f78c 100644 --- a/tests/playwright/digital-letters-component-tests/send-reports-trust.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/send-reports-trust.component.spec.ts @@ -5,9 +5,11 @@ import { REPORTING_S3_BUCKET_NAME, REPORT_SENDER_DLQ_NAME, } from 'constants/backend-constants'; -import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; -import expectToPassEventually from 'helpers/expectations'; +import { + REPORTING_OBSERVER_QUEUE_URL, + expectEventOnTestObserverQueue, +} from 'helpers/test-observer-helpers'; import { downloadFromS3, uploadToS3 } from 'helpers/s3-helpers'; import { expectMessageContainingString } from 'helpers/sqs-helpers'; import { v4 as uuidv4 } from 'uuid'; @@ -53,35 +55,24 @@ test.describe('Digital Letters - Send reports to Trust', () => { async function expectReportSentEventAndMeshMessageSent( meshMailboxReportsId: string, ): Promise { - 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.reporting.report.sent.v1"', - `$.details.event_detail = "*\\"meshMailboxReportsId\\":\\"${meshMailboxReportsId}\\"*"`, - `$.details.event_detail = "*\\"senderId\\":\\"${senderId}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toBeGreaterThanOrEqual(1); - - const parsedEvents = eventLogEntry.map((entry: any) => - JSON.parse(entry.details.event_detail), - ); + const detail = await expectEventOnTestObserverQueue( + REPORTING_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.reporting.report.sent.v1', + (d) => + (d.data as any).meshMailboxReportsId === meshMailboxReportsId && + (d.data as any).senderId === senderId, + 120_000, + ); - for (const event of parsedEvents) { - const { sentMeshMessageId } = event.data; - expect(sentMeshMessageId).toBeTruthy(); - // Mock MESH uses NON_PII_S3_BUCKET_NAME bucket, the object key is the sentMeshMessageId. - const storedMessage = await downloadFromS3( - NON_PII_S3_BUCKET_NAME, - `mock-mesh/mock-mailbox/out/${trustMeshMailboxReportsId}/${sentMeshMessageId}`, - ); + const { sentMeshMessageId } = detail.data as any; + expect(sentMeshMessageId).toBeTruthy(); + // Mock MESH uses NON_PII_S3_BUCKET_NAME bucket, the object key is the sentMeshMessageId. + const storedMessage = await downloadFromS3( + NON_PII_S3_BUCKET_NAME, + `mock-mesh/mock-mailbox/out/${trustMeshMailboxReportsId}/${sentMeshMessageId}`, + ); - expect(storedMessage.body).toContain(messageContent); - } - }, 120_000); + expect(storedMessage.body).toContain(messageContent); } test('should send a ReportSent event following a successful reportGenerated event', async () => { @@ -95,9 +86,7 @@ test.describe('Digital Letters - Send reports to Trust', () => { await uploadToS3(messageContent, REPORTING_S3_BUCKET_NAME, reportKey); await publishReportGeneratedEvent(reportKey); - await expectToPassEventually(async () => { - await expectReportSentEventAndMeshMessageSent(trustMeshMailboxReportsId); - }, 120_000); + await expectReportSentEventAndMeshMessageSent(trustMeshMailboxReportsId); }); test('should send message to report-sender DLQ when file does not exists', async () => { diff --git a/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts b/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts index b1a30c109..a39fefe47 100644 --- a/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts @@ -2,7 +2,6 @@ import { expect, test } from '@playwright/test'; import { CREATE_TTL_DLQ_NAME, CREATE_TTL_LAMBDA_LOG_GROUP_NAME, - ENV, } from 'constants/backend-constants'; import { SENDER_ID_SKIPS_NOTIFY, @@ -17,6 +16,10 @@ import { getTtl } from 'helpers/dynamodb-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers'; +import { + QUEUE_ITEMS_OBSERVER_QUEUE_URL, + expectEventOnTestObserverQueue, +} from 'helpers/test-observer-helpers'; import { v4 as uuidv4 } from 'uuid'; test.describe('Digital Letters - Create TTL', () => { @@ -50,6 +53,7 @@ test.describe('Digital Letters - Create TTL', () => { }; test('should create TTL and publish item enqueued event following message downloaded event', async () => { + test.setTimeout(110_000); // 30s TTL check + 60s event + 20s buffer const letterId = uuidv4(); const messageUri = `https://example.com/ttl/resource/${letterId}`; const messageReference = letterId; @@ -80,21 +84,19 @@ test.describe('Digital Letters - Create TTL', () => { }); // Verify item enqueued event published - 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.queue.item.enqueued.v1"', - `$.details.event_detail = "*\\"messageUri\\":\\"${messageUri}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }); + await expectEventOnTestObserverQueue( + QUEUE_ITEMS_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.queue.item.enqueued.v1', + (detail) => { + const { data } = detail as { data: { messageUri: string } }; + return data.messageUri === messageUri; + }, + 80_000, + ); }); test('should create TTL and publish item enqueued event following message downloaded event - direct to print', async () => { + test.setTimeout(110_000); // 30s TTL check + 60s event + 20s buffer const letterId = uuidv4(); const messageUri = `https://example.com/ttl/resource/${letterId}`; const messageReference = letterId; @@ -123,18 +125,15 @@ test.describe('Digital Letters - Create TTL', () => { }); // Verify item enqueued event published - 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.queue.item.enqueued.v1"', - `$.details.event_detail = "*\\"messageUri\\":\\"${messageUri}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }); + await expectEventOnTestObserverQueue( + QUEUE_ITEMS_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.queue.item.enqueued.v1', + (detail) => { + const { data } = detail as { data: { messageUri: string } }; + return data.messageUri === messageUri; + }, + 80_000, + ); }); test('should send invalid event to dlq', async () => { @@ -159,21 +158,7 @@ test.describe('Digital Letters - Create TTL', () => { () => true, ); - await Promise.all([ - expectToPassEventually(async () => { - const eventLogEntry = await getLogsFromCloudwatch( - CREATE_TTL_LAMBDA_LOG_GROUP_NAME, - [ - '$.message.description = "Error parsing MESHInboxMessageDownloaded event"', - `$.message.err[0].params.additionalProperty = "${unexpectedField}"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }, 150), - - expectMessageContainingString(CREATE_TTL_DLQ_NAME, letterId, 150), - ]); + expectMessageContainingString(CREATE_TTL_DLQ_NAME, letterId, 150); }); test('should send events from unknown sender to dlq', async () => { diff --git a/tests/playwright/digital-letters-component-tests/ttl-handle.component.spec.ts b/tests/playwright/digital-letters-component-tests/ttl-handle.component.spec.ts index d57d71f8a..e3c6e4fa4 100644 --- a/tests/playwright/digital-letters-component-tests/ttl-handle.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/ttl-handle.component.spec.ts @@ -1,10 +1,17 @@ import { expect, test } from '@playwright/test'; -import { ENV, HANDLE_TTL_DLQ_NAME } from 'constants/backend-constants'; +import { + HANDLE_TTL_DLQ_NAME, + TTL_HANDLE_EXPIRY_LAMBDA_LOG_GROUP_NAME, +} from 'constants/backend-constants'; import { MESHInboxMessageDownloaded } from 'digital-letters-events'; import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import { deleteTtl, putTtl } from 'helpers/dynamodb-helpers'; import expectToPassEventually from 'helpers/expectations'; import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers'; +import { + QUEUE_ITEMS_OBSERVER_QUEUE_URL, + expectEventOnTestObserverQueue, +} from 'helpers/test-observer-helpers'; import { v4 as uuidv4 } from 'uuid'; test.describe('Digital Letters - Handle TTL', () => { @@ -70,7 +77,7 @@ test.describe('Digital Letters - Handle TTL', () => { await expectToPassEventually(async () => { const eventLogEntry = await getLogsFromCloudwatch( - `/aws/lambda/nhs-${ENV}-dl-ttl-handle-expiry`, + TTL_HANDLE_EXPIRY_LAMBDA_LOG_GROUP_NAME, [ `$.message.messageUri = "${messageUri}"`, '$.message.description = "ItemDequeued event not sent as item withdrawn"', @@ -78,7 +85,7 @@ test.describe('Digital Letters - Handle TTL', () => { ); expect(eventLogEntry.length).toEqual(1); - }); + }, 120_000); }); test('should handle expired item', async () => { @@ -111,18 +118,15 @@ test.describe('Digital Letters - Handle TTL', () => { const deleteResponseCode = await deleteTtl(senderId, messageReference); expect(deleteResponseCode).toBe(200); - 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.queue.item.dequeued.v1"', - `$.details.event_detail = "*\\"messageUri\\":\\"${messageUri}\\"*"`, - ], - ); - - expect(eventLogEntry.length).toEqual(1); - }); + await expectEventOnTestObserverQueue( + QUEUE_ITEMS_OBSERVER_QUEUE_URL, + 'uk.nhs.notify.digital.letters.queue.item.dequeued.v1', + (detail) => { + const { data } = detail as { data: { messageUri: string } }; + return data.messageUri === messageUri; + }, + 120_000, + ); }); test('should send invalid item to dlq', async () => { diff --git a/tests/playwright/helpers/expectations.ts b/tests/playwright/helpers/expectations.ts index 2f171fa72..fbbda6384 100644 --- a/tests/playwright/helpers/expectations.ts +++ b/tests/playwright/helpers/expectations.ts @@ -36,7 +36,7 @@ test.afterEach(async () => { */ async function expectToPassEventually( expectationFunction: () => Promise, - timeout = 30, + timeout = 60, delay = 1, ): Promise { const invocationToken = Symbol('invocationToken'); diff --git a/tests/playwright/helpers/sqs-helpers.ts b/tests/playwright/helpers/sqs-helpers.ts index ec9a7a4dc..9e0103bb5 100644 --- a/tests/playwright/helpers/sqs-helpers.ts +++ b/tests/playwright/helpers/sqs-helpers.ts @@ -15,13 +15,13 @@ function getQueueUrl(queueName: string) { export async function expectMessageContainingString( queueName: string, searchTerm: string, - timeout = 30, + timeout = 60, ) { const input: ReceiveMessageCommandInput = { QueueUrl: getQueueUrl(queueName), MaxNumberOfMessages: 10, WaitTimeSeconds: 1, - VisibilityTimeout: 2, + VisibilityTimeout: 5, }; await expectToPassEventually(async () => { diff --git a/tests/playwright/helpers/test-observer-helpers.ts b/tests/playwright/helpers/test-observer-helpers.ts new file mode 100644 index 000000000..f000546ba --- /dev/null +++ b/tests/playwright/helpers/test-observer-helpers.ts @@ -0,0 +1,85 @@ +import { + ChangeMessageVisibilityCommand, + DeleteMessageCommand, + ReceiveMessageCommand, +} from '@aws-sdk/client-sqs'; +import { + SQS_URL_PREFIX, + TEST_OBSERVER_MESH_QUEUE_NAME, + TEST_OBSERVER_MESSAGES_QUEUE_NAME, + TEST_OBSERVER_PDM_QUEUE_NAME, + TEST_OBSERVER_PRINT_QUEUE_NAME, + TEST_OBSERVER_QUEUE_ITEMS_QUEUE_NAME, + TEST_OBSERVER_REPORTING_QUEUE_NAME, +} from 'constants/backend-constants'; +import { sqsClient } from 'utils'; + +export const MESH_OBSERVER_QUEUE_URL = `${SQS_URL_PREFIX}${TEST_OBSERVER_MESH_QUEUE_NAME}`; +export const PDM_OBSERVER_QUEUE_URL = `${SQS_URL_PREFIX}${TEST_OBSERVER_PDM_QUEUE_NAME}`; +export const MESSAGES_OBSERVER_QUEUE_URL = `${SQS_URL_PREFIX}${TEST_OBSERVER_MESSAGES_QUEUE_NAME}`; +export const PRINT_OBSERVER_QUEUE_URL = `${SQS_URL_PREFIX}${TEST_OBSERVER_PRINT_QUEUE_NAME}`; +export const QUEUE_ITEMS_OBSERVER_QUEUE_URL = `${SQS_URL_PREFIX}${TEST_OBSERVER_QUEUE_ITEMS_QUEUE_NAME}`; +export const REPORTING_OBSERVER_QUEUE_URL = `${SQS_URL_PREFIX}${TEST_OBSERVER_REPORTING_QUEUE_NAME}`; + +/** + * Polls a test observer SQS queue for an event matching the given type and predicate. + * Deletes the matched message from the queue and returns the event detail. + * + * Each queue subscribes to a filtered subset of uk.nhs.notify.digital.letters.* events + * Unmatched messages are immediately returned (VisibilityTimeout: 0) so concurrent + * tests within the same spec are not starved. + */ +export async function expectEventOnTestObserverQueue( + queueUrl: string, + eventType: string, + matchFn: (detail: Record) => boolean, + timeoutMs = 80_000, +): Promise> { + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + const { Messages = [] } = await sqsClient.send( + new ReceiveMessageCommand({ + QueueUrl: queueUrl, + MaxNumberOfMessages: 10, + WaitTimeSeconds: 10, + VisibilityTimeout: 30, + }), + ); + + for (const msg of Messages) { + if (msg.Body) { + const envelope = JSON.parse(msg.Body) as Record; + const detailType = envelope['detail-type'] as string | undefined; + const detail = envelope.detail as Record | undefined; + + if (detailType === eventType && detail && matchFn(detail)) { + await sqsClient.send( + new DeleteMessageCommand({ + QueueUrl: queueUrl, + ReceiptHandle: msg.ReceiptHandle!, + }), + ); + return detail; + } + + // Immediately return unmatched messages so concurrent tests are not starved + try { + await sqsClient.send( + new ChangeMessageVisibilityCommand({ + QueueUrl: queueUrl, + ReceiptHandle: msg.ReceiptHandle!, + VisibilityTimeout: 0, + }), + ); + } catch { + // Receipt handle already expired. SQS returns the message automatically + } + } + } + } + + throw new Error( + `Event of type "${eventType}" not found on test observer queue within ${timeoutMs}ms`, + ); +} diff --git a/tests/playwright/package.json b/tests/playwright/package.json index 7ebede52e..a8367ec76 100644 --- a/tests/playwright/package.json +++ b/tests/playwright/package.json @@ -26,7 +26,9 @@ "scripts": { "lint": "eslint .", "lint:fix": "npm run lint -- --fix", - "test:component": "playwright test -c config/component/component.config.ts", + "test:component": "playwright test -c config/component/component.config.ts --project component", + "test:component:setup": "playwright test -c config/component/component.config.ts --project senders:setup --project component:setup", + "test:component:teardown": "playwright test -c config/component/component.config.ts --project component:teardown", "test:unit": "echo \"Unit tests not required\"", "typecheck": "tsc --noEmit" },