From 5bcc7d74c7ec0fda6be90b8cf352d3215336607c Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Wed, 1 Oct 2025 14:03:38 +0100 Subject: [PATCH 1/6] NI-117 - Incorporating xmldom and update message class for processMessage Sonarcloud stuff sonarcloud removing file Initial work to support /v2 endpoint Sonarcloud fixes v2 endpoint unit tests WIP | broken WIP WIP, fixed older tests, new test coverage required sonarcloud fixes fixing depedencies for security reverting debug settings sonarcloud message model tests Unit test coverage for message model, and adding in instructions field for v2 additional unit tests to cover processMessage sonarcloud refactoring some unit tests for getmessage and getMessagesAtom returning some basic tests for getmessage and getmessages lambda functions alligning localstack request-templates with aws remote package update2 some review fixes couple of bits missed for node v22 fixing node v22.x in package security audit fix updating node v22 in github actions --- .github/workflows/ci.yml | 2 +- .labrc.js | 2 +- .nvmrc | 2 +- capAlert.json | 3 - docker/dev-tools.yml | 2 +- docker/scripts/load-dummy-data.sh | 2 +- docker/scripts/register-api-gateway.sh | 47 +- docker/scripts/register-lambda-functions.sh | 19 +- lib/functions/getMessage.js | 46 +- lib/functions/getMessagesAtom.js | 68 +- lib/functions/processMessage.js | 84 +- lib/functions/v2/getMessage.js | 5 + lib/functions/v2/getMessagesAtom.js | 5 + lib/helpers/message.js | 47 + lib/helpers/messages.js | 66 + lib/models/message.js | 158 ++- lib/models/v2MessageMapping.js | 78 ++ package-lock.json | 1085 +++++++++-------- package.json | 24 +- readme.md | 2 +- test/lib/functions/data/capAlert.json | 3 - test/lib/functions/data/capMessageId.json | 5 - test/lib/functions/data/capUpdate.json | 3 - test/lib/functions/data/nws-alert.xml | 34 + test/lib/functions/getMessage.js | 162 +-- .../lib/functions/getMessageAtomValidation.js | 4 +- test/lib/functions/getMessagesAtom.js | 81 +- test/lib/functions/processMessage.js | 385 +++--- .../lib/functions/processMessageValidation.js | 51 +- test/lib/functions/v2/getMessage.js | 46 + test/lib/functions/v2/getMessagesAtom.js | 44 + test/lib/helpers/message.js | 335 +++++ test/lib/helpers/messages.js | 280 +++++ test/lib/models/message.js | 258 +++- 34 files changed, 2321 insertions(+), 1117 deletions(-) delete mode 100644 capAlert.json create mode 100644 lib/functions/v2/getMessage.js create mode 100644 lib/functions/v2/getMessagesAtom.js create mode 100644 lib/helpers/message.js create mode 100644 lib/helpers/messages.js create mode 100644 lib/models/v2MessageMapping.js delete mode 100644 test/lib/functions/data/capAlert.json delete mode 100644 test/lib/functions/data/capMessageId.json delete mode 100644 test/lib/functions/data/capUpdate.json create mode 100644 test/lib/functions/data/nws-alert.xml create mode 100644 test/lib/functions/v2/getMessage.js create mode 100644 test/lib/functions/v2/getMessagesAtom.js create mode 100644 test/lib/helpers/message.js create mode 100644 test/lib/helpers/messages.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54c645c..cbc6b93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: - name: Install nodejs uses: actions/setup-node@v4 with: - node-version: "20.x" + node-version: "22.x" - name: Install node dependencies run: npm ci diff --git a/.labrc.js b/.labrc.js index 1207a37..e835d0d 100644 --- a/.labrc.js +++ b/.labrc.js @@ -7,7 +7,7 @@ const globalsAsArray = [ '__asyncGenerator', '__asyncDelegator', '__asyncValues', '__makeTemplateObject', '__importStar', '__importDefault', '__classPrivateFieldGet', '__classPrivateFieldSet', '__classPrivateFieldIn', '__addDisposableResource', '__disposeResources', - '__rewriteRelativeImportExtension' + '__rewriteRelativeImportExtension', 'awslambda' ] const globals = globalsAsArray.toString() diff --git a/.nvmrc b/.nvmrc index 4a207c5..c6a66a6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.18.3 +v22.21.1 diff --git a/capAlert.json b/capAlert.json deleted file mode 100644 index 8e4c9b0..0000000 --- a/capAlert.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "message": "\r\n\r\n 4eb3b7350ab7aa443650fc9351f02940E\r\n www.gov.uk/environment-agency\r\n 2026-05-28T11:00:02-00:00\r\n Actual\r\n Alert\r\n Flood warning service\r\n Public\r\n \r\n en-GB\r\n Met\r\n \r\n ImmediateMinorLikely2026-05-29T11:00:02-00:00Environment AgencyArea descriptionpointsTargetAreaCode" -} \ No newline at end of file diff --git a/docker/dev-tools.yml b/docker/dev-tools.yml index 257dd64..8a8b3bb 100644 --- a/docker/dev-tools.yml +++ b/docker/dev-tools.yml @@ -27,7 +27,7 @@ services: - capxmlliquibase:/capxmldb networks: ls: - command: update + command: /bin/sh -c "lpm add postgresql && liquibase update" volumes: capxmlpgadmin: external: true diff --git a/docker/scripts/load-dummy-data.sh b/docker/scripts/load-dummy-data.sh index 3f3171a..21fd070 100755 --- a/docker/scripts/load-dummy-data.sh +++ b/docker/scripts/load-dummy-data.sh @@ -7,7 +7,7 @@ set -e # Constants BASE_GUID="4eb3b7350ab7aa443650fc9351f02940E" BASE_AREA="TESTAREA" -DATA_FILE="capAlert.json" +DATA_FILE="test/lib/functions/data/nws-alert.xml" LAMBDA_URL=http://$(awslocal apigateway get-rest-apis | jq -r ".items[0].id").execute-api.localhost.localstack.cloud:4566/local/message # Loop 10 times diff --git a/docker/scripts/register-api-gateway.sh b/docker/scripts/register-api-gateway.sh index cb43da7..51b979e 100755 --- a/docker/scripts/register-api-gateway.sh +++ b/docker/scripts/register-api-gateway.sh @@ -11,16 +11,28 @@ main() { cap_xml_rest_api_root_resource_id=$(awslocal apigateway get-resources --rest-api-id $cap_xml_rest_api_id | jq -r '.items[0].id') lambda_functions_dir="lib/functions" - for lambda_function in "$lambda_functions_dir"/*; do + #for lambda_function in "$lambda_functions_dir"/*; do + find "$lambda_functions_dir" -type f -name "*.js" | while read -r lambda_function; do + relative_path="${lambda_function#$lambda_functions_dir/}" + dir_prefix=$(dirname "$relative_path") lambda_function_name=$(basename "$lambda_function" .js) http_method=$(get_http_method $lambda_function_name) + + case "$dir_prefix" in + v[0-9]*) + lambda_function_name="${lambda_function_name}_${dir_prefix}" + ;; + *) + echo "No version prefix" + ;; + esac if [ $lambda_function_name = "archiveMessages" ]; then echo Skipping $lambda_function because it is not accessed through an API Gateway continue fi - - # Convert the Lambda function name from camel case to undersore case to call the correct API gateway registration function. + + # Convert the Lambda function name from camel case to undersore case to call the correct API gateway registration function. $(echo register_api_gateway_support_for_$lambda_function_name | sed -E "s/([a-z0-9])([A-Z])/\1_\2/g; s/([A-Z])([A-Z][a-z])/\1_\2/g" | tr "[:upper:]" "[:lower:]") echo "API Gateway support added for $lambda_function_name" @@ -47,11 +59,30 @@ register_api_gateway_support_for_get_message() { put_method_and_integration $message_resource_id } +register_api_gateway_support_for_get_message_v2() { + if [ -z "$v2_resource_id" ]; then + v2_resource_id=$(create_resource "$cap_xml_rest_api_root_resource_id" "v2") + fi + get_message_v2_resource_id=$(create_resource $v2_resource_id "message") + message_v2_resource_id=$(create_resource $get_message_v2_resource_id "{id}") + put_method_and_integration $message_v2_resource_id + return 0 +} + register_api_gateway_support_for_get_messages_atom() { get_messages_atom_resource_id=$(create_resource $cap_xml_rest_api_root_resource_id "messages.atom") put_method_and_integration $get_messages_atom_resource_id } +register_api_gateway_support_for_get_messages_atom_v2() { + if [ -z "$v2_resource_id" ]; then + v2_resource_id=$(create_resource "$cap_xml_rest_api_root_resource_id" "v2") + fi + get_messages_atom_v2_resource_id=$(create_resource $v2_resource_id "messages.atom") + put_method_and_integration $get_messages_atom_v2_resource_id + return 0 +} + register_api_gateway_support_for_process_message() { process_message_resource_id=$(create_resource $cap_xml_rest_api_root_resource_id "message") put_method_and_integration $process_message_resource_id @@ -90,7 +121,7 @@ put_integration() { # by a function. This results in some duplication. case $lambda_function_name in - getMessage) + getMessage|getMessage_v2) awslocal apigateway put-integration \ --rest-api-id $cap_xml_rest_api_id \ --resource-id $resource_id \ @@ -104,7 +135,7 @@ put_integration() { put_responses_for_get_message ;; - getMessagesAtom) + getMessagesAtom|getMessagesAtom_v2) awslocal apigateway put-integration \ --rest-api-id $cap_xml_rest_api_id \ --resource-id $resource_id \ @@ -127,7 +158,11 @@ put_integration() { --uri arn:aws:apigateway:eu-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:eu-west-2:000000000000:function:$lambda_function_name/invocations \ --passthrough-behavior WHEN_NO_TEMPLATES \ --content-handling CONVERT_TO_TEXT \ - --request-templates '{"text/html": "{\"bodyXml\": $input.json(\"$.message\")}", "text/xml": "{\"bodyXml\": $input.json(\"$.message\")}"}' + --request-templates '{ + "text/html": "{\"bodyXml\": \"$util.escapeJavaScript($input.body)\"}", + "text/xml": "{\"bodyXml\": \"$util.escapeJavaScript($input.body)\"}" + }' + put_responses_for_process_message ;; diff --git a/docker/scripts/register-lambda-functions.sh b/docker/scripts/register-lambda-functions.sh index c95f205..0b6c73a 100755 --- a/docker/scripts/register-lambda-functions.sh +++ b/docker/scripts/register-lambda-functions.sh @@ -17,9 +17,24 @@ set -- $cpx_db_username $cpx_db_password $cpx_db_name $cpx_db_host $cpx_agw_url custom_environment_variables=$(printf '%s,' "$@" | sed 's/,*$//g') # Iterate over each file in lambda_functions_dir -for lambda_function in "$lambda_functions_dir"/*; do +find "$lambda_functions_dir" -type f -name "*.js" | while read -r lambda_function; do if [ -f "$lambda_function" ]; then + relative_path="${lambda_function#$lambda_functions_dir/}" + dir_prefix=$(dirname "$relative_path") function_name=$(basename "$lambda_function" .js) + handler_path="lib/functions/$function_name.$function_name" # default + + # If the directory matches v{number}, update function name and handler path + case "$dir_prefix" in + v[0-9]*) + handler_path="lib/functions/$dir_prefix/$function_name.$function_name" + function_name="${function_name}_${dir_prefix}" + ;; + *) + echo "No version prefix" + ;; + esac + echo Registering $function_name with LocalStack awslocal lambda create-function \ @@ -28,7 +43,7 @@ for lambda_function in "$lambda_functions_dir"/*; do --runtime nodejs20.x \ --timeout $LAMBDA_TIMEOUT \ --role arn:aws:iam::000000000000:role/lambda-role \ - --handler lib/functions/$function_name.$function_name \ + --handler "$handler_path" \ --environment "Variables={$custom_environment_variables}" \ --no-cli-pager sleep 1 diff --git a/lib/functions/getMessage.js b/lib/functions/getMessage.js index 2638d41..220e16d 100644 --- a/lib/functions/getMessage.js +++ b/lib/functions/getMessage.js @@ -1,45 +1,5 @@ -'use strict' +const { getMessage } = require('../helpers/message') -const service = require('../helpers/service') -const eventSchema = require('../schemas/getMessageEventSchema') -const { validateXML } = require('xmllint-wasm') -const fs = require('fs') -const path = require('path') -const xsdSchema = fs.readFileSync(path.join(__dirname, '..', 'schemas', 'CAP-v1.2.xsd'), 'utf8') - -module.exports.getMessage = async (event) => { - const { error } = eventSchema.validate(event) - - if (error) { - throw error - } - - const ret = await service.getMessage(event.pathParameters.id) - - if (!ret || !ret.rows || !Array.isArray(ret.rows) || ret.rows.length < 1 || !ret.rows[0].getmessage) { - console.log('No message found for ' + event.pathParameters.id) - throw new Error('No message found') - } - - const validationResult = await validateXML({ - xml: [{ - fileName: 'message.xml', - contents: ret.rows[0].getmessage.alert - }], - schema: [xsdSchema] - }) - - // NI-95 log validation errors and continue processing - if (validationResult.errors?.length > 0) { - console.log('CAP get message failed validation') - console.log(JSON.stringify(validationResult.errors)) - } - - return { - statusCode: 200, - headers: { - 'content-type': 'application/xml' - }, - body: ret.rows[0].getmessage.alert - } +module.exports.getMessage = (event) => { + return getMessage(event, false) } diff --git a/lib/functions/getMessagesAtom.js b/lib/functions/getMessagesAtom.js index 7c97be0..13f6951 100644 --- a/lib/functions/getMessagesAtom.js +++ b/lib/functions/getMessagesAtom.js @@ -1,67 +1,5 @@ -'use strict' +const { messages } = require('../helpers/messages') -const service = require('../helpers/service') -const { validateXML } = require('xmllint-wasm') -const fs = require('fs') -const path = require('path') -const xsdSchema = fs.readFileSync(path.join(__dirname, '..', 'schemas', 'atom.xsd'), 'utf8') - -module.exports.getMessagesAtom = async (event) => { - const { Feed } = await import('feed') - - const ret = await service.getAllMessages() - - const feed = new Feed({ - title: 'Flood warnings for England', - generator: 'Environment Agency CAP XML flood warnings', - description: 'Flood warnings for England', - id: `${process.env.CPX_AGW_URL}/messages.atom`, - link: `${process.env.CPX_AGW_URL}/messages.atom`, - updated: new Date(), - author: { - name: 'Environment Agency', - email: 'enquiries@environment-agency.gov.uk', - link: 'https://www.gov.uk/government/organisations/environment-agency' - }, - copyright: 'Copyright, Environment Agency. Licensed under Creative Commons BY 4.0' - }) - - if (!!ret && Array.isArray(ret.rows)) { - ret.rows.forEach((item) => { - feed.addItem({ - title: item.fwis_code, - id: `${process.env.CPX_AGW_URL}/message/${item.identifier}`, - link: `${process.env.CPX_AGW_URL}/message/${item.identifier}`, - author: { - name: 'Environment Agency', - email: 'enquiries@environment-agency.gov.uk', - link: 'https://www.gov.uk/government/organisations/environment-agency' - }, - date: item.sent - }) - }) - } - - const xmlFeed = feed.atom1() - - const validationResult = await validateXML({ - xml: [{ - fileName: 'atom-feed.xml', - contents: xmlFeed - }], - schema: [xsdSchema] - }) - // NI-95 log validation errors and continue processing - if (validationResult.errors?.length > 0) { - console.log('ATOM feed failed validation') - console.log(JSON.stringify(validationResult.errors)) - } - - return { - statusCode: 200, - headers: { - 'content-type': 'application/xml' - }, - body: xmlFeed - } +module.exports.getMessagesAtom = () => { + return messages(false) } diff --git a/lib/functions/processMessage.js b/lib/functions/processMessage.js index 36f305a..b8b7cf8 100644 --- a/lib/functions/processMessage.js +++ b/lib/functions/processMessage.js @@ -11,6 +11,9 @@ const path = require('node:path') const xsdSchema = fs.readFileSync(path.join(__dirname, '..', 'schemas', 'CAP-v1.2.xsd'), 'utf8') const additionalCapMessageSchema = require('../schemas/additionalCapMessageSchema') const Message = require('../models/message') +const EA_WHO = '2.49.0.0.826.1' +const CODE = 'MCP:v2.0' +const severityV2Mapping = require('../models/v2MessageMapping') module.exports.processMessage = async (event) => { try { @@ -19,7 +22,7 @@ module.exports.processMessage = async (event) => { // parse the xml const message = new Message(event.bodyXml) - console.log(`Processing CAP message: ' + ${message.identifier} for ${message.fwisCode}`) + console.log(`Processing CAP message: ${message.identifier} for ${message.fwisCode}`) // get Last message const dbResult = await service.getLastMessage(message.fwisCode) @@ -30,18 +33,21 @@ module.exports.processMessage = async (event) => { message.status = 'Test' } - // Add in the references field and update msgtype to Update if references exist and is Alert - const references = getReferences(lastMessage, message.sender) + // Add in the references field and update msgtype to Update if references exist and is Alert (does this in message model) + const references = buildReference(lastMessage, message.sender, 'identifier', 'references') if (references) { message.references = references } - // do validation + // Generate message V2 for meteoalarm spec + const messageV2 = processMessageV2(message, lastMessage) + + // do validation against OASIS CAP xml schema and extended JOI schema const results = await Promise.allSettled([ - // Validate xml against CAP XSD schema https://eaflood.atlassian.net/browse/NI-95 validateAgainstXsdSchema(message), - // Convert xml to js object for joi extended validation https://eaflood.atlassian.net/browse/NI-113 - validateAgainstJoiSchema(message) + validateAgainstJoiSchema(message), + validateAgainstXsdSchema(messageV2), + validateAgainstJoiSchema(messageV2) ]) // Check for validation failures and throw @@ -51,7 +57,7 @@ module.exports.processMessage = async (event) => { } // store the message in database - await service.putMessage(message.putQuery()) + await service.putMessage(message.putQuery(message, messageV2)) console.log(`Finished processing CAP message: ${message.identifier} for ${message.fwisCode}`) return { @@ -74,7 +80,7 @@ module.exports.processMessage = async (event) => { } const processFailedMessage = async (originalError, xmlResult) => { - // For backwards compapibility, only send a notification if an AWS SNS topic + // For backwards compatibility, only send a notification if an AWS SNS topic // is configured. if (process.env.CPX_SNS_TOPIC) { try { @@ -98,13 +104,12 @@ const processFailedMessage = async (originalError, xmlResult) => { } } -const getReferences = (lastMessage, sender) => { +const buildReference = (lastMessage, sender, idField, refField) => { if (lastMessage && lastMessage.expires > new Date()) { - const newReference = `${sender},${lastMessage.identifier},${moment(lastMessage.sent).utc().format('YYYY-MM-DDTHH:mm:ssZ')}` - return lastMessage.references ? `${lastMessage.references} ${newReference}` : newReference - } else { - return '' + const newReference = `${sender},${lastMessage[idField]},${moment(lastMessage.sent).utc().format('YYYY-MM-DDTHH:mm:ssZ')}` + return lastMessage[refField] ? `${lastMessage[refField]} ${newReference}` : newReference } + return '' } const validateAgainstXsdSchema = async (message) => { @@ -131,6 +136,55 @@ const validateAgainstJoiSchema = async (message) => { const joiValidation = additionalCapMessageSchema.validate(jsMessage, { abortEarly: false }) if (joiValidation.error) { - throw joiValidation.error.details ?? [joiValidation.error] + throw joiValidation.error?.details + } +} + +const formatDate = (isoString) => { + const date = new Date(isoString) + const pad = n => n.toString().padStart(2, '0') + + const YYYY = date.getUTCFullYear() + const MM = pad(date.getUTCMonth() + 1) + const DD = pad(date.getUTCDate()) + const HH = pad(date.getUTCHours()) + const mm = pad(date.getUTCMinutes()) + const SS = pad(date.getUTCSeconds()) + + return `${YYYY}${MM}${DD}${HH}${mm}${SS}` +} + +// Generates a new message based on the Meteoalarm specification https://eaflood.atlassian.net/browse/NI-121 +const processMessageV2 = (message, lastMessage) => { + const messageV2 = new Message(message.toString()) + messageV2.identifier = message.sent && message.identifier ? `${EA_WHO}.${formatDate(message.sent)}.${message.identifier}` : '' + messageV2.code = CODE + // Add in the references field and update msgtype to Update if references exist and is Alert (does this in message model) + const referencesV2 = buildReference(lastMessage, message.sender, 'identifier_v2', 'references_v2') + if (referencesV2) { + messageV2.references = referencesV2 } + messageV2.event = `${severityV2Mapping[message.severity]?.description}: ${messageV2.areaDesc}` + messageV2.severity = severityV2Mapping[message.severity]?.severity || '' + messageV2.onset = message.sent + messageV2.headline = `${severityV2Mapping[message.severity]?.headline}: ${messageV2.areaDesc}` + + let instruction = severityV2Mapping[message.severity]?.instruction + if (instruction) { + const quickdialSentence = severityV2Mapping[message.severity]?.quickdialSentence + const quickdialNumber = messageV2.quickdialNumber + // add fwisCode to instruction target area url + instruction = instruction.replace('{{ fwisCode }}', messageV2.fwisCode) + // if we have a number inject into the sentence, otherwise remove the sentence fully + instruction = instruction.replace('{{ quickdialSentence }}', quickdialNumber ? quickdialSentence.replace('{{ quickdialNumber }}', quickdialNumber) : '') + messageV2.instruction = instruction + } + + messageV2.addParameter('awareness_level', severityV2Mapping[message.severity]?.awarenessLevel) + messageV2.addParameter('awareness_type', '12; Flooding') + messageV2.addParameter('impacts', messageV2.headline) + messageV2.addParameter('use_polygon_over_geocode', 'true') + messageV2.addParameter('uk_ea_ta_code', message.fwisCode) + + return messageV2 } diff --git a/lib/functions/v2/getMessage.js b/lib/functions/v2/getMessage.js new file mode 100644 index 0000000..e384680 --- /dev/null +++ b/lib/functions/v2/getMessage.js @@ -0,0 +1,5 @@ +const { getMessage } = require('../../helpers/message') + +module.exports.getMessage = (event) => { + return getMessage(event, true) +} diff --git a/lib/functions/v2/getMessagesAtom.js b/lib/functions/v2/getMessagesAtom.js new file mode 100644 index 0000000..6b83a69 --- /dev/null +++ b/lib/functions/v2/getMessagesAtom.js @@ -0,0 +1,5 @@ +const { messages } = require('../../helpers/messages') + +module.exports.getMessagesAtom = () => { + return messages(true) +} diff --git a/lib/helpers/message.js b/lib/helpers/message.js new file mode 100644 index 0000000..9b2fe64 --- /dev/null +++ b/lib/helpers/message.js @@ -0,0 +1,47 @@ +'use strict' + +const service = require('../helpers/service') +const eventSchema = require('../schemas/getMessageEventSchema') +const { validateXML } = require('xmllint-wasm') +const fs = require('node:fs') +const path = require('node:path') +const xsdSchema = fs.readFileSync(path.join(__dirname, '..', 'schemas', 'CAP-v1.2.xsd'), 'utf8') + +module.exports.getMessage = async (event, v2) => { + const { error } = eventSchema.validate(event) + + if (error) { + throw error + } + + const ret = await service.getMessage(event.pathParameters.id) + + if (!ret?.rows || !Array.isArray(ret.rows) || ret.rows.length < 1 || !ret.rows[0].getmessage) { + console.log('No message found for ' + event.pathParameters.id) + throw new Error('No message found') + } + + const body = v2 ? ret.rows[0].getmessage.alert_v2 : ret.rows[0].getmessage.alert + + const validationResult = await validateXML({ + xml: [{ + fileName: 'message.xml', + contents: body + }], + schema: [xsdSchema] + }) + + // NI-95 log validation errors and continue processing + if (validationResult.errors?.length > 0) { + console.log('CAP get message failed validation') + console.log(JSON.stringify(validationResult.errors)) + } + + return { + statusCode: 200, + headers: { + 'content-type': 'application/xml' + }, + body + } +} diff --git a/lib/helpers/messages.js b/lib/helpers/messages.js new file mode 100644 index 0000000..a6036f4 --- /dev/null +++ b/lib/helpers/messages.js @@ -0,0 +1,66 @@ +'use strict' +const service = require('./service') +const { validateXML } = require('xmllint-wasm') +const fs = require('node:fs') +const path = require('node:path') +const xsdSchema = fs.readFileSync(path.join(__dirname, '..', 'schemas', 'atom.xsd'), 'utf8') + +module.exports.messages = async (v2 = false) => { + const { Feed } = await import('feed') + const ret = await service.getAllMessages() + const uriPrefix = v2 ? '/v2' : '' + + const feed = new Feed({ + title: 'Flood warnings for England', + generator: 'Environment Agency CAP XML flood warnings', + description: 'Flood warnings for England', + id: `${process.env.CPX_AGW_URL}${uriPrefix}/messages.atom`, + link: `${process.env.CPX_AGW_URL}${uriPrefix}/messages.atom`, + updated: new Date(), + author: { + name: 'Environment Agency', + email: 'enquiries@environment-agency.gov.uk', + link: 'https://www.gov.uk/government/organisations/environment-agency' + }, + copyright: 'Copyright, Environment Agency. Licensed under Creative Commons BY 4.0' + }) + + if (!!ret && Array.isArray(ret.rows)) { + for (const item of ret.rows) { + feed.addItem({ + title: item.fwis_code, + id: `${process.env.CPX_AGW_URL}${uriPrefix}/message/${item.identifier}`, + link: `${process.env.CPX_AGW_URL}${uriPrefix}/message/${item.identifier}`, + author: { + name: 'Environment Agency', + email: 'enquiries@environment-agency.gov.uk', + link: 'https://www.gov.uk/government/organisations/environment-agency' + }, + date: item.sent + }) + } + } + + const xmlFeed = feed.atom1() + + const validationResult = await validateXML({ + xml: [{ + fileName: 'atom-feed.xml', + contents: xmlFeed + }], + schema: [xsdSchema] + }) + // NI-95 log validation errors and continue processing + if (validationResult.errors?.length > 0) { + console.log('ATOM feed failed validation') + console.log(JSON.stringify(validationResult.errors)) + } + + return { + statusCode: 200, + headers: { + 'content-type': 'application/xml' + }, + body: xmlFeed + } +} diff --git a/lib/models/message.js b/lib/models/message.js index 2370b44..951795e 100644 --- a/lib/models/message.js +++ b/lib/models/message.js @@ -4,7 +4,7 @@ const { Sql } = require('sql-ts') const sql = new Sql('postgres') const messages = sql.define({ name: 'messages', - columns: ['identifier', 'msg_type', 'references', 'alert', 'fwis_code', 'expires', 'sent', 'created'] + columns: ['identifier', 'msg_type', 'references', 'alert', 'fwis_code', 'expires', 'sent', 'created', 'identifier_v2', 'references_v2', 'alert_v2'] }) class Message { @@ -13,19 +13,23 @@ class Message { } get fwisCode () { - return this.getFirstElement('geocode').getElementsByTagName('value')[0].textContent + return this.getFirstElement('geocode')?.getElementsByTagName('value')[0].textContent || '' } get identifier () { - return this.getFirstElement('identifier').textContent + return this.getFirstElement('identifier')?.textContent || '' + } + + set identifier (value) { + this.getFirstElement('identifier').textContent = value } get sender () { - return this.getFirstElement('sender').textContent + return this.getFirstElement('sender')?.textContent || '' } get msgType () { - return this.getFirstElement('msgType').textContent + return this.getFirstElement('msgType')?.textContent || '' } set msgType (value) { @@ -33,24 +37,23 @@ class Message { } get references () { - return this.getFirstElement('references') ? this.getFirstElement('references').textContent : '' + return this.getFirstElement('references')?.textContent || '' } set references (value) { - if (value) { - if (this.references) { - this.getFirstElement('references').textContent = value - } else { - this.addElement('scope', 'references', value) - } - if (this.msgType === 'Alert') { - this.msgType = 'Update' - } + const referencesEl = this.getFirstElement('references') + if (referencesEl) { + referencesEl.textContent = value + } else { + this.addElement('scope', 'references', value) + } + if (this.msgType === 'Alert') { + this.msgType = 'Update' } } get status () { - return this.getFirstElement('status').textContent + return this.getFirstElement('status')?.textContent || '' } set status (value) { @@ -58,11 +61,91 @@ class Message { } get expires () { - return this.getFirstElement('expires').textContent + return this.getFirstElement('expires')?.textContent || '' } get sent () { - return this.getFirstElement('sent').textContent + return this.getFirstElement('sent')?.textContent || '' + } + + get code () { + return this.getFirstElement('code')?.textContent || '' + } + + set code (value) { + const codeEl = this.getFirstElement('code') + if (codeEl) { + codeEl.textContent = value + } else { + this.addElement('scope', 'code', value) + } + } + + get event () { + return this.getFirstElement('event')?.textContent || '' + } + + set event (value) { + this.getFirstElement('event').textContent = value + } + + get severity () { + return this.getFirstElement('severity')?.textContent || '' + } + + set severity (value) { + this.getFirstElement('severity').textContent = value + } + + get onset () { + return this.getFirstElement('onset')?.textContent || '' + } + + set onset (value) { + const onsetEl = this.getFirstElement('onset') + if (onsetEl) { + onsetEl.textContent = value + } else { + this.addElement('certainty', 'onset', value) + } + } + + get headline () { + return this.getFirstElement('headline')?.textContent || '' + } + + set headline (value) { + const headlineEl = this.getFirstElement('headline') + if (headlineEl) { + headlineEl.textContent = value + } else { + this.addElement('senderName', 'headline', value) + } + } + + get areaDesc () { + return this.getFirstElement('areaDesc')?.textContent || '' + } + + get quickdialNumber () { + return this.getFirstElement('instruction')?.textContent.match(/quickdial code:\s*(\d{6})\./i)?.[1] || '' + } + + get instruction () { + return this.getFirstElement('instruction')?.textContent || '' + } + + set instruction (value) { + const instruction = this.doc.getElementsByTagName('instruction')[0] + const newCData = this.doc.createCDATASection(value) + if (instruction) { + while (instruction.firstChild) { + instruction.removeChild(instruction.firstChild) + } + instruction.appendChild(newCData) + } else { + this.addElement('description', 'instruction', '').appendChild(newCData) + } } getFirstElement (tagName) { @@ -76,20 +159,41 @@ class Message { return parentEl.parentNode.insertBefore(newEl, parentEl.nextSibling) } + addParameter (name, value) { + const infoEl = this.doc.getElementsByTagName('info')[0] + const areaEl = infoEl.getElementsByTagName('area')[0] + const parameterEl = this.doc.createElement('parameter') + const valueNameEl = this.doc.createElement('valueName') + const valueEl = this.doc.createElement('value') + valueNameEl.textContent = name + valueEl.textContent = value + parameterEl.appendChild(valueNameEl) + parameterEl.appendChild(valueEl) + if (areaEl) { + return infoEl.insertBefore(parameterEl, areaEl) + } else { + return infoEl.appendChild(parameterEl) + } + } + toString () { return xmlFormat(new xmldom.XMLSerializer().serializeToString(this.doc), { indentation: ' ', collapseContent: true }) } - putQuery () { + // Handles multiple message versions to create the single database record + putQuery (messageV1, messageV2) { const message = { - identifier: this.identifier, - msg_type: this.msgType, - references: this.references, - alert: this.toString(), - fwis_code: this.fwisCode, - expires: this.expires, - sent: this.sent, - created: new Date().toISOString() + identifier: messageV1.identifier, + msg_type: messageV1.msgType, + references: messageV1.references, + alert: messageV1.toString(), + fwis_code: messageV1.fwisCode, + expires: messageV1.expires, + sent: messageV1.sent, + created: new Date().toISOString(), + identifier_v2: messageV2.identifier, + references_v2: messageV2.references, + alert_v2: messageV2.toString() } return messages.insert(message).toQuery() } diff --git a/lib/models/v2MessageMapping.js b/lib/models/v2MessageMapping.js new file mode 100644 index 0000000..0bb80c5 --- /dev/null +++ b/lib/models/v2MessageMapping.js @@ -0,0 +1,78 @@ +const quickdialSentence = '- call Floodline on 0345 988 1188, using quickdial code {{ quickdialNumber }}' + +module.exports = { + Minor: { + severity: 'Minor', + description: 'Flood Alert', + headline: 'Flooding is possible', + instruction: `Be prepared + +You should: + +- go to Check for flooding for a map of the area and to monitor up-to-date local flood information – https://check-for-flooding.service.gov.uk/target-area/{{ fwisCode }} +- get ready to act on your personal flood plan if you have one - https://www.gov.uk/government/publications/personal-flood-plan +- follow the guidance in 'What to do before or during a flood' - https://www.gov.uk/help-during-flood + +You can also read more about what flood alerts are – [https://www.gov.uk/guidance/flood-alerts-and-warnings-what-they-are-and-what-to-do#flood-alert] + +Stay up to date + +To get the latest flood information, you can: + +- go to Check for flooding +- monitor local weather, news and travel updates +{{ quickdialSentence }}`, + awarenessLevel: '1; green; Minor', + quickdialSentence + }, + Moderate: { + severity: 'Severe', + description: 'Flood Warning', + headline: 'Flooding is expected', + instruction: `Act now + +You should: + +- go to Check for flooding for a map of the area and to monitor up-to-date local flood information – https://check-for-flooding.service.gov.uk/target-area/{{ fwisCode }} +- act on your personal flood plan if you have one - https://www.gov.uk/government/publications/personal-flood-plan +- follow the guidance in 'What to do before or during a flood' - https://www.gov.uk/help-during-flood + +You can also read more about what flood warnings are – [https://www.gov.uk/guidance/flood-alerts-and-warnings-what-they-are-and-what-to-do#flood-warning] + +Stay up to date + +To get the latest flood information, you can: + +- go to Check for flooding +- monitor local weather, news and travel updates +{{ quickdialSentence }}`, + awarenessLevel: '3; orange; Severe', + quickdialSentence + }, + Severe: { + severity: 'Extreme', + description: 'Severe Flood Warning', + headline: 'Danger to life', + instruction: `Act now - danger to life + +You should: + +- call 999 if you are in immediate danger + +- go to Check for flooding for a map of the area and to monitor up-to-date local flood information – https://check-for-flooding.service.gov.uk/target-area/{{ fwisCode }} +- act on your personal flood plan if you have one - https://www.gov.uk/government/publications/personal-flood-plan +- follow the guidance in 'What to do before or during a flood' - https://www.gov.uk/help-during-flood + +You can also read more about what severe flood warnings are – [https://www.gov.uk/guidance/flood-alerts-and-warnings-what-they-are-and-what-to-do#severe-flood-warning] + +Stay up to date + +To get the latest flood information, you can: + +- go to Check for flooding +- monitor local weather, news and travel updates +{{ quickdialSentence }}`, + awarenessLevel: '4; red; Extreme', + quickdialSentence + } +} diff --git a/package-lock.json b/package-lock.json index 56552d8..441dd30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,30 +6,30 @@ "packages": { "": { "name": "cap-xml", - "version": "2.1.0", + "version": "3.0.0", "license": "OGL", "dependencies": { - "@aws-sdk/client-sns": "^3.873.0", - "@xmldom/xmldom": "^0.8.11", + "@aws-sdk/client-sns": "3.932.0", + "@xmldom/xmldom": "0.8.11", "feed": "5.1.0", - "joi": "^18.0.1", - "moment": "^2.30.1", + "joi": "18.0.1", + "moment": "2.30.1", "pg": "8.16.3", "sql-ts": "7.1.0", - "xml-formatter": "^3.6.7", + "xml-formatter": "3.6.7", "xml2js": "0.6.2", - "xmllint-wasm": "^5.0.0" + "xmllint-wasm": "5.1.0" }, "devDependencies": { - "@hapi/code": "^9.0.3", - "@hapi/lab": "^26.0.0", - "aws-sdk-client-mock": "^4.1.0", - "proxyquire": "^2.1.3", - "sinon": "^21.0.0", + "@hapi/code": "9.0.3", + "@hapi/lab": "26.0.0", + "aws-sdk-client-mock": "4.1.0", + "proxyquire": "2.1.3", + "sinon": "21.0.0", "standard": "17.1.2" }, "engines": { - "node": ">=20" + "node": "22.x" } }, "node_modules/@ampproject/remapping": { @@ -172,49 +172,49 @@ } }, "node_modules/@aws-sdk/client-sns": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sns/-/client-sns-3.873.0.tgz", - "integrity": "sha512-NuAkmtMozX1I9biFNfyGazm91lbfbmZfF43SW32lJLmEyAV/1acn2MubTh91SjmnLGqxfgc9jrplX9b/M8Mnyw==", + "version": "3.932.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sns/-/client-sns-3.932.0.tgz", + "integrity": "sha512-xkNxxViG9YOsm4DUYM8wQ8KnkAgc9yktVijbFKhIItEdlyaXl4A4sHATsOzZfhuqz/JGZnVF7gUGfBdntOtukA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.873.0", - "@aws-sdk/credential-provider-node": "3.873.0", - "@aws-sdk/middleware-host-header": "3.873.0", - "@aws-sdk/middleware-logger": "3.873.0", - "@aws-sdk/middleware-recursion-detection": "3.873.0", - "@aws-sdk/middleware-user-agent": "3.873.0", - "@aws-sdk/region-config-resolver": "3.873.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.873.0", - "@aws-sdk/util-user-agent-browser": "3.873.0", - "@aws-sdk/util-user-agent-node": "3.873.0", - "@smithy/config-resolver": "^4.1.5", - "@smithy/core": "^3.8.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/hash-node": "^4.0.5", - "@smithy/invalid-dependency": "^4.0.5", - "@smithy/middleware-content-length": "^4.0.5", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-retry": "^4.1.19", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.26", - "@smithy/util-defaults-mode-node": "^4.0.26", - "@smithy/util-endpoints": "^3.0.7", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "3.932.0", + "@aws-sdk/credential-provider-node": "3.932.0", + "@aws-sdk/middleware-host-header": "3.930.0", + "@aws-sdk/middleware-logger": "3.930.0", + "@aws-sdk/middleware-recursion-detection": "3.930.0", + "@aws-sdk/middleware-user-agent": "3.932.0", + "@aws-sdk/region-config-resolver": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@aws-sdk/util-user-agent-browser": "3.930.0", + "@aws-sdk/util-user-agent-node": "3.932.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.2", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.9", + "@smithy/middleware-retry": "^4.4.9", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.8", + "@smithy/util-defaults-mode-node": "^4.2.11", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -222,48 +222,48 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.873.0.tgz", - "integrity": "sha512-EmcrOgFODWe7IsLKFTeSXM9TlQ80/BO1MBISlr7w2ydnOaUYIiPGRRJnDpeIgMaNqT4Rr2cRN2RiMrbFO7gDdA==", + "version": "3.932.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.932.0.tgz", + "integrity": "sha512-XHqHa5iv2OQsKoM2tUQXs7EAyryploC00Wg0XSFra/KAKqyGizUb5XxXsGlyqhebB29Wqur+zwiRwNmejmN0+Q==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.873.0", - "@aws-sdk/middleware-host-header": "3.873.0", - "@aws-sdk/middleware-logger": "3.873.0", - "@aws-sdk/middleware-recursion-detection": "3.873.0", - "@aws-sdk/middleware-user-agent": "3.873.0", - "@aws-sdk/region-config-resolver": "3.873.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.873.0", - "@aws-sdk/util-user-agent-browser": "3.873.0", - "@aws-sdk/util-user-agent-node": "3.873.0", - "@smithy/config-resolver": "^4.1.5", - "@smithy/core": "^3.8.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/hash-node": "^4.0.5", - "@smithy/invalid-dependency": "^4.0.5", - "@smithy/middleware-content-length": "^4.0.5", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-retry": "^4.1.19", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.26", - "@smithy/util-defaults-mode-node": "^4.0.26", - "@smithy/util-endpoints": "^3.0.7", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "3.932.0", + "@aws-sdk/middleware-host-header": "3.930.0", + "@aws-sdk/middleware-logger": "3.930.0", + "@aws-sdk/middleware-recursion-detection": "3.930.0", + "@aws-sdk/middleware-user-agent": "3.932.0", + "@aws-sdk/region-config-resolver": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@aws-sdk/util-user-agent-browser": "3.930.0", + "@aws-sdk/util-user-agent-node": "3.932.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.2", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.9", + "@smithy/middleware-retry": "^4.4.9", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.8", + "@smithy/util-defaults-mode-node": "^4.2.11", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -271,25 +271,23 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.873.0.tgz", - "integrity": "sha512-WrROjp8X1VvmnZ4TBzwM7RF+EB3wRaY9kQJLXw+Aes0/3zRjUXvGIlseobGJMqMEGnM0YekD2F87UaVfot1xeQ==", + "version": "3.932.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.932.0.tgz", + "integrity": "sha512-AS8gypYQCbNojwgjvZGkJocC2CoEICDx9ZJ15ILsv+MlcCVLtUJSRSx3VzJOUY2EEIaGLRrPNlIqyn/9/fySvA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.862.0", - "@aws-sdk/xml-builder": "3.873.0", - "@smithy/core": "^3.8.0", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/property-provider": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/signature-v4": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-utf8": "^4.0.0", - "fast-xml-parser": "5.2.5", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.2", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -297,15 +295,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.873.0.tgz", - "integrity": "sha512-FWj1yUs45VjCADv80JlGshAttUHBL2xtTAbJcAxkkJZzLRKVkdyrepFWhv/95MvDyzfbT6PgJiWMdW65l/8ooA==", + "version": "3.932.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.932.0.tgz", + "integrity": "sha512-ozge/c7NdHUDyHqro6+P5oHt8wfKSUBN+olttiVfBe9Mw3wBMpPa3gQ0pZnG+gwBkKskBuip2bMR16tqYvUSEA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.873.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/types": "^4.3.2", + "@aws-sdk/core": "3.932.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -313,20 +311,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.873.0.tgz", - "integrity": "sha512-0sIokBlXIsndjZFUfr3Xui8W6kPC4DAeBGAXxGi9qbFZ9PWJjn1vt2COLikKH3q2snchk+AsznREZG8NW6ezSg==", + "version": "3.932.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.932.0.tgz", + "integrity": "sha512-b6N9Nnlg8JInQwzBkUq5spNaXssM3h3zLxGzpPrnw0nHSIWPJPTbZzA5Ca285fcDUFuKP+qf3qkuqlAjGOdWhg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.873.0", - "@aws-sdk/types": "3.862.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/property-provider": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/util-stream": "^4.2.4", + "@aws-sdk/core": "3.932.0", + "@aws-sdk/types": "3.930.0", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" }, "engines": { @@ -334,23 +332,23 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.873.0.tgz", - "integrity": "sha512-bQdGqh47Sk0+2S3C+N46aNQsZFzcHs7ndxYLARH/avYXf02Nl68p194eYFaAHJSQ1re5IbExU1+pbums7FJ9fA==", + "version": "3.932.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.932.0.tgz", + "integrity": "sha512-ZBjSAXVGy7danZRHCRMJQ7sBkG1Dz39thYlvTiUaf9BKZ+8ymeiFhuTeV1OkWUBBnY0ki2dVZJvboTqfINhNxA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.873.0", - "@aws-sdk/credential-provider-env": "3.873.0", - "@aws-sdk/credential-provider-http": "3.873.0", - "@aws-sdk/credential-provider-process": "3.873.0", - "@aws-sdk/credential-provider-sso": "3.873.0", - "@aws-sdk/credential-provider-web-identity": "3.873.0", - "@aws-sdk/nested-clients": "3.873.0", - "@aws-sdk/types": "3.862.0", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", + "@aws-sdk/core": "3.932.0", + "@aws-sdk/credential-provider-env": "3.932.0", + "@aws-sdk/credential-provider-http": "3.932.0", + "@aws-sdk/credential-provider-process": "3.932.0", + "@aws-sdk/credential-provider-sso": "3.932.0", + "@aws-sdk/credential-provider-web-identity": "3.932.0", + "@aws-sdk/nested-clients": "3.932.0", + "@aws-sdk/types": "3.930.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -358,22 +356,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.873.0.tgz", - "integrity": "sha512-+v/xBEB02k2ExnSDL8+1gD6UizY4Q/HaIJkNSkitFynRiiTQpVOSkCkA0iWxzksMeN8k1IHTE5gzeWpkEjNwbA==", + "version": "3.932.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.932.0.tgz", + "integrity": "sha512-SEG9t2taBT86qe3gTunfrK8BxT710GVLGepvHr+X5Pw+qW225iNRaGN0zJH+ZE/j91tcW9wOaIoWnURkhR5wIg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.873.0", - "@aws-sdk/credential-provider-http": "3.873.0", - "@aws-sdk/credential-provider-ini": "3.873.0", - "@aws-sdk/credential-provider-process": "3.873.0", - "@aws-sdk/credential-provider-sso": "3.873.0", - "@aws-sdk/credential-provider-web-identity": "3.873.0", - "@aws-sdk/types": "3.862.0", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", + "@aws-sdk/credential-provider-env": "3.932.0", + "@aws-sdk/credential-provider-http": "3.932.0", + "@aws-sdk/credential-provider-ini": "3.932.0", + "@aws-sdk/credential-provider-process": "3.932.0", + "@aws-sdk/credential-provider-sso": "3.932.0", + "@aws-sdk/credential-provider-web-identity": "3.932.0", + "@aws-sdk/types": "3.930.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -381,16 +379,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.873.0.tgz", - "integrity": "sha512-ycFv9WN+UJF7bK/ElBq1ugWA4NMbYS//1K55bPQZb2XUpAM2TWFlEjG7DIyOhLNTdl6+CbHlCdhlKQuDGgmm0A==", + "version": "3.932.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.932.0.tgz", + "integrity": "sha512-BodZYKvT4p/Dkm28Ql/FhDdS1+p51bcZeMMu2TRtU8PoMDHnVDhHz27zASEKSZwmhvquxHrZHB0IGuVqjZUtSQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.873.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", + "@aws-sdk/core": "3.932.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -398,18 +396,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.873.0.tgz", - "integrity": "sha512-SudkAOZmjEEYgUrqlUUjvrtbWJeI54/0Xo87KRxm4kfBtMqSx0TxbplNUAk8Gkg4XQNY0o7jpG8tK7r2Wc2+uw==", + "version": "3.932.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.932.0.tgz", + "integrity": "sha512-XYmkv+ltBjjmPZ6AmR1ZQZkQfD0uzG61M18/Lif3HAGxyg3dmod0aWx9aL6lj9SvxAGqzscrx5j4PkgLqjZruw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.873.0", - "@aws-sdk/core": "3.873.0", - "@aws-sdk/token-providers": "3.873.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", + "@aws-sdk/client-sso": "3.932.0", + "@aws-sdk/core": "3.932.0", + "@aws-sdk/token-providers": "3.932.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -417,16 +415,17 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.873.0.tgz", - "integrity": "sha512-Gw2H21+VkA6AgwKkBtTtlGZ45qgyRZPSKWs0kUwXVlmGOiPz61t/lBX0vG6I06ZIz2wqeTJ5OA1pWZLqw1j0JQ==", + "version": "3.932.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.932.0.tgz", + "integrity": "sha512-Yw/hYNnC1KHuVIQF9PkLXbuKN7ljx70OSbJYDRufllQvej3kRwNcqQSnzI1M4KaObccqKaE6srg22DqpPy9p8w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.873.0", - "@aws-sdk/nested-clients": "3.873.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/types": "^4.3.2", + "@aws-sdk/core": "3.932.0", + "@aws-sdk/nested-clients": "3.932.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -434,14 +433,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.873.0.tgz", - "integrity": "sha512-KZ/W1uruWtMOs7D5j3KquOxzCnV79KQW9MjJFZM/M0l6KI8J6V3718MXxFHsTjUE4fpdV6SeCNLV1lwGygsjJA==", + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.930.0.tgz", + "integrity": "sha512-x30jmm3TLu7b/b+67nMyoV0NlbnCVT5DI57yDrhXAPCtdgM1KtdLWt45UcHpKOm1JsaIkmYRh2WYu7Anx4MG0g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", + "@aws-sdk/types": "3.930.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -449,13 +448,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.873.0.tgz", - "integrity": "sha512-QhNZ8X7pW68kFez9QxUSN65Um0Feo18ZmHxszQZNUhKDsXew/EG9NPQE/HgYcekcon35zHxC4xs+FeNuPurP2g==", + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.930.0.tgz", + "integrity": "sha512-vh4JBWzMCBW8wREvAwoSqB2geKsZwSHTa0nSt0OMOLp2PdTYIZDi0ZiVMmpfnjcx9XbS6aSluLv9sKx4RrG46A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/types": "^4.3.2", + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -463,14 +462,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.873.0.tgz", - "integrity": "sha512-OtgY8EXOzRdEWR//WfPkA/fXl0+WwE8hq0y9iw2caNyKPtca85dzrrZWnPqyBK/cpImosrpR1iKMYr41XshsCg==", + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.930.0.tgz", + "integrity": "sha512-gv0sekNpa2MBsIhm2cjP3nmYSfI4nscx/+K9u9ybrWZBWUIC4kL2sV++bFjjUz4QxUIlvKByow3/a9ARQyCu7Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", + "@aws-sdk/types": "3.930.0", + "@aws/lambda-invoke-store": "^0.1.1", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -478,17 +478,17 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.873.0.tgz", - "integrity": "sha512-gHqAMYpWkPhZLwqB3Yj83JKdL2Vsb64sryo8LN2UdpElpS+0fT4yjqSxKTfp7gkhN6TCIxF24HQgbPk5FMYJWw==", + "version": "3.932.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.932.0.tgz", + "integrity": "sha512-9BGTbJyA/4PTdwQWE9hAFIJGpsYkyEW20WON3i15aDqo5oRZwZmqaVageOD57YYqG8JDJjvcwKyDdR4cc38dvg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.873.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.873.0", - "@smithy/core": "^3.8.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", + "@aws-sdk/core": "3.932.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@smithy/core": "^3.18.2", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -496,48 +496,48 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.873.0.tgz", - "integrity": "sha512-yg8JkRHuH/xO65rtmLOWcd9XQhxX1kAonp2CliXT44eA/23OBds6XoheY44eZeHfCTgutDLTYitvy3k9fQY6ZA==", + "version": "3.932.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.932.0.tgz", + "integrity": "sha512-E2ucBfiXSpxZflHTf3UFbVwao4+7v7ctAeg8SWuglc1UMqMlpwMFFgWiSONtsf0SR3+ZDoWGATyCXOfDWerJuw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.873.0", - "@aws-sdk/middleware-host-header": "3.873.0", - "@aws-sdk/middleware-logger": "3.873.0", - "@aws-sdk/middleware-recursion-detection": "3.873.0", - "@aws-sdk/middleware-user-agent": "3.873.0", - "@aws-sdk/region-config-resolver": "3.873.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.873.0", - "@aws-sdk/util-user-agent-browser": "3.873.0", - "@aws-sdk/util-user-agent-node": "3.873.0", - "@smithy/config-resolver": "^4.1.5", - "@smithy/core": "^3.8.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/hash-node": "^4.0.5", - "@smithy/invalid-dependency": "^4.0.5", - "@smithy/middleware-content-length": "^4.0.5", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-retry": "^4.1.19", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.26", - "@smithy/util-defaults-mode-node": "^4.0.26", - "@smithy/util-endpoints": "^3.0.7", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "3.932.0", + "@aws-sdk/middleware-host-header": "3.930.0", + "@aws-sdk/middleware-logger": "3.930.0", + "@aws-sdk/middleware-recursion-detection": "3.930.0", + "@aws-sdk/middleware-user-agent": "3.932.0", + "@aws-sdk/region-config-resolver": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@aws-sdk/util-user-agent-browser": "3.930.0", + "@aws-sdk/util-user-agent-node": "3.932.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.2", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.9", + "@smithy/middleware-retry": "^4.4.9", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.8", + "@smithy/util-defaults-mode-node": "^4.2.11", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -545,16 +545,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.873.0.tgz", - "integrity": "sha512-q9sPoef+BBG6PJnc4x60vK/bfVwvRWsPgcoQyIra057S/QGjq5VkjvNk6H8xedf6vnKlXNBwq9BaANBXnldUJg==", + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.930.0.tgz", + "integrity": "sha512-KL2JZqH6aYeQssu1g1KuWsReupdfOoxD6f1as2VC+rdwYFUu4LfzMsFfXnBvvQWWqQ7rZHWOw1T+o5gJmg7Dzw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", + "@aws-sdk/types": "3.930.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -562,17 +561,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.873.0.tgz", - "integrity": "sha512-BWOCeFeV/Ba8fVhtwUw/0Hz4wMm9fjXnMb4Z2a5he/jFlz5mt1/rr6IQ4MyKgzOaz24YrvqsJW2a0VUKOaYDvg==", + "version": "3.932.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.932.0.tgz", + "integrity": "sha512-43u82ulVuHK4zWhcSPyuPS18l0LNHi3QJQ1YtP2MfP8bPf5a6hMYp5e3lUr9oTDEWcpwBYtOW0m1DVmoU/3veA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.873.0", - "@aws-sdk/nested-clients": "3.873.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", + "@aws-sdk/core": "3.932.0", + "@aws-sdk/nested-clients": "3.932.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -580,12 +579,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz", - "integrity": "sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==", + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.930.0.tgz", + "integrity": "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -593,15 +592,15 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.873.0.tgz", - "integrity": "sha512-YByHrhjxYdjKRf/RQygRK1uh0As1FIi9+jXTcIEX/rBgN8mUByczr2u4QXBzw7ZdbdcOBMOkPnLRjNOWW1MkFg==", + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.930.0.tgz", + "integrity": "sha512-M2oEKBzzNAYr136RRc6uqw3aWlwCxqTP1Lawps9E1d2abRPvl1p1ztQmmXp1Ak4rv8eByIZ+yQyKQ3zPdRG5dw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-endpoints": "^3.0.7", + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-endpoints": "^3.2.5", "tslib": "^2.6.2" }, "engines": { @@ -609,9 +608,9 @@ } }, "node_modules/@aws-sdk/util-locate-window": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.873.0.tgz", - "integrity": "sha512-xcVhZF6svjM5Rj89T1WzkjQmrTF6dpR2UvIHPMTnSZoNe6CixejPZ6f0JJ2kAhO8H+dUHwNBlsUgOTIKiK/Syg==", + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -621,27 +620,27 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.873.0.tgz", - "integrity": "sha512-AcRdbK6o19yehEcywI43blIBhOCSo6UgyWcuOJX5CFF8k39xm1ILCjQlRRjchLAxWrm0lU0Q7XV90RiMMFMZtA==", + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.930.0.tgz", + "integrity": "sha512-q6lCRm6UAe+e1LguM5E4EqM9brQlDem4XDcQ87NzEvlTW6GzmNCO0w1jS0XgCFXQHjDxjdlNFX+5sRbHijwklg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/types": "^4.3.2", + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.873.0.tgz", - "integrity": "sha512-9MivTP+q9Sis71UxuBaIY3h5jxH0vN3/ZWGxO8ADL19S2OIfknrYSAfzE5fpoKROVBu0bS4VifHOFq4PY1zsxw==", + "version": "3.932.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.932.0.tgz", + "integrity": "sha512-/kC6cscHrZL74TrZtgiIL5jJNbVsw9duGGPurmaVgoCbP7NnxyaSWEurbNV3VPNPhNE3bV3g4Ci+odq+AlsYQg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.873.0", - "@aws-sdk/types": "3.862.0", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", + "@aws-sdk/middleware-user-agent": "3.932.0", + "@aws-sdk/types": "3.930.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -657,18 +656,28 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.873.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.873.0.tgz", - "integrity": "sha512-kLO7k7cGJ6KaHiExSJWojZurF7SnGMDHXRuQunFnEoD0n1yB6Lqy/S/zHiQ7oJnBhPr9q0TW9qFkrsZb1Uc54w==", + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", + "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.9.0", + "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", + "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1678,12 +1687,12 @@ "license": "(Unlicense OR Apache-2.0)" }, "node_modules/@smithy/abort-controller": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz", - "integrity": "sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", + "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1691,15 +1700,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.5.tgz", - "integrity": "sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", + "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" }, "engines": { @@ -1707,37 +1717,36 @@ } }, "node_modules/@smithy/core": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.8.0.tgz", - "integrity": "sha512-EYqsIYJmkR1VhVE9pccnk353xhs+lB6btdutJEtsp7R055haMJp2yE16eSxw8fv+G0WUY6vqxyYOP8kOqawxYQ==", + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.4.tgz", + "integrity": "sha512-o5tMqPZILBvvROfC8vC+dSVnWJl9a0u9ax1i1+Bq8515eYjUJqqk5XjjEsDLoeL5dSqGSh6WGdVx1eJ1E/Nwhw==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.0.9", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-stream": "^4.2.4", - "@smithy/util-utf8": "^4.0.0", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@smithy/middleware-serde": "^4.2.6", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.7.tgz", - "integrity": "sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", + "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/property-provider": "^4.0.5", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" }, "engines": { @@ -1745,15 +1754,15 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.1.tgz", - "integrity": "sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", + "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.3", - "@smithy/querystring-builder": "^4.0.5", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -1761,14 +1770,14 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.5.tgz", - "integrity": "sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", + "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/types": "^4.9.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -1776,12 +1785,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.5.tgz", - "integrity": "sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", + "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1789,9 +1798,9 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", - "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1801,13 +1810,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.5.tgz", - "integrity": "sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", + "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1815,18 +1824,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.18.tgz", - "integrity": "sha512-ZhvqcVRPZxnZlokcPaTwb+r+h4yOIOCJmx0v2d1bpVlmP465g3qpVSf7wxcq5zZdu4jb0H4yIMxuPwDJSQc3MQ==", + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.11.tgz", + "integrity": "sha512-eJXq9VJzEer1W7EQh3HY2PDJdEcEUnv6sKuNt4eVjyeNWcQFS4KmnY+CKkYOIR6tSqarn6bjjCqg1UB+8UJiPQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.8.0", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-middleware": "^4.0.5", + "@smithy/core": "^3.18.4", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" }, "engines": { @@ -1834,34 +1843,33 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.1.19", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.19.tgz", - "integrity": "sha512-X58zx/NVECjeuUB6A8HBu4bhx72EoUz+T5jTMIyeNKx2lf+Gs9TmWPNNkH+5QF0COjpInP/xSpJGJ7xEnAklQQ==", + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.11.tgz", + "integrity": "sha512-EL5OQHvFOKneJVRgzRW4lU7yidSwp/vRJOe542bHgExN3KNThr1rlg0iE4k4SnA+ohC+qlUxoK+smKeAYPzfAQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/protocol-http": "^5.1.3", - "@smithy/service-error-classification": "^4.0.7", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/smithy-client": "^4.9.7", + "@smithy/types": "^4.9.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/middleware-serde": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.9.tgz", - "integrity": "sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.6.tgz", + "integrity": "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1869,12 +1877,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.5.tgz", - "integrity": "sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", + "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1882,14 +1890,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.4.tgz", - "integrity": "sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", + "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1897,15 +1905,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.1.tgz", - "integrity": "sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", + "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/querystring-builder": "^4.0.5", - "@smithy/types": "^4.3.2", + "@smithy/abort-controller": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1913,12 +1921,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.5.tgz", - "integrity": "sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", + "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1926,12 +1934,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.3.tgz", - "integrity": "sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", + "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1939,13 +1947,13 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.5.tgz", - "integrity": "sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", + "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", - "@smithy/util-uri-escape": "^4.0.0", + "@smithy/types": "^4.9.0", + "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -1953,12 +1961,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.5.tgz", - "integrity": "sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", + "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1966,24 +1974,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", - "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", + "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2" + "@smithy/types": "^4.9.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.5.tgz", - "integrity": "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", + "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1991,18 +1999,18 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.3.tgz", - "integrity": "sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", + "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -2010,17 +2018,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.10.tgz", - "integrity": "sha512-iW6HjXqN0oPtRS0NK/zzZ4zZeGESIFcxj2FkWed3mcK8jdSdHzvnCKXSjvewESKAgGKAbJRA+OsaqKhkdYRbQQ==", + "version": "4.9.7", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.7.tgz", + "integrity": "sha512-pskaE4kg0P9xNQWihfqlTMyxyFR3CH6Sr6keHYghgyqqDXzjl2QJg5lAzuVe/LzZiOzcbcVtxKYi1/fZPt/3DA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.8.0", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "@smithy/util-stream": "^4.2.4", + "@smithy/core": "^3.18.4", + "@smithy/middleware-endpoint": "^4.3.11", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" }, "engines": { @@ -2028,9 +2036,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.2.tgz", - "integrity": "sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", + "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -2040,13 +2048,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.5.tgz", - "integrity": "sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", + "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.0.5", - "@smithy/types": "^4.3.2", + "@smithy/querystring-parser": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -2054,13 +2062,13 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", - "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -2068,9 +2076,9 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", - "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -2080,9 +2088,9 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", - "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -2092,12 +2100,12 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", - "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", + "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -2105,9 +2113,9 @@ } }, "node_modules/@smithy/util-config-provider": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", - "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -2117,15 +2125,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.26", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.26.tgz", - "integrity": "sha512-xgl75aHIS/3rrGp7iTxQAOELYeyiwBu+eEgAk4xfKwJJ0L8VUjhO2shsDpeil54BOFsqmk5xfdesiewbUY5tKQ==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.10.tgz", + "integrity": "sha512-3iA3JVO1VLrP21FsZZpMCeF93aqP3uIOMvymAT3qHIJz2YlgDeRvNUspFwCNqd/j3qqILQJGtsVQnJZICh/9YA==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.0.5", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "bowser": "^2.11.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.7", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -2133,17 +2140,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.26", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.26.tgz", - "integrity": "sha512-z81yyIkGiLLYVDetKTUeCZQ8x20EEzvQjrqJtb/mXnevLq2+w3XCEWTJ2pMp401b6BkEkHVfXb/cROBpVauLMQ==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.13.tgz", + "integrity": "sha512-PTc6IpnpSGASuzZAgyUtaVfOFpU0jBD2mcGwrgDuHf7PlFgt5TIPxCYBDbFQs06jxgeV3kd/d/sok1pzV0nJRg==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.1.5", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/property-provider": "^4.0.5", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", + "@smithy/config-resolver": "^4.4.3", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.7", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -2151,13 +2158,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.7.tgz", - "integrity": "sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", + "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -2165,9 +2172,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", - "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -2177,12 +2184,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.5.tgz", - "integrity": "sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", + "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.2", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -2190,13 +2197,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.7.tgz", - "integrity": "sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", + "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.0.7", - "@smithy/types": "^4.3.2", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -2204,18 +2211,18 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.4.tgz", - "integrity": "sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==", + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", + "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -2223,9 +2230,9 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", - "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -2235,12 +2242,24 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" }, "engines": { @@ -2300,12 +2319,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "license": "MIT" - }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -2625,9 +2638,9 @@ "dev": true }, "node_modules/bowser": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.0.tgz", - "integrity": "sha512-HcOcTudTeEWgbHh0Y1Tyb6fdeR71m4b/QACf0D4KswGTsNeIJQmg38mRENZPAYPZvGFN3fk3604XbQEPdxXdKg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", "license": "MIT" }, "node_modules/brace-expansion": { @@ -4711,10 +4724,11 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -6446,19 +6460,6 @@ "punycode": "^2.1.0" } }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/version-guard": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/version-guard/-/version-guard-1.1.3.tgz", @@ -6662,9 +6663,9 @@ } }, "node_modules/xmllint-wasm": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xmllint-wasm/-/xmllint-wasm-5.0.0.tgz", - "integrity": "sha512-vHgxKtU1ooKxlvaB/YcUj+bO+c53EvPXrk9my83/SZhcnf8D32GbACPiC3kyrMLqJQJzpkzSykmh23Cv21fvlg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xmllint-wasm/-/xmllint-wasm-5.1.0.tgz", + "integrity": "sha512-6HCIJKAJWt96UzA2dgPXsnMuYQihD7U1DU9Tu3BdXqVruha1KV8nUofOxbw8f5ULgQGdNsJMwtX3dyaTHd9hQQ==", "license": "MIT", "engines": { "node": ">=16" diff --git a/package.json b/package.json index acf774d..8adb999 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "3.0.0", "description": "CAP XML service", "engines": { - "node": ">=20" + "node": "22.x" }, "main": "handler.js", "scripts": { @@ -18,23 +18,23 @@ "author": "The Environment Agency", "license": "OGL", "dependencies": { - "@aws-sdk/client-sns": "^3.873.0", - "@xmldom/xmldom": "^0.8.11", + "@aws-sdk/client-sns": "3.932.0", + "@xmldom/xmldom": "0.8.11", "feed": "5.1.0", - "joi": "^18.0.1", - "moment": "^2.30.1", + "joi": "18.0.1", + "moment": "2.30.1", "pg": "8.16.3", "sql-ts": "7.1.0", - "xml-formatter": "^3.6.7", + "xml-formatter": "3.6.7", "xml2js": "0.6.2", - "xmllint-wasm": "^5.0.0" + "xmllint-wasm": "5.1.0" }, "devDependencies": { - "@hapi/code": "^9.0.3", - "@hapi/lab": "^26.0.0", - "aws-sdk-client-mock": "^4.1.0", - "proxyquire": "^2.1.3", - "sinon": "^21.0.0", + "@hapi/code": "9.0.3", + "@hapi/lab": "26.0.0", + "aws-sdk-client-mock": "4.1.0", + "proxyquire": "2.1.3", + "sinon": "21.0.0", "standard": "17.1.2" } } diff --git a/readme.md b/readme.md index ff206d4..43119c8 100644 --- a/readme.md +++ b/readme.md @@ -20,7 +20,7 @@ This project provides CAP XML services through the use of AWS Lambda. ## Prerequisites -- **Node.js 20** or higher +- **Node.js 22** or higher ## Installing diff --git a/test/lib/functions/data/capAlert.json b/test/lib/functions/data/capAlert.json deleted file mode 100644 index 416bdcf..0000000 --- a/test/lib/functions/data/capAlert.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "bodyXml": "\r\n\r\n 4eb3b7350ab7aa443650fc9351f02940E\r\n www.gov.uk/environment-agency\r\n 2017-05-28T11:00:02-00:00\r\n Actual\r\n Alert\r\n Flood warning service\r\n Public\r\n \r\n en-GB\r\n Met\r\n \r\n ImmediateMinorLikely2017-05-29T11:00:02-00:00Environment AgencyArea descriptionpointspointspointsTargetAreaCode" -} \ No newline at end of file diff --git a/test/lib/functions/data/capMessageId.json b/test/lib/functions/data/capMessageId.json deleted file mode 100644 index e82a1c2..0000000 --- a/test/lib/functions/data/capMessageId.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "pathParameters": { - "id": "4eb3b7350ab7aa443650fc9351f" - } -} \ No newline at end of file diff --git a/test/lib/functions/data/capUpdate.json b/test/lib/functions/data/capUpdate.json deleted file mode 100644 index 8aa77c5..0000000 --- a/test/lib/functions/data/capUpdate.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "bodyXml": "\r\n\r\n 4eb3b7350ab7aa443650fc9351f02940E\r\n www.gov.uk/environment-agency\r\n 2017-05-28T11:00:02-00:00\r\n Actual\r\n Update\r\n Flood warning service\r\n Public\r\n \r\n en-GB\r\n Met\r\n \r\n ImmediateMinorLikely2017-05-29T11:00:02-00:00Environment AgencyArea descriptionpointsTargetAreaCode" -} \ No newline at end of file diff --git a/test/lib/functions/data/nws-alert.xml b/test/lib/functions/data/nws-alert.xml new file mode 100644 index 0000000..aaefc79 --- /dev/null +++ b/test/lib/functions/data/nws-alert.xml @@ -0,0 +1,34 @@ + + 4eb3b7350ab7aa443650fc9351f02940E + www.gov.uk/environment-agency + 2025-11-06T08:00:27+00:00 + Actual + Alert + Flood warning service + Public + + + en-GB + Met + + Immediate + Minor + Likely + 2025-11-16T08:00:27+00:00 + Environment Agency + + + https://check-for-flooding.service.gov.uk + 0345 988 1188 + + + 54.54509,-2.95255 54.54498,-2.95268... + + TargetAreaCode + + + + + \ No newline at end of file diff --git a/test/lib/functions/getMessage.js b/test/lib/functions/getMessage.js index 0dc88d3..7714d07 100644 --- a/test/lib/functions/getMessage.js +++ b/test/lib/functions/getMessage.js @@ -4,153 +4,43 @@ const Lab = require('@hapi/lab') const lab = exports.lab = Lab.script() const Code = require('@hapi/code') const sinon = require('sinon') -const fs = require('fs') -const path = require('path') -let getMessage = require('../../../lib/functions/getMessage').getMessage -const service = require('../../../lib/helpers/service') -const getMessageXmlInvalid = fs.readFileSync(path.join(__dirname, 'data', 'getMessage-invalid.xml'), 'utf8') -const getMessageXmlValid = fs.readFileSync(path.join(__dirname, 'data', 'getMessage-valid.xml'), 'utf8') -let event - -lab.experiment('getMessage', () => { - lab.beforeEach(() => { - event = { - pathParameters: { - id: '4eb3b7350ab7aa443650fc9351f' - } - } - // mock service - service.getMessage = (query, params) => Promise.resolve({ - rows: [{ - getmessage: { - alert: 'test' - } - }] +const Proxyquire = require('proxyquire').noCallThru() + +lab.experiment('getMessage v1 wrapper', () => { + lab.test('Calls getMessage helper with v2=false', async () => { + const getMessageStub = sinon.stub().resolves({ + statusCode: 200, + headers: { 'content-type': 'application/xml' }, + body: 'test' }) - }) - lab.test('Correct data test', async () => { - const ret = await getMessage(event) - Code.expect(ret.statusCode).to.equal(200) - Code.expect(ret.headers['content-type']).to.equal('application/xml') - Code.expect(ret.body).to.equal('test') - }) + const getMessage = Proxyquire('../../../lib/functions/getMessage', { + '../helpers/message': { getMessage: getMessageStub } + }).getMessage - lab.test('No data found test', async () => { - service.getMessage = (query, params) => Promise.resolve({ - rows: [] - }) + const event = { pathParameters: { id: 'test123' } } + await getMessage(event) - const err = await Code.expect(getMessage(event)).to.reject() - Code.expect(err.message).to.equal('No message found') + Code.expect(getMessageStub.callCount).to.equal(1) + Code.expect(getMessageStub.calledWith(event, false)).to.be.true() }) - lab.test('Incorrect database rows object', async () => { - service.getMessage = (query, params) => Promise.resolve({ - rows: 1 - }) - - const err = await Code.expect(getMessage(event)).to.reject() - Code.expect(err.message).to.equal('No message found') - }) - - lab.test('Incorrect database rows object', async () => { - service.getMessage = (query, params) => Promise.resolve({ - rows: [{}] - }) - - const err = await Code.expect(getMessage(event)).to.reject() - Code.expect(err.message).to.equal('No message found') - }) - - lab.test('Missing database rows object', async () => { - service.getMessage = (query, params) => Promise.resolve({ - no_rows: [] - }) - - const err = await Code.expect(getMessage(event)).to.reject() - Code.expect(err.message).to.equal('No message found') - }) - - lab.test('No database return', async () => { - service.getMessage = (query, params) => { - return new Promise((resolve, reject) => { - resolve() - }) + lab.test('Returns the result from getMessage helper', async () => { + const expectedResult = { + statusCode: 200, + headers: { 'content-type': 'application/xml' }, + body: 'v1 alert' } - const err = await Code.expect(getMessage(event)).to.reject() - Code.expect(err.message).to.equal('No message found') - }) - - lab.test('Error test', async () => { - service.getMessage = (query, params) => Promise.reject(new Error('test error')) - const err = await Code.expect(getMessage(event)).to.reject() - Code.expect(err.message).to.equal('test error') - }) - - lab.test('event validation test', async () => { - event.id = {} - await Code.expect(getMessage(event)).to.reject() - }) + const getMessageStub = sinon.stub().resolves(expectedResult) - lab.test('event validation test 2', async () => { - event = {} - await Code.expect(getMessage(event)).to.reject() - }) - lab.test('Invalid id format test', async () => { - event.pathParameters.id = 'invalid_id_format' + const getMessage = Proxyquire('../../../lib/functions/getMessage', { + '../helpers/message': { getMessage: getMessageStub } + }).getMessage - await Code.expect(getMessage(event)).to.reject() - }) - lab.test('Valid id format test', async () => { - event.pathParameters.id = 'a1b2c3' + const event = { pathParameters: { id: 'test123' } } const result = await getMessage(event) - const body = result.body - - Code.expect(body).to.equal('test') - }) - lab.test('XsdSchema validation test: invalid alert', async () => { - let consoleLogStub - try { - delete require.cache[require.resolve('../../../lib/functions/getMessage')] - consoleLogStub = sinon.stub(console, 'log') - const func = require('../../../lib/functions/getMessage').getMessage - service.getMessage = () => Promise.resolve({ - rows: [{ - getmessage: { - alert: getMessageXmlInvalid - } - }] - }) - await func(event) - Code.expect(consoleLogStub.callCount).to.equal(2) - Code.expect(consoleLogStub.getCall(0).args[0]).to.equal('CAP get message failed validation') - Code.expect(consoleLogStub.getCall(1).args[0]).to.equal('[{"rawMessage":"message.xml:19: Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}geocode\': This element is not expected. Expected is ( {urn:oasis:names:tc:emergency:cap:1.2}areaDesc ).","message":"Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}geocode\': This element is not expected. Expected is ( {urn:oasis:names:tc:emergency:cap:1.2}areaDesc ).","loc":{"fileName":"message.xml","lineNumber":19}}]') - } finally { - consoleLogStub.restore() - getMessage = require('../../../lib/functions/getMessage').getMessage - } - }) - lab.test('XsdSchema validation test: valid alert', async () => { - let consoleLogStub - try { - delete require.cache[require.resolve('../../../lib/functions/getMessage')] - consoleLogStub = sinon.stub(console, 'log') - const func = require('../../../lib/functions/getMessage').getMessage - service.getMessage = () => Promise.resolve({ - rows: [{ - getmessage: { - alert: getMessageXmlValid - } - }] - }) - await func(event) - Code.expect(consoleLogStub.callCount).to.equal(0) - } finally { - consoleLogStub.restore() - getMessage = require('../../../lib/functions/getMessage').getMessage - } + Code.expect(result).to.equal(expectedResult) }) }) diff --git a/test/lib/functions/getMessageAtomValidation.js b/test/lib/functions/getMessageAtomValidation.js index dabf520..0137470 100644 --- a/test/lib/functions/getMessageAtomValidation.js +++ b/test/lib/functions/getMessageAtomValidation.js @@ -8,9 +8,9 @@ const Proxyquire = require('proxyquire').noCallThru() // Mock the service.getAllMessages function for validation experiment and tests const loadHandlerWithValidateMock = (validateMock) => { - return Proxyquire('../../../lib/functions/getMessagesAtom', { + return Proxyquire('../../../lib/helpers/messages', { 'xmllint-wasm': { validateXML: validateMock } - }).getMessagesAtom + }).messages } lab.experiment('getMessagesAtom validation logging', () => { diff --git a/test/lib/functions/getMessagesAtom.js b/test/lib/functions/getMessagesAtom.js index fd601da..5ab9887 100644 --- a/test/lib/functions/getMessagesAtom.js +++ b/test/lib/functions/getMessagesAtom.js @@ -3,63 +3,42 @@ const Lab = require('@hapi/lab') const lab = exports.lab = Lab.script() const Code = require('@hapi/code') -const getMessagesAtom = require('../../../lib/functions/getMessagesAtom').getMessagesAtom -const service = require('../../../lib/helpers/service') +const sinon = require('sinon') +const Proxyquire = require('proxyquire').noCallThru() -lab.experiment('getMessagesAtom', () => { - lab.beforeEach(() => { - // mock database query - service.getAllMessages = (query) => { - return new Promise((resolve, reject) => { - resolve({ - rows: [{ - fwis_code: 'test_fwis_code', - alert: 'test', - sent: new Date(), - identifier: '4eb3b7350ab7aa443650fc9351f' - }] - }) - }) - } - }) +lab.experiment('getMessagesAtom v1 wrapper', () => { + lab.test('Calls messages helper with v2=false', async () => { + const messagesStub = sinon.stub().resolves({ + statusCode: 200, + headers: { 'content-type': 'application/xml' }, + body: 'test' + }) - lab.test('Correct data test', async () => { - const ret = await getMessagesAtom({}) - Code.expect(ret.statusCode).to.equal(200) - Code.expect(ret.headers['content-type']).to.equal('application/xml') - }) + const getMessagesAtom = Proxyquire('../../../lib/functions/getMessagesAtom', { + '../helpers/messages': { messages: messagesStub } + }).getMessagesAtom - lab.test('Bad rows returned', async () => { - service.getAllMessages = (query) => { - return new Promise((resolve, reject) => { - resolve({ - rows: 1 - }) - }) - } - const ret = await getMessagesAtom({}) - Code.expect(ret.statusCode).to.equal(200) - Code.expect(ret.headers['content-type']).to.equal('application/xml') - }) + await getMessagesAtom() - lab.test('No return from database', async () => { - service.getAllMessages = (query) => { - return new Promise((resolve, reject) => { - resolve() - }) - } - const ret = await getMessagesAtom({}) - Code.expect(ret.statusCode).to.equal(200) - Code.expect(ret.headers['content-type']).to.equal('application/xml') + Code.expect(messagesStub.callCount).to.equal(1) + Code.expect(messagesStub.calledWith(false)).to.be.true() }) - lab.test('Error test', async () => { - service.getAllMessages = (query) => { - return new Promise((resolve, reject) => { - reject(new Error('test error')) - }) + lab.test('Returns the result from messages helper', async () => { + const expectedResult = { + statusCode: 200, + headers: { 'content-type': 'application/xml' }, + body: 'v1 feed' } - const err = await Code.expect(getMessagesAtom({})).to.reject() - Code.expect(err.message).to.equal('test error') + + const messagesStub = sinon.stub().resolves(expectedResult) + + const getMessagesAtom = Proxyquire('../../../lib/functions/getMessagesAtom', { + '../helpers/messages': { messages: messagesStub } + }).getMessagesAtom + + const result = await getMessagesAtom() + + Code.expect(result).to.equal(expectedResult) }) }) diff --git a/test/lib/functions/processMessage.js b/test/lib/functions/processMessage.js index 1d062b4..d63fe65 100644 --- a/test/lib/functions/processMessage.js +++ b/test/lib/functions/processMessage.js @@ -4,24 +4,118 @@ const Lab = require('@hapi/lab') const lab = exports.lab = Lab.script() const Code = require('@hapi/code') const sinon = require('sinon') +const fs = require('fs') +const path = require('path') +const xml2js = require('xml2js') const processMessage = require('../../../lib/functions/processMessage').processMessage const service = require('../../../lib/helpers/service') const aws = require('../../../lib/helpers/aws') -const moment = require('moment') -let capAlert -let capUpdate - +const Message = require('../../../lib/models/message') +const v2MessageMapping = require('../../../lib/models/v2MessageMapping') +const nwsAlert = { bodyXml: fs.readFileSync(path.join(__dirname, 'data', 'nws-alert.xml'), 'utf8') } const ORIGINAL_ENV = process.env - +let clock const tomorrow = new Date(new Date().getTime() + (24 * 60 * 60 * 1000)) -const yesterday = new Date(new Date().getTime() - (24 * 60 * 60 * 1000)) +const identifier = '4eb3b7350ab7aa443650fc9351f02940E' +const identifierV2 = `2.49.0.0.826.1.20251106080027.${identifier}` +const code = 'MCP:v2.0' +const referencesV1 = 'www.gov.uk/environment-agency,4eb3b7350ab7aa443650fc9351f2,2020-01-01T00:00:00+00:00' +const referencesV2 = 'www.gov.uk/environment-agency,2.49.0.0.826.1.20251106080027.4eb3b7350ab7aa443650fc9351f02940E,2020-01-01T00:00:00+00:00' + +// *********************************************************** +// Helper functions +// *********************************************************** +const expectResponse = (response, putQuery, severity = 'Minor', status = 'Test', msgType = 'Alert', references = false, previousReferences = false, quickdialNumber = true) => { + expectResponseAndPutQuery(response, putQuery, status, msgType, references, previousReferences) + expectMessageV1(new Message(putQuery.values[3]), severity, status, references, previousReferences, quickdialNumber) + expectMessageV2(new Message(putQuery.values[10]), severity, status, references, previousReferences, quickdialNumber) +} + +const expectResponseAndPutQuery = (response, putQuery, status, msgType, references, previousReferences) => { + // test response + Code.expect(response.statusCode).to.equal(200) + Code.expect(response.body.identifier).to.equal(identifier) + Code.expect(response.body.fwisCode).to.equal('TESTAREA1') + Code.expect(response.body.sent).to.equal('2025-11-06T08:00:27+00:00') + Code.expect(response.body.expires).to.equal('2025-11-16T08:00:27+00:00') + Code.expect(response.body.status).to.equal(status) + + // test putquery + Code.expect(putQuery.text).to.equal('INSERT INTO "messages" ("identifier", "msg_type", "references", "alert", "fwis_code", "expires", "sent", "created", "identifier_v2", "references_v2", "alert_v2") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)') + Code.expect(putQuery.values[0]).to.equal(identifier) + Code.expect(putQuery.values[1]).to.equal(msgType) + if (references) { + Code.expect(putQuery.values[2]).to.equal(previousReferences ? `${referencesV1} ${referencesV1}` : referencesV1) + } else { + Code.expect(putQuery.values[2]).to.be.empty() + } + Code.expect(putQuery.values[3]).to.not.be.empty() + Code.expect(putQuery.values[4]).to.equal('TESTAREA1') + Code.expect(putQuery.values[5]).to.equal('2025-11-16T08:00:27+00:00') + Code.expect(putQuery.values[6]).to.equal('2025-11-06T08:00:27+00:00') + Code.expect(putQuery.values[7]).to.equal('2020-01-01T00:00:00.000Z') + Code.expect(putQuery.values[8]).to.equal(identifierV2) + if (references) { + Code.expect(putQuery.values[9]).to.equal(previousReferences ? `${referencesV2} ${referencesV2}` : referencesV2) + } else { + Code.expect(putQuery.values[9]).to.be.empty() + } + Code.expect(putQuery.values[10]).to.not.be.empty() +} + +const expectMessageV1 = (message, severity, status, references, previousReferences, quickdialNumber) => { + Code.expect(message.identifier).to.equal(identifier) + Code.expect(message.status).to.equal(status) + Code.expect(message.code).to.equal('') + if (references) { + Code.expect(message.references).to.equal(previousReferences ? `${referencesV1} ${referencesV1}` : referencesV1) + } else { + Code.expect(message.references).to.be.empty() + } + Code.expect(message.event).to.equal('Update') + Code.expect(message.severity).to.equal(severity) + Code.expect(message.onset).to.equal('') + Code.expect(message.headline).to.equal('') + Code.expect(message.instruction).not.to.contain('https://check-for-flooding.service.gov.uk/target-area/TESTAREA1') + if (quickdialNumber) { + Code.expect(message.instruction).not.to.contain('- call Floodline on 0345 988 1188, using quickdial code 210010') + Code.expect(message.instruction).to.contain('- For access to flood warning information offline call Floodline on 0345 988 1188 using quickdial code: 210010.') + } else { + Code.expect(message.instruction).not.to.contain('- call Floodline on 0345 988 1188, using quickdial code 210010') + Code.expect(message.instruction).to.contain('- For access to flood warning information offline call Floodline on 0345 988 1188 using') + } +} + +const expectMessageV2 = (message, severity, status, references, previousReferences, quickdialNumber) => { + const mapping = v2MessageMapping[severity] + // Test message fields updated for message V2 + Code.expect(message.identifier).to.equal(identifierV2) + Code.expect(message.status).to.equal(status) + Code.expect(message.code).to.equal(code) + if (references) { + Code.expect(message.references).to.equal(previousReferences ? `${referencesV2} ${referencesV2}` : referencesV2) + } else { + Code.expect(message.references).to.be.empty() + } + Code.expect(message.event).to.equal(`${mapping.description}: Rivers Lowther and Eamont`) + Code.expect(message.severity).to.equal(mapping.severity) + Code.expect(message.onset).to.equal(message.sent) + Code.expect(message.headline).to.equal(`${mapping.headline}: Rivers Lowther and Eamont`) + Code.expect(message.instruction).to.contain('https://check-for-flooding.service.gov.uk/target-area/TESTAREA1') + if (quickdialNumber) { + Code.expect(message.instruction).to.contain('- call Floodline on 0345 988 1188, using quickdial code 210010') + Code.expect(message.instruction).not.to.contain('- For access to flood warning information offline call Floodline on 0345 988 1188 using quickdial code: 210010.') + } else { + Code.expect(message.instruction).not.to.contain('- call Floodline on 0345 988 1188, using quickdial code 210010') + Code.expect(message.instruction).not.to.contain('- For access to flood warning information offline call Floodline on 0345 988 1188 using') + } +} +// *********************************************************** lab.experiment('processMessage', () => { lab.beforeEach(() => { + clock = sinon.useFakeTimers(new Date('2020-01-01T00:00:00Z').getTime()) process.env = { ...ORIGINAL_ENV } - capAlert = require('./data/capAlert.json') - capUpdate = require('./data/capUpdate.json') - // mock services service.putMessage = (query) => { return new Promise((resolve, reject) => { @@ -37,187 +131,208 @@ lab.experiment('processMessage', () => { }) lab.afterEach(() => { + clock.restore() sinon.restore() }) - lab.test('Correct data test with no previous alert on test', async () => { + lab.test('Correct data test with no previous alert on test (empty array from db)', async () => { service.getLastMessage = (id) => Promise.resolve({ rows: [] }) + let putQuery service.putMessage = (query) => Promise.resolve().then(() => { - Code.expect(query.values[2]).to.be.empty() - Code.expect(query.values[1]).to.equal('Alert') + putQuery = query }) - const ret = await processMessage(capAlert) - Code.expect(ret.statusCode).to.equal(200) - Code.expect(ret.body.identifier).to.equal('4eb3b7350ab7aa443650fc9351f02940E') - Code.expect(ret.body.fwisCode).to.equal('TESTAREA1') - Code.expect(ret.body.sent).to.equal('2017-05-28T11:00:02-00:00') - Code.expect(ret.body.expires).to.equal('2017-05-29T11:00:02-00:00') - Code.expect(ret.body.status).to.equal('Test') + // do alert and test output xml + let response = await processMessage(nwsAlert) + expectResponse(response, putQuery, 'Minor') + + // do warning and test output xml + response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') }) + expectResponse(response, putQuery, 'Moderate') + + // do severe warning and test output xml + response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) + expectResponse(response, putQuery, 'Severe') }) - lab.test('Correct data test with no previous alert on test 2', async () => { - service.getLastMessage = (id) => { - return new Promise((resolve, reject) => { + lab.test('Correct data test with no previous alert on test 2 (nothing resolved from db)', async () => { + service.getLastMessage = () => { + return new Promise((resolve) => { resolve() }) } - service.putMessage = (query) => { - return new Promise((resolve, reject) => { - Code.expect(query.values[2]).to.be.empty() - Code.expect(query.values[1]).to.equal('Alert') - resolve() - }) - } - const ret = await processMessage(capAlert) - Code.expect(ret.statusCode).to.equal(200) - Code.expect(ret.body.identifier).to.equal('4eb3b7350ab7aa443650fc9351f02940E') - Code.expect(ret.body.fwisCode).to.equal('TESTAREA1') - Code.expect(ret.body.sent).to.equal('2017-05-28T11:00:02-00:00') - Code.expect(ret.body.expires).to.equal('2017-05-29T11:00:02-00:00') - Code.expect(ret.body.status).to.equal('Test') + let putQuery + service.putMessage = (query) => Promise.resolve().then(() => { + putQuery = query + }) + // do alert and test output xml + let response = await processMessage(nwsAlert) + expectResponse(response, putQuery, 'Minor') + + // do warning and test output xml + response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') }) + expectResponse(response, putQuery, 'Moderate') + + // do severe warning and test output xml + response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) + expectResponse(response, putQuery, 'Severe') }) - lab.test('Correct data test with no previous alert on production', async () => { + lab.test('Correct data test with no previous alert on production, tests status switches to Actual', async () => { process.env.stage = 'prd' + let putQuery + service.putMessage = (query) => Promise.resolve().then(() => { + putQuery = query + }) - service.putMessage = (query) => { - return new Promise((resolve, reject) => { - // Check that reference field is blank - Code.expect(query.values[2]).to.be.empty() - Code.expect(query.values[1]).to.equal('Alert') - resolve() - }) - } + // do alert and test output xml + let response = await processMessage(nwsAlert) + expectResponse(response, putQuery, 'Minor', 'Actual') - const ret = await processMessage(capAlert) - Code.expect(ret.statusCode).to.equal(200) - Code.expect(ret.body.identifier).to.equal('4eb3b7350ab7aa443650fc9351f02940E') - Code.expect(ret.body.fwisCode).to.equal('TESTAREA1') - Code.expect(ret.body.sent).to.equal('2017-05-28T11:00:02-00:00') - Code.expect(ret.body.expires).to.equal('2017-05-29T11:00:02-00:00') - Code.expect(ret.body.status).to.not.equal('Test') - Code.expect(ret.body.status).to.equal('Actual') + // do warning and test output xml + response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') }) + expectResponse(response, putQuery, 'Moderate', 'Actual') + + // do severe warning and test output xml + response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) + expectResponse(response, putQuery, 'Severe', 'Actual') }) lab.test('Correct data test with active alert on test', async () => { - process.env.stage = 'prd' - service.getLastMessage = (id) => Promise.resolve({ rows: [{ id: '51', identifier: '4eb3b7350ab7aa443650fc9351f2', expires: tomorrow, - sent: yesterday + sent: '2020-01-01T00:00:00Z', + identifier_v2: identifierV2 }] }) + let putQuery + service.putMessage = (query) => Promise.resolve().then(() => { - Code.expect(query.values[2]).to.not.be.empty() - Code.expect(query.values[2]).to.contain(yesterday.toISOString().substring(0, yesterday.toISOString().length - 5)) - Code.expect(query.values[1]).to.equal('Update') + putQuery = query }) - const ret = await processMessage(capAlert) - Code.expect(ret.statusCode).to.equal(200) - Code.expect(ret.body.identifier).to.equal('4eb3b7350ab7aa443650fc9351f02940E') - Code.expect(ret.body.fwisCode).to.equal('TESTAREA1') - Code.expect(ret.body.sent).to.equal('2017-05-28T11:00:02-00:00') - Code.expect(ret.body.expires).to.equal('2017-05-29T11:00:02-00:00') - Code.expect(ret.body.status).to.not.equal('Test') - Code.expect(ret.body.status).to.equal('Actual') + // do alert and test output xml + let response = await processMessage(nwsAlert) + expectResponse(response, putQuery, 'Minor', 'Test', 'Update', true) + + // do warning and test output xml + response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') }) + expectResponse(response, putQuery, 'Moderate', 'Test', 'Update', true) + + // do severe warning and test output xml + response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) + expectResponse(response, putQuery, 'Severe', 'Test', 'Update', true) }) - lab.test('Correct data test with active alert on test with prexisting references field', async () => { + lab.test('Correct alert data test with an active on production', async () => { process.env.stage = 'prd' service.getLastMessage = (id) => Promise.resolve({ rows: [{ id: '51', identifier: '4eb3b7350ab7aa443650fc9351f2', + sent: '2020-01-01T00:00:00Z', expires: tomorrow, - sent: yesterday, - references: yesterday.toISOString() + msgType: 'Alert', + identifier_v2: identifierV2 }] }) - + let putQuery service.putMessage = (query) => Promise.resolve().then(() => { - Code.expect(query.values[2]).to.not.be.empty() - Code.expect(query.values[2]).to.contain(yesterday.toISOString().substring(0, yesterday.toISOString().length - 5)) - Code.expect(query.values[1]).to.equal('Update') + putQuery = query }) - const ret = await processMessage(capAlert) - Code.expect(ret.statusCode).to.equal(200) - Code.expect(ret.body.identifier).to.equal('4eb3b7350ab7aa443650fc9351f02940E') - Code.expect(ret.body.fwisCode).to.equal('TESTAREA1') - Code.expect(ret.body.sent).to.equal('2017-05-28T11:00:02-00:00') - Code.expect(ret.body.expires).to.equal('2017-05-29T11:00:02-00:00') - Code.expect(ret.body.status).to.not.equal('Test') - Code.expect(ret.body.status).to.equal('Actual') - }) + // do alert and test output xml + let response = await processMessage(nwsAlert) + expectResponse(response, putQuery, 'Minor', 'Actual', 'Update', true) - lab.test('Correct alert data test with an active on production', async () => { - process.env.stage = 'prd' + // do warning and test output xml + response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') }) + expectResponse(response, putQuery, 'Moderate', 'Actual', 'Update', true) + // do severe warning and test output xml + response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) + expectResponse(response, putQuery, 'Severe', 'Actual', 'Update', true) + }) + + lab.test('Edge cases: Correct data test with active alert on test including references and no quickdial code', async () => { service.getLastMessage = (id) => Promise.resolve({ rows: [{ id: '51', identifier: '4eb3b7350ab7aa443650fc9351f2', - sent: yesterday, + references: referencesV1, expires: tomorrow, - msgType: 'Alert' + sent: '2020-01-01T00:00:00Z', + identifier_v2: identifierV2, + references_v2: referencesV2 }] }) + let putQuery + service.putMessage = (query) => Promise.resolve().then(() => { - Code.expect(query.values[2]).to.not.be.empty() - Code.expect(query.values[1]).to.equal('Update') - Code.expect(query.values[2]).to.contain(yesterday.toISOString().substring(0, yesterday.toISOString().length - 5)) + putQuery = query }) - const ret = await processMessage(capAlert) - Code.expect(ret.statusCode).to.equal(200) - Code.expect(ret.body.identifier).to.equal('4eb3b7350ab7aa443650fc9351f02940E') - Code.expect(ret.body.fwisCode).to.equal('TESTAREA1') - Code.expect(ret.body.sent).to.equal('2017-05-28T11:00:02-00:00') - Code.expect(ret.body.expires).to.equal('2017-05-29T11:00:02-00:00') - Code.expect(ret.body.status).to.not.equal('Test') - Code.expect(ret.body.status).to.equal('Actual') + // strip out quick dial code + const alert = { bodyXml: nwsAlert.bodyXml.replace('quickdial code: 210010.', '') } + + // do alert and test output xml + let response = await processMessage(alert) + expectResponse(response, putQuery, 'Minor', 'Test', 'Update', true, true, false) + + // do warning and test output xml + response = await processMessage({ bodyXml: alert.bodyXml.replace('Minor', 'Moderate') }) + expectResponse(response, putQuery, 'Moderate', 'Test', 'Update', true, true, false) + + // do severe warning and test output xml + response = await processMessage({ bodyXml: alert.bodyXml.replace('Minor', 'Severe') }) + expectResponse(response, putQuery, 'Severe', 'Test', 'Update', true, true, false) }) - lab.test('Correct update data test with an active on production', async () => { + lab.test('Edge cases: Correct alert data test with an active on production including references and no quickdial code', async () => { process.env.stage = 'prd' service.getLastMessage = (id) => Promise.resolve({ rows: [{ id: '51', identifier: '4eb3b7350ab7aa443650fc9351f2', - sent: yesterday, + references: referencesV1, + sent: '2020-01-01T00:00:00Z', expires: tomorrow, - msgType: 'Alert' + msgType: 'Alert', + identifier_v2: identifierV2, + references_v2: referencesV2 }] }) - + let putQuery service.putMessage = (query) => Promise.resolve().then(() => { - Code.expect(query.values[2]).to.not.be.empty() - Code.expect(query.values[1]).to.equal('Update') - Code.expect(query.values[2]).to.contain(yesterday.toISOString().substring(0, yesterday.toISOString().length - 5)) + putQuery = query }) - const ret = await processMessage(capUpdate) - Code.expect(ret.statusCode).to.equal(200) - Code.expect(ret.body.identifier).to.equal('4eb3b7350ab7aa443650fc9351f02940E') - Code.expect(ret.body.fwisCode).to.equal('TESTAREA1') - Code.expect(ret.body.sent).to.equal('2017-05-28T11:00:02-00:00') - Code.expect(ret.body.expires).to.equal('2017-05-29T11:00:02-00:00') - Code.expect(ret.body.status).to.not.equal('Test') - Code.expect(ret.body.status).to.equal('Actual') + // do alert and test output xml + let response = await processMessage(nwsAlert) + expectResponse(response, putQuery, 'Minor', 'Actual', 'Update', true, true) + + // do warning and test output xml + response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') }) + expectResponse(response, putQuery, 'Moderate', 'Actual', 'Update', true, true) + + // do severe warning and test output xml + response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) + expectResponse(response, putQuery, 'Severe', 'Actual', 'Update', true, true) }) + // *********************************************************** + // Sad path tests + // *********************************************************** lab.test('Bad data test', async () => { sinon.stub(aws.email, 'publishMessage').callsFake((message) => { return new Promise((resolve, reject) => { @@ -234,47 +349,16 @@ lab.experiment('processMessage', () => { lab.test('Database error', async () => { service.putMessage = (query) => Promise.reject(new Error('unit test error')) - const err = await Code.expect(processMessage(capAlert)).to.reject() + const err = await Code.expect(processMessage(nwsAlert)).to.reject() Code.expect(err.message).to.equal('unit test error') }) lab.test('Database error 2', async () => { service.getLastMessage = (id) => Promise.reject(new Error('unit test error')) - const err = await Code.expect(processMessage(capAlert)).to.reject() + const err = await Code.expect(processMessage(nwsAlert)).to.reject() Code.expect(err.message).to.equal('unit test error') }) - - lab.test('Correct data test for processMessage where previous message is active and has reference', async () => { - process.env.stage = 'prd' - // Replace the trivial promise with Promise.resolve - service.getLastMessage = (id) => Promise.resolve({ - rows: [{ - id: '51', - identifier: '4eb3b7350ab7aa443650fc9351f2', - expires: tomorrow, - sent: yesterday, - references: 'Previous_Active_Message' - }] - }) - - service.putMessage = (query) => Promise.resolve().then(() => { - const lastDate = moment(yesterday).utc().format('YYYY-MM-DDTHH:mm:ssZ') - Code.expect(query.values[2]).to.not.be.empty() - Code.expect(query.values[1]).to.equal('Update') - Code.expect(query.values[2]).to.contain(`Previous_Active_Message www.gov.uk/environment-agency,4eb3b7350ab7aa443650fc9351f2,${lastDate}`) - Code.expect(query.values[2]).to.not.contain('00:00+00:00') - }) - - const ret = await processMessage(capAlert) - Code.expect(ret.statusCode).to.equal(200) - Code.expect(ret.body.identifier).to.equal('4eb3b7350ab7aa443650fc9351f02940E') - Code.expect(ret.body.fwisCode).to.equal('TESTAREA1') - Code.expect(ret.body.sent).to.equal('2017-05-28T11:00:02-00:00') - Code.expect(ret.body.expires).to.equal('2017-05-29T11:00:02-00:00') - Code.expect(ret.body.status).to.not.equal('Test') - Code.expect(ret.body.status).to.equal('Actual') - }) lab.test('Invalid bodyXml format test', async () => { // Set bodyXml to an invalid value (e.g., null, undefined, or an object) const invalidBodyXml = null @@ -282,9 +366,10 @@ lab.experiment('processMessage', () => { // Expect the processMessage function to reject due to validation failure await Code.expect(processMessage({ bodyXml: invalidBodyXml })).to.reject() }) - lab.test('Valid bodyXml format test', async () => { - const validBodyXml = capAlert.bodyXml - - await Code.expect(processMessage({ bodyXml: validBodyXml })).to.not.reject() + lab.test('Handles xml2js error', async () => { + sinon.stub(xml2js, 'parseString').callsFake((xml, callback) => { + callback(new Error('xml2js parse error')) + }) + await Code.expect(processMessage(nwsAlert)).to.reject() }) }) diff --git a/test/lib/functions/processMessageValidation.js b/test/lib/functions/processMessageValidation.js index 806b933..fa8f816 100644 --- a/test/lib/functions/processMessageValidation.js +++ b/test/lib/functions/processMessageValidation.js @@ -5,7 +5,9 @@ const lab = exports.lab = Lab.script() const Code = require('@hapi/code') const Proxyquire = require('proxyquire').noCallThru() const sinon = require('sinon') -const capAlert = require('./data/capAlert.json') +const fs = require('node:fs') +const path = require('node:path') +const nwsAlert = { bodyXml: fs.readFileSync(path.join(__dirname, 'data', 'nws-alert.xml'), 'utf8') } const fakeService = { getLastMessage: async () => ({ rows: [] }), @@ -34,9 +36,9 @@ lab.experiment('processMessage validation logging', () => { const validateMock = async () => ({ errors: [{ message: 'oops' }] }) const processMessage = loadWithValidateMock(validateMock) - await Code.expect(processMessage(capAlert)) + await Code.expect(processMessage(nwsAlert)) .to - .reject('[{"message":"oops"}]') + .reject('[{"message":"oops"},{"message":"oops"}]') Code.expect(fakeAws.email.publishMessage.callCount).to.equal(0) }) @@ -45,9 +47,9 @@ lab.experiment('processMessage validation logging', () => { const validateMock = async () => ({ errors: [{ message: 'oops' }] }) const processMessage = loadWithValidateMock(validateMock) - await Code.expect(processMessage(capAlert)) + await Code.expect(processMessage(nwsAlert)) .to - .reject('[500] [{"message":"oops"}]') + .reject('[500] [{"message":"oops"},{"message":"oops"}]') Code.expect(fakeAws.email.publishMessage.callCount).to.equal(1) }) @@ -61,7 +63,7 @@ lab.experiment('processMessage validation logging', () => { console.log = (msg) => logs.push(String(msg)) try { - await processMessage(capAlert) + await processMessage(nwsAlert) Code.expect(logs).to.include('Finished processing CAP message: 4eb3b7350ab7aa443650fc9351f02940E for TESTAREA1') Code.expect(logs.some(l => l.includes('failed validation'))).to.be.false() } finally { @@ -78,12 +80,12 @@ lab.experiment('processMessage validation logging', () => { '../helpers/aws': awsStub }).processMessage - const ret = await processMessage(capAlert) + const ret = await processMessage(nwsAlert) Code.expect(ret.statusCode).to.equal(200) Code.expect(ret.body.identifier).to.equal('4eb3b7350ab7aa443650fc9351f02940E') Code.expect(ret.body.fwisCode).to.equal('TESTAREA1') - Code.expect(ret.body.sent).to.equal('2017-05-28T11:00:02-00:00') - Code.expect(ret.body.expires).to.equal('2017-05-29T11:00:02-00:00') + Code.expect(ret.body.sent).to.equal('2025-11-06T08:00:27+00:00') + Code.expect(ret.body.expires).to.equal('2025-11-16T08:00:27+00:00') Code.expect(ret.body.status).to.equal('Test') Code.expect(awsStub.email.publishMessage.callCount).to.equal(0) @@ -134,7 +136,7 @@ lab.experiment('processMessage validation logging', () => { .reject() const errors = JSON.parse(ret.message.replace('[500] ', '')) - Code.expect(errors.length).to.equal(15) + Code.expect(errors.length).to.equal(31) // Helper to generate message asserts below // errors.forEach((er, i) => { // console.log(`Code.expect(errors[${i}].message).to.equal('${er.message.replace(/'/g, "\\'")}')`) @@ -155,6 +157,22 @@ lab.experiment('processMessage validation logging', () => { Code.expect(errors[12].message).to.equal('"alert.info[0].senderName[0]" is not allowed to be empty') Code.expect(errors[13].message).to.equal('"alert.info[0].area[0].areaDesc[0]" is not allowed to be empty') Code.expect(errors[14].message).to.equal('"alert.info[0].area[0].polygon[0]" is not allowed to be empty') + // v2 errors + Code.expect(errors[15].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}sent\': \'\' is not a valid value of the local atomic type.') + Code.expect(errors[16].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}msgType\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Alert\', \'Update\', \'Cancel\', \'Ack\', \'Error\'}.') + Code.expect(errors[17].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}scope\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Public\', \'Restricted\', \'Private\'}.') + Code.expect(errors[18].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}category\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Geo\', \'Met\', \'Safety\', \'Security\', \'Rescue\', \'Fire\', \'Health\', \'Env\', \'Transport\', \'Infra\', \'CBRNE\', \'Other\'}.') + Code.expect(errors[19].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}urgency\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Immediate\', \'Expected\', \'Future\', \'Past\', \'Unknown\'}.') + Code.expect(errors[20].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}severity\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Extreme\', \'Severe\', \'Moderate\', \'Minor\', \'Unknown\'}.') + Code.expect(errors[21].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}certainty\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Observed\', \'Likely\', \'Possible\', \'Unlikely\', \'Unknown\'}.') + Code.expect(errors[22].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}onset\': \'\' is not a valid value of the local atomic type.') + Code.expect(errors[23].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}expires\': \'\' is not a valid value of the local atomic type.') + Code.expect(errors[25].message).to.equal('"alert.sender[0]" must be [www.gov.uk/environment-agency]') + Code.expect(errors[26].message).to.equal('"alert.sender[0]" is not allowed to be empty') + Code.expect(errors[27].message).to.equal('"alert.source[0]" is not allowed to be empty') + Code.expect(errors[28].message).to.equal('"alert.info[0].senderName[0]" is not allowed to be empty') + Code.expect(errors[29].message).to.equal('"alert.info[0].area[0].areaDesc[0]" is not allowed to be empty') + Code.expect(errors[30].message).to.equal('"alert.info[0].area[0].polygon[0]" is not allowed to be empty') Code.expect(awsStub.email.publishMessage.callCount).to.equal(1) }) @@ -202,7 +220,7 @@ lab.experiment('processMessage validation logging', () => { .to .reject() const errors = JSON.parse(ret.message.replace('[500] ', '')) - Code.expect(errors.length).to.equal(9) + Code.expect(errors.length).to.equal(19) Code.expect(errors[0].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}sent\': \'2026-05-28\' is not a valid value of the local atomic type.') Code.expect(errors[1].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}msgType\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Alert\', \'Update\', \'Cancel\', \'Ack\', \'Error\'}.') Code.expect(errors[2].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}scope\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Public\', \'Restricted\', \'Private\'}.') @@ -212,6 +230,17 @@ lab.experiment('processMessage validation logging', () => { Code.expect(errors[6].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}certainty\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Observed\', \'Likely\', \'Possible\', \'Unlikely\', \'Unknown\'}.') Code.expect(errors[7].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}expires\': \'2026-05-29\' is not a valid value of the local atomic type.') Code.expect(errors[8].message).to.equal('"alert.sender[0]" must be [www.gov.uk/environment-agency]') + // v2 errors + Code.expect(errors[9].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}sent\': \'2026-05-28\' is not a valid value of the local atomic type.') + Code.expect(errors[10].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}msgType\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Alert\', \'Update\', \'Cancel\', \'Ack\', \'Error\'}.') + Code.expect(errors[11].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}scope\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Public\', \'Restricted\', \'Private\'}.') + Code.expect(errors[12].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}category\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Geo\', \'Met\', \'Safety\', \'Security\', \'Rescue\', \'Fire\', \'Health\', \'Env\', \'Transport\', \'Infra\', \'CBRNE\', \'Other\'}.') + Code.expect(errors[13].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}urgency\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Immediate\', \'Expected\', \'Future\', \'Past\', \'Unknown\'}.') + Code.expect(errors[14].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}severity\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Extreme\', \'Severe\', \'Moderate\', \'Minor\', \'Unknown\'}.') + Code.expect(errors[15].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}certainty\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Observed\', \'Likely\', \'Possible\', \'Unlikely\', \'Unknown\'}.') + Code.expect(errors[16].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}onset\': \'2026-05-28\' is not a valid value of the local atomic type.') + Code.expect(errors[17].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}expires\': \'2026-05-29\' is not a valid value of the local atomic type.') + Code.expect(errors[18].message).to.equal('"alert.sender[0]" must be [www.gov.uk/environment-agency]') Code.expect(awsStub.email.publishMessage.callCount).to.equal(1) }) diff --git a/test/lib/functions/v2/getMessage.js b/test/lib/functions/v2/getMessage.js new file mode 100644 index 0000000..f7be287 --- /dev/null +++ b/test/lib/functions/v2/getMessage.js @@ -0,0 +1,46 @@ +'use strict' + +const Lab = require('@hapi/lab') +const lab = exports.lab = Lab.script() +const Code = require('@hapi/code') +const sinon = require('sinon') +const Proxyquire = require('proxyquire').noCallThru() + +lab.experiment('getMessage v2 wrapper', () => { + lab.test('Calls getMessage helper with v2=true', async () => { + const getMessageStub = sinon.stub().resolves({ + statusCode: 200, + headers: { 'content-type': 'application/xml' }, + body: 'test' + }) + + const getMessage = Proxyquire('../../../../lib/functions/v2/getMessage', { + '../../helpers/message': { getMessage: getMessageStub } + }).getMessage + + const event = { pathParameters: { id: 'test123' } } + await getMessage(event) + + Code.expect(getMessageStub.callCount).to.equal(1) + Code.expect(getMessageStub.calledWith(event, true)).to.be.true() + }) + + lab.test('Returns the result from getMessage helper', async () => { + const expectedResult = { + statusCode: 200, + headers: { 'content-type': 'application/xml' }, + body: 'v2 alert' + } + + const getMessageStub = sinon.stub().resolves(expectedResult) + + const getMessage = Proxyquire('../../../../lib/functions/v2/getMessage', { + '../../helpers/message': { getMessage: getMessageStub } + }).getMessage + + const event = { pathParameters: { id: 'test123' } } + const result = await getMessage(event) + + Code.expect(result).to.equal(expectedResult) + }) +}) diff --git a/test/lib/functions/v2/getMessagesAtom.js b/test/lib/functions/v2/getMessagesAtom.js new file mode 100644 index 0000000..ee37f16 --- /dev/null +++ b/test/lib/functions/v2/getMessagesAtom.js @@ -0,0 +1,44 @@ +'use strict' + +const Lab = require('@hapi/lab') +const lab = exports.lab = Lab.script() +const Code = require('@hapi/code') +const sinon = require('sinon') +const Proxyquire = require('proxyquire').noCallThru() + +lab.experiment('getMessagesAtom v2 wrapper', () => { + lab.test('Calls messages helper with v2=true', async () => { + const messagesStub = sinon.stub().resolves({ + statusCode: 200, + headers: { 'content-type': 'application/xml' }, + body: 'test' + }) + + const getMessagesAtom = Proxyquire('../../../../lib/functions/v2/getMessagesAtom', { + '../../helpers/messages': { messages: messagesStub } + }).getMessagesAtom + + await getMessagesAtom() + + Code.expect(messagesStub.callCount).to.equal(1) + Code.expect(messagesStub.calledWith(true)).to.be.true() + }) + + lab.test('Returns the result from messages helper', async () => { + const expectedResult = { + statusCode: 200, + headers: { 'content-type': 'application/xml' }, + body: 'v2 feed' + } + + const messagesStub = sinon.stub().resolves(expectedResult) + + const getMessagesAtom = Proxyquire('../../../../lib/functions/v2/getMessagesAtom', { + '../../helpers/messages': { messages: messagesStub } + }).getMessagesAtom + + const result = await getMessagesAtom() + + Code.expect(result).to.equal(expectedResult) + }) +}) diff --git a/test/lib/helpers/message.js b/test/lib/helpers/message.js new file mode 100644 index 0000000..71e68f6 --- /dev/null +++ b/test/lib/helpers/message.js @@ -0,0 +1,335 @@ +'use strict' + +const Lab = require('@hapi/lab') +const lab = exports.lab = Lab.script() +const Code = require('@hapi/code') +const sinon = require('sinon') +const fs = require('fs') +const path = require('path') +const { getMessage } = require('../../../lib/helpers/message') +const service = require('../../../lib/helpers/service') +const getMessageXmlInvalid = fs.readFileSync(path.join(__dirname, '..', 'functions', 'data', 'getMessage-invalid.xml'), 'utf8') +const getMessageXmlValid = fs.readFileSync(path.join(__dirname, '..', 'functions', 'data', 'getMessage-valid.xml'), 'utf8') +let event + +lab.experiment('getMessage helper', () => { + lab.beforeEach(() => { + event = { + pathParameters: { + id: '4eb3b7350ab7aa443650fc9351f' + } + } + }) + + lab.experiment('getMessage v1 (v2=false)', () => { + lab.beforeEach(() => { + // mock service + service.getMessage = (query, params) => Promise.resolve({ + rows: [{ + getmessage: { + alert: 'test', + alert_v2: 'test v2' + } + }] + }) + }) + + lab.test('Returns v1 alert when v2=false', async () => { + const ret = await getMessage(event, false) + Code.expect(ret.statusCode).to.equal(200) + Code.expect(ret.headers['content-type']).to.equal('application/xml') + Code.expect(ret.body).to.equal('test') + }) + + lab.test('No data found test', async () => { + service.getMessage = (query, params) => Promise.resolve({ + rows: [] + }) + + const err = await Code.expect(getMessage(event, false)).to.reject() + Code.expect(err.message).to.equal('No message found') + }) + + lab.test('Incorrect database rows object (not array)', async () => { + service.getMessage = (query, params) => Promise.resolve({ + rows: 1 + }) + + const err = await Code.expect(getMessage(event, false)).to.reject() + Code.expect(err.message).to.equal('No message found') + }) + + lab.test('Incorrect database rows object (empty getmessage)', async () => { + service.getMessage = (query, params) => Promise.resolve({ + rows: [{}] + }) + + const err = await Code.expect(getMessage(event, false)).to.reject() + Code.expect(err.message).to.equal('No message found') + }) + + lab.test('Missing database rows object', async () => { + service.getMessage = (query, params) => Promise.resolve({ + no_rows: [] + }) + + const err = await Code.expect(getMessage(event, false)).to.reject() + Code.expect(err.message).to.equal('No message found') + }) + + lab.test('No database return', async () => { + service.getMessage = (query, params) => { + return new Promise((resolve, reject) => { + resolve() + }) + } + const err = await Code.expect(getMessage(event, false)).to.reject() + Code.expect(err.message).to.equal('No message found') + }) + + lab.test('Database error', async () => { + service.getMessage = (query, params) => Promise.reject(new Error('test error')) + + const err = await Code.expect(getMessage(event, false)).to.reject() + Code.expect(err.message).to.equal('test error') + }) + + lab.test('Event validation test (invalid id property)', async () => { + event.id = {} + await Code.expect(getMessage(event, false)).to.reject() + }) + + lab.test('Event validation test (missing pathParameters)', async () => { + event = {} + await Code.expect(getMessage(event, false)).to.reject() + }) + + lab.test('Invalid id format test', async () => { + event.pathParameters.id = 'invalid_id_format' + + await Code.expect(getMessage(event, false)).to.reject() + }) + + lab.test('Valid id format test', async () => { + event.pathParameters.id = 'a1b2c3' + const result = await getMessage(event, false) + const body = result.body + + Code.expect(body).to.equal('test') + }) + + lab.test('XSD validation logs errors for invalid alert but continues', async () => { + const consoleLogStub = sinon.stub(console, 'log') + try { + service.getMessage = () => Promise.resolve({ + rows: [{ + getmessage: { + alert: getMessageXmlInvalid + } + }] + }) + await getMessage(event, false) + Code.expect(consoleLogStub.callCount).to.equal(2) + Code.expect(consoleLogStub.getCall(0).args[0]).to.equal('CAP get message failed validation') + Code.expect(consoleLogStub.getCall(1).args[0]).to.equal('[{"rawMessage":"message.xml:19: Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}geocode\': This element is not expected. Expected is ( {urn:oasis:names:tc:emergency:cap:1.2}areaDesc ).","message":"Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}geocode\': This element is not expected. Expected is ( {urn:oasis:names:tc:emergency:cap:1.2}areaDesc ).","loc":{"fileName":"message.xml","lineNumber":19}}]') + } finally { + consoleLogStub.restore() + } + }) + + lab.test('XSD validation does not log for valid alert', async () => { + const consoleLogStub = sinon.stub(console, 'log') + try { + service.getMessage = () => Promise.resolve({ + rows: [{ + getmessage: { + alert: getMessageXmlValid + } + }] + }) + await getMessage(event, false) + Code.expect(consoleLogStub.callCount).to.equal(0) + } finally { + consoleLogStub.restore() + } + }) + }) + + lab.experiment('getMessage v2 (v2=true)', () => { + lab.beforeEach(() => { + // mock service + service.getMessage = (query, params) => Promise.resolve({ + rows: [{ + getmessage: { + alert: 'test', + alert_v2: 'test v2' + } + }] + }) + }) + + lab.test('Returns v2 alert when v2=true', async () => { + const ret = await getMessage(event, true) + Code.expect(ret.statusCode).to.equal(200) + Code.expect(ret.headers['content-type']).to.equal('application/xml') + Code.expect(ret.body).to.equal('test v2') + }) + + lab.test('No data found test', async () => { + service.getMessage = (query, params) => Promise.resolve({ + rows: [] + }) + + const err = await Code.expect(getMessage(event, true)).to.reject() + Code.expect(err.message).to.equal('No message found') + }) + + lab.test('Incorrect database rows object (not array)', async () => { + service.getMessage = (query, params) => Promise.resolve({ + rows: 1 + }) + + const err = await Code.expect(getMessage(event, true)).to.reject() + Code.expect(err.message).to.equal('No message found') + }) + + lab.test('Incorrect database rows object (empty getmessage)', async () => { + service.getMessage = (query, params) => Promise.resolve({ + rows: [{}] + }) + + const err = await Code.expect(getMessage(event, true)).to.reject() + Code.expect(err.message).to.equal('No message found') + }) + + lab.test('Missing database rows object', async () => { + service.getMessage = (query, params) => Promise.resolve({ + no_rows: [] + }) + + const err = await Code.expect(getMessage(event, true)).to.reject() + Code.expect(err.message).to.equal('No message found') + }) + + lab.test('No database return', async () => { + service.getMessage = (query, params) => { + return new Promise((resolve, reject) => { + resolve() + }) + } + const err = await Code.expect(getMessage(event, true)).to.reject() + Code.expect(err.message).to.equal('No message found') + }) + + lab.test('Database error', async () => { + service.getMessage = (query, params) => Promise.reject(new Error('test error')) + + const err = await Code.expect(getMessage(event, true)).to.reject() + Code.expect(err.message).to.equal('test error') + }) + + lab.test('Event validation test (invalid id property)', async () => { + event.id = {} + await Code.expect(getMessage(event, true)).to.reject() + }) + + lab.test('Event validation test (missing pathParameters)', async () => { + event = {} + await Code.expect(getMessage(event, true)).to.reject() + }) + + lab.test('Invalid id format test', async () => { + event.pathParameters.id = 'invalid_id_format' + + await Code.expect(getMessage(event, true)).to.reject() + }) + + lab.test('Valid id format test', async () => { + event.pathParameters.id = 'a1b2c3' + const result = await getMessage(event, true) + const body = result.body + + Code.expect(body).to.equal('test v2') + }) + + lab.test('XSD validation logs errors for invalid alert but continues', async () => { + const consoleLogStub = sinon.stub(console, 'log') + try { + service.getMessage = () => Promise.resolve({ + rows: [{ + getmessage: { + alert_v2: getMessageXmlInvalid + } + }] + }) + await getMessage(event, true) + Code.expect(consoleLogStub.callCount).to.equal(2) + Code.expect(consoleLogStub.getCall(0).args[0]).to.equal('CAP get message failed validation') + Code.expect(consoleLogStub.getCall(1).args[0]).to.equal('[{"rawMessage":"message.xml:19: Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}geocode\': This element is not expected. Expected is ( {urn:oasis:names:tc:emergency:cap:1.2}areaDesc ).","message":"Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}geocode\': This element is not expected. Expected is ( {urn:oasis:names:tc:emergency:cap:1.2}areaDesc ).","loc":{"fileName":"message.xml","lineNumber":19}}]') + } finally { + consoleLogStub.restore() + } + }) + + lab.test('XSD validation does not log for valid alert', async () => { + const consoleLogStub = sinon.stub(console, 'log') + try { + service.getMessage = () => Promise.resolve({ + rows: [{ + getmessage: { + alert_v2: getMessageXmlValid + } + }] + }) + await getMessage(event, true) + Code.expect(consoleLogStub.callCount).to.equal(0) + } finally { + consoleLogStub.restore() + } + }) + }) + + lab.experiment('Edge cases and behavior differences', () => { + lab.beforeEach(() => { + service.getMessage = (query, params) => Promise.resolve({ + rows: [{ + getmessage: { + alert: 'v1 content', + alert_v2: 'v2 content' + } + }] + }) + }) + + lab.test('Returns different content for v1 vs v2', async () => { + const retV1 = await getMessage(event, false) + const retV2 = await getMessage(event, true) + + Code.expect(retV1.body).to.equal('v1 content') + Code.expect(retV2.body).to.equal('v2 content') + Code.expect(retV1.body).to.not.equal(retV2.body) + }) + + lab.test('Both v1 and v2 return same status code and headers', async () => { + const retV1 = await getMessage(event, false) + const retV2 = await getMessage(event, true) + + Code.expect(retV1.statusCode).to.equal(retV2.statusCode) + Code.expect(retV1.headers).to.equal(retV2.headers) + }) + + lab.test('Logs correct message id when no message found', async () => { + const consoleLogStub = sinon.stub(console, 'log') + service.getMessage = () => Promise.resolve({ rows: [] }) + + try { + await getMessage(event, false) + } catch (err) { + Code.expect(consoleLogStub.callCount).to.equal(1) + Code.expect(consoleLogStub.getCall(0).args[0]).to.equal('No message found for 4eb3b7350ab7aa443650fc9351f') + } finally { + consoleLogStub.restore() + } + }) + }) +}) diff --git a/test/lib/helpers/messages.js b/test/lib/helpers/messages.js new file mode 100644 index 0000000..15b0761 --- /dev/null +++ b/test/lib/helpers/messages.js @@ -0,0 +1,280 @@ +'use strict' + +const Lab = require('@hapi/lab') +const lab = exports.lab = Lab.script() +const Code = require('@hapi/code') +const { messages } = require('../../../lib/helpers/messages') +const service = require('../../../lib/helpers/service') +let CPX_AGW_URL + +lab.experiment('messages helper', () => { + lab.before(() => { + CPX_AGW_URL = process.env.CPX_AGW_URL + process.env.CPX_AGW_URL = 'http://localhost:3000' + }) + + lab.after(() => { + process.env.CPX_AGW_URL = CPX_AGW_URL + }) + + lab.experiment('messages v1 (v2=false)', () => { + lab.beforeEach(() => { + // mock database query + service.getAllMessages = (query) => { + return new Promise((resolve, reject) => { + resolve({ + rows: [{ + fwis_code: 'test_fwis_code', + alert: 'test', + sent: new Date(), + identifier: '4eb3b7350ab7aa443650fc9351f' + }] + }) + }) + } + }) + + lab.test('Returns v1 atom feed with correct URLs', async () => { + const ret = await messages(false) + Code.expect(ret.statusCode).to.equal(200) + Code.expect(ret.headers['content-type']).to.equal('application/xml') + Code.expect(ret.body).to.contain('http://localhost:3000/messages.atom') + Code.expect(ret.body).to.contain('http://localhost:3000/message/4eb3b7350ab7aa443650fc9351f') + Code.expect(ret.body).to.not.contain('/v2/') + }) + + lab.test('Handles bad rows returned', async () => { + service.getAllMessages = (query) => { + return new Promise((resolve, reject) => { + resolve({ + rows: 1 + }) + }) + } + const ret = await messages(false) + Code.expect(ret.statusCode).to.equal(200) + Code.expect(ret.headers['content-type']).to.equal('application/xml') + }) + + lab.test('Handles no return from database', async () => { + service.getAllMessages = (query) => { + return new Promise((resolve, reject) => { + resolve() + }) + } + const ret = await messages(false) + Code.expect(ret.statusCode).to.equal(200) + Code.expect(ret.headers['content-type']).to.equal('application/xml') + }) + + lab.test('Throws error on database failure', async () => { + service.getAllMessages = (query) => { + return new Promise((resolve, reject) => { + reject(new Error('test error')) + }) + } + const err = await Code.expect(messages(false)).to.reject() + Code.expect(err.message).to.equal('test error') + }) + + lab.test('Includes feed metadata', async () => { + const ret = await messages(false) + Code.expect(ret.body).to.contain('Flood warnings for England') + Code.expect(ret.body).to.contain('Environment Agency CAP XML flood warnings') + Code.expect(ret.body).to.contain('Environment Agency') + Code.expect(ret.body).to.contain('enquiries@environment-agency.gov.uk') + }) + + lab.test('Includes entry for each message', async () => { + service.getAllMessages = () => { + return Promise.resolve({ + rows: [ + { + fwis_code: 'AREA1', + alert: 'test1', + sent: new Date('2025-01-01'), + identifier: 'id1' + }, + { + fwis_code: 'AREA2', + alert: 'test2', + sent: new Date('2025-01-02'), + identifier: 'id2' + } + ] + }) + } + const ret = await messages(false) + Code.expect(ret.body).to.contain('<![CDATA[AREA1]]>') + Code.expect(ret.body).to.contain('<![CDATA[AREA2]]>') + Code.expect(ret.body).to.contain('http://localhost:3000/message/id1') + Code.expect(ret.body).to.contain('http://localhost:3000/message/id2') + }) + }) + + lab.experiment('messages v2 (v2=true)', () => { + lab.beforeEach(() => { + // mock database query + service.getAllMessages = (query) => { + return new Promise((resolve, reject) => { + resolve({ + rows: [{ + fwis_code: 'test_fwis_code', + alert: 'test', + sent: new Date(), + identifier: '4eb3b7350ab7aa443650fc9351f', + identifier_v2: '2.49.0.0.826.1.YYYYMMDDHHMMSS.4eb3b7350ab7aa443650fc9351f' + }] + }) + }) + } + }) + + lab.test('Returns v2 atom feed with correct URLs', async () => { + const ret = await messages(true) + Code.expect(ret.statusCode).to.equal(200) + Code.expect(ret.headers['content-type']).to.equal('application/xml') + Code.expect(ret.body).to.contain('http://localhost:3000/v2/messages.atom') + Code.expect(ret.body).to.contain('http://localhost:3000/v2/message/4eb3b7350ab7aa443650fc9351f') + }) + + lab.test('Handles bad rows returned', async () => { + service.getAllMessages = (query) => { + return new Promise((resolve, reject) => { + resolve({ + rows: 1 + }) + }) + } + const ret = await messages(true) + Code.expect(ret.statusCode).to.equal(200) + Code.expect(ret.headers['content-type']).to.equal('application/xml') + }) + + lab.test('Handles no return from database', async () => { + service.getAllMessages = (query) => { + return new Promise((resolve, reject) => { + resolve() + }) + } + const ret = await messages(true) + Code.expect(ret.statusCode).to.equal(200) + Code.expect(ret.headers['content-type']).to.equal('application/xml') + }) + + lab.test('Throws error on database failure', async () => { + service.getAllMessages = (query) => { + return new Promise((resolve, reject) => { + reject(new Error('test error')) + }) + } + const err = await Code.expect(messages(true)).to.reject() + Code.expect(err.message).to.equal('test error') + }) + + lab.test('Includes feed metadata', async () => { + const ret = await messages(true) + Code.expect(ret.body).to.contain('Flood warnings for England') + Code.expect(ret.body).to.contain('Environment Agency CAP XML flood warnings') + Code.expect(ret.body).to.contain('Environment Agency') + Code.expect(ret.body).to.contain('enquiries@environment-agency.gov.uk') + }) + + lab.test('Includes entry for each message', async () => { + service.getAllMessages = () => { + return Promise.resolve({ + rows: [ + { + fwis_code: 'AREA1', + alert: 'test1', + sent: new Date('2025-01-01'), + identifier: 'id1', + identifier_v2: '2.49.0.0.826.1.20250101000000.id1' + }, + { + fwis_code: 'AREA2', + alert: 'test2', + sent: new Date('2025-01-02'), + identifier: 'id2', + identifier_v2: '2.49.0.0.826.1.20250102000000.id2' + } + ] + }) + } + const ret = await messages(true) + Code.expect(ret.body).to.contain('<![CDATA[AREA1]]>') + Code.expect(ret.body).to.contain('<![CDATA[AREA2]]>') + Code.expect(ret.body).to.contain('http://localhost:3000/v2/message/id1') + Code.expect(ret.body).to.contain('http://localhost:3000/v2/message/id2') + }) + }) + + lab.experiment('Edge cases and behavior differences', () => { + lab.beforeEach(() => { + service.getAllMessages = () => { + return Promise.resolve({ + rows: [{ + fwis_code: 'TEST_CODE', + alert: 'test', + sent: new Date('2025-01-01T12:00:00Z'), + identifier: 'test_id', + identifier_v2: '2.49.0.0.826.1.20250101120000.test_id' + }] + }) + } + }) + + lab.test('V1 and V2 feeds have different URI prefixes', async () => { + const retV1 = await messages(false) + const retV2 = await messages(true) + + Code.expect(retV1.body).to.contain('http://localhost:3000/messages.atom') + Code.expect(retV1.body).to.not.contain('/v2/') + + Code.expect(retV2.body).to.contain('http://localhost:3000/v2/messages.atom') + Code.expect(retV2.body).to.contain('/v2/message/') + }) + + lab.test('Both v1 and v2 return same status code and headers', async () => { + const retV1 = await messages(false) + const retV2 = await messages(true) + + Code.expect(retV1.statusCode).to.equal(retV2.statusCode) + Code.expect(retV1.headers).to.equal(retV2.headers) + }) + + lab.test('Empty database returns valid empty feed for both versions', async () => { + service.getAllMessages = () => Promise.resolve({ rows: [] }) + + const retV1 = await messages(false) + const retV2 = await messages(true) + + Code.expect(retV1.statusCode).to.equal(200) + Code.expect(retV2.statusCode).to.equal(200) + Code.expect(retV1.body).to.contain(' { + service.getAllMessages = () => { + return Promise.resolve({ + rows: Array.from({ length: 5 }, (_, i) => ({ + fwis_code: `AREA${i}`, + alert: `test${i}`, + sent: new Date(`2025-01-0${i + 1}`), + identifier: `id${i}`, + identifier_v2: `2.49.0.0.826.1.2025010${i + 1}000000.id${i}` + })) + }) + } + + const retV1 = await messages(false) + const retV2 = await messages(true) + + for (let i = 0; i < 5; i++) { + Code.expect(retV1.body).to.contain(`<![CDATA[AREA${i}]]>`) + Code.expect(retV2.body).to.contain(`<![CDATA[AREA${i}]]>`) + } + }) + }) +}) diff --git a/test/lib/models/message.js b/test/lib/models/message.js index 8edbdc1..66ea022 100644 --- a/test/lib/models/message.js +++ b/test/lib/models/message.js @@ -3,47 +3,117 @@ const Lab = require('@hapi/lab') const lab = exports.lab = Lab.script() const Code = require('@hapi/code') - +const sinon = require('sinon') +const fs = require('node:fs') +const path = require('node:path') const Message = require('../../../lib/models/message') +let clock +const xml = fs.readFileSync(path.join(__dirname, '..', 'functions', 'data', 'nws-alert.xml'), 'utf8') + +const blankXml = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` -const xml = ` +const blankXml2 = ` - 123456 - www.gov.uk/environment-agency - 2026-05-28T11:00:02-00:00 - Actual - Alert - Flood warning service - Public + + + + + + + + + - en-GB - Met - - Immediate - Minor - Likely - 2026-05-29T11:00:02-00:00 - Environment Agency + + + + + + + + + + + + - Area description - points + + - TargetAreaCode - + + ` +const blankXmlMissingFields = ` + + + + + + + + + + + + + + + + + + + +` + lab.experiment('Message class', () => { - let message + let message, messageV2 lab.beforeEach(() => { + clock = sinon.useFakeTimers(new Date('2020-01-01T00:00:00Z').getTime()) message = new Message(xml) + messageV2 = new Message(xml) + }) + + lab.afterEach(() => { + clock.restore() + sinon.restore() }) lab.test('parses identifier', () => { - Code.expect(message.identifier).to.equal('123456') + Code.expect(message.identifier).to.equal('4eb3b7350ab7aa443650fc9351f02940E') }) lab.test('parses sender', () => { @@ -51,7 +121,7 @@ lab.experiment('Message class', () => { }) lab.test('parses fwisCode (geocode value)', () => { - Code.expect(message.fwisCode).to.equal('TESTAREA') + Code.expect(message.fwisCode).to.equal('TESTAREA1') }) lab.test('parses msgType', () => { @@ -73,11 +143,11 @@ lab.experiment('Message class', () => { }) lab.test('parses sent timestamp', () => { - Code.expect(message.sent).to.equal('2026-05-28T11:00:02-00:00') + Code.expect(message.sent).to.equal('2025-11-06T08:00:27+00:00') }) lab.test('parses expires timestamp', () => { - Code.expect(message.expires).to.equal('2026-05-29T11:00:02-00:00') + Code.expect(message.expires).to.equal('2025-11-16T08:00:27+00:00') }) lab.test('references defaults to empty string when missing', () => { @@ -94,10 +164,9 @@ lab.experiment('Message class', () => { }) lab.test('does not add references if value is falsy', () => { - // Initial: no references Code.expect(message.references).to.equal('') - message.references = '' // falsy value - Code.expect(message.references).to.equal('') // still unchanged + message.references = '' + Code.expect(message.references).to.equal('') Code.expect(message.toString()).to.not.include('') }) @@ -112,17 +181,136 @@ lab.experiment('Message class', () => { Code.expect(message.toString()).to.include('REF2') }) + lab.test('parses quickdial number from instruction', () => { + Code.expect(message.quickdialNumber).to.equal('210010') + }) + + lab.test('parses instruction', () => { + Code.expect(message.instruction).to.equal(`instructions + - For access to flood warning information offline call Floodline on 0345 988 1188 using quickdial code: 210010. + `) + }) + lab.test('toString returns valid XML string containing identifier', () => { const xmlOut = message.toString() Code.expect(xmlOut).to.be.a.string() - Code.expect(xmlOut).to.include('123456') + Code.expect(xmlOut).to.include('4eb3b7350ab7aa443650fc9351f02940E') }) lab.test('putQuery generates SQL insert with correct values', () => { - const sql = message.putQuery() - Code.expect(sql.text).to.equal('INSERT INTO "messages" ("identifier", "msg_type", "references", "alert", "fwis_code", "expires", "sent", "created") VALUES ($1, $2, $3, $4, $5, $6, $7, $8)') - Code.expect(sql.values).to.include('123456') - Code.expect(sql.values).to.include('TESTAREA') - Code.expect(sql.values).to.include('2026-05-29T11:00:02-00:00') + const sql = message.putQuery(message, messageV2) + Code.expect(sql.text).to.equal('INSERT INTO "messages" ("identifier", "msg_type", "references", "alert", "fwis_code", "expires", "sent", "created", "identifier_v2", "references_v2", "alert_v2") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)') + // TODO need to test for more values and v2 values here + Code.expect(sql.values[0]).to.equal('4eb3b7350ab7aa443650fc9351f02940E') + Code.expect(sql.values[1]).to.equal('Alert') + Code.expect(sql.values[2]).to.be.empty() + Code.expect(sql.values[3]).to.not.be.empty() + Code.expect(sql.values[4]).to.equal('TESTAREA1') + Code.expect(sql.values[5]).to.equal('2025-11-16T08:00:27+00:00') + Code.expect(sql.values[6]).to.equal('2025-11-06T08:00:27+00:00') + Code.expect(sql.values[7]).to.equal('2020-01-01T00:00:00.000Z') // TODO: bug change to not use Zulu shorthand timezone + Code.expect(sql.values[8]).to.equal('4eb3b7350ab7aa443650fc9351f02940E') + Code.expect(sql.values[9]).to.be.empty() + Code.expect(sql.values[10]).to.not.be.empty() + }) + + lab.test('blank message results in blank fields', () => { + const messageBlank = new Message(blankXml) + Code.expect(messageBlank.fwisCode).to.equal('') + Code.expect(messageBlank.identifier).to.equal('') + Code.expect(messageBlank.sender).to.equal('') + Code.expect(messageBlank.msgType).to.equal('') + Code.expect(messageBlank.references).to.equal('') + Code.expect(messageBlank.status).to.equal('') + Code.expect(messageBlank.expires).to.equal('') + Code.expect(messageBlank.instruction).to.equal('') + Code.expect(messageBlank.quickdialNumber).to.equal('') + Code.expect(messageBlank.sent).to.equal('') + Code.expect(messageBlank.code).to.equal('') + Code.expect(messageBlank.event).to.equal('') + Code.expect(messageBlank.severity).to.equal('') + Code.expect(messageBlank.onset).to.equal('') + Code.expect(messageBlank.headline).to.equal('') + Code.expect(messageBlank.areaDesc).to.equal('') + }) + + lab.test('Test setters with blank message with syntax', () => { + const messageBlank = new Message(blankXml2) + messageBlank.identifier = 'ID123' + messageBlank.msgType = 'Alert' + messageBlank.references = 'REF123' + messageBlank.status = 'Actual' + messageBlank.code = 'CODE123' + messageBlank.event = 'Test Event' + messageBlank.severity = 'Severe' + messageBlank.onset = '2026-06-01T10:00:00-00:00' + messageBlank.headline = 'Test Headline' + messageBlank.instruction = 'Test Instruction' + + Code.expect(messageBlank.identifier).to.equal('ID123') + Code.expect(messageBlank.references).to.equal('REF123') + Code.expect(messageBlank.msgType).to.equal('Update') // references setter flips msgType + Code.expect(messageBlank.status).to.equal('Actual') + Code.expect(messageBlank.code).to.equal('CODE123') + Code.expect(messageBlank.event).to.equal('Test Event') + Code.expect(messageBlank.severity).to.equal('Severe') + Code.expect(messageBlank.onset).to.equal('2026-06-01T10:00:00-00:00') + Code.expect(messageBlank.headline).to.equal('Test Headline') + Code.expect(messageBlank.instruction).to.equal('Test Instruction') + }) + lab.test('Test setters with blank message and missing fields with syntax', () => { + const messageBlank = new Message(blankXmlMissingFields) + messageBlank.identifier = 'ID123' + messageBlank.msgType = 'Alert' + messageBlank.references = 'REF123' + messageBlank.status = 'Actual' + messageBlank.code = 'CODE123' + messageBlank.event = 'Test Event' + messageBlank.severity = 'Severe' + messageBlank.onset = '2026-06-01T10:00:00-00:00' + messageBlank.headline = 'Test Headline' + messageBlank.instruction = 'Test Instruction' + + Code.expect(messageBlank.identifier).to.equal('ID123') + Code.expect(messageBlank.references).to.equal('REF123') + Code.expect(messageBlank.msgType).to.equal('Update') // references setter flips msgType + Code.expect(messageBlank.status).to.equal('Actual') + Code.expect(messageBlank.code).to.equal('CODE123') + Code.expect(messageBlank.event).to.equal('Test Event') + Code.expect(messageBlank.severity).to.equal('Severe') + Code.expect(messageBlank.onset).to.equal('2026-06-01T10:00:00-00:00') + Code.expect(messageBlank.headline).to.equal('Test Headline') + Code.expect(messageBlank.instruction).to.equal('Test Instruction') + }) + + lab.test('Setting parameters on a message (no getter available, so must check XML)', () => { + const normalize = s => s.replace(/\r\n/g, '\n') + const messageBlankMissingFields = new Message(blankXmlMissingFields) + messageBlankMissingFields.addParameter('awareness_level', 'awareness level') + messageBlankMissingFields.addParameter('awareness_type', '12; Flooding') + messageBlankMissingFields.addParameter('impacts', 'headline') + messageBlankMissingFields.addParameter('use_polygon_over_geocode', 'true') + messageBlankMissingFields.addParameter('uk_ea_ta_code', 'fwisCode') + + Code.expect(normalize(messageBlankMissingFields.toString())).to.include(normalize(` + awareness_level + awareness level + + + awareness_type + 12; Flooding + + + impacts + headline + + + use_polygon_over_geocode + true + + + uk_ea_ta_code + fwisCode + `)) }) }) From 1edfc99c6c6e273172af258886172dfad4d4e4a5 Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Tue, 18 Nov 2025 10:23:29 +0000 Subject: [PATCH 2/6] sonarcloud review fixes --- docker/scripts/initialize-named-volumes.sh | 8 ++++---- ...space-folder-on-host-to-local-repository.sh | 16 ++++++++-------- docker/scripts/load-dummy-data.sh | 5 ++++- ...p-for-rootless-docker-with-dev-container.sh | 18 +++++++++--------- ...or-rootless-docker-without-dev-container.sh | 4 ++-- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/docker/scripts/initialize-named-volumes.sh b/docker/scripts/initialize-named-volumes.sh index bba45df..f8ea3f5 100755 --- a/docker/scripts/initialize-named-volumes.sh +++ b/docker/scripts/initialize-named-volumes.sh @@ -4,13 +4,13 @@ set -e # The macOS version of realpath does not support the -m switch so the GNU version # is needed. -if [ `uname` = "Darwin" ] && [ x`command -v grealpath` = "x" ]; then +if [ $(uname) = "Darwin" ] && [ x$(command -v grealpath) = "x" ]; then echo "GNU coreutils need to be installed to use realpath with the -m switch" exit 1 fi # If running on macOS use the GNU version of realpath. -if [ `uname` = "Darwin" ]; then +if [ $(uname) = "Darwin" ]; then alias realpath="grealpath" fi @@ -74,7 +74,7 @@ fi docker container create --name capxmlpgbootstraptemp -v capxmlpgbootstrap:/docker-entrypoint-initdb.d -v capxmlpgtmp:/tmp alpine echo Created capxmlpgbootstraptemp container docker cp ${CAP_XML_HOST_DIR}/docker/cap-xml-db/bootstrap-cap-xml-db.sh capxmlpgbootstraptemp:/docker-entrypoint-initdb.d/bootstrap-cap-xml-db.sh -(cd `realpath -m ${CAP_XML_HOST_DIR}`/../cap-xml-db && docker cp ./cx/0.0.1/setup.sql capxmlpgbootstraptemp:/tmp/setup.sql) +(cd $(realpath -m ${CAP_XML_HOST_DIR})/../cap-xml-db && docker cp ./cx/0.0.1/setup.sql capxmlpgbootstraptemp:/tmp/setup.sql) docker rm capxmlpgbootstraptemp echo Removed capxmlpgbootstraptemp container @@ -90,6 +90,6 @@ fi # https://stackoverflow.com/questions/37468788/what-is-the-right-way-to-add-data-to-an-existing-named-volume-in-docker docker container create --name capxmlliquibasetemp -v capxmlliquibase:/capxmldb alpine echo Created capxmlliquibasetemp container -(cd `realpath -m ${CAP_XML_HOST_DIR}`/../cap-xml-db/cx && docker cp . capxmlliquibasetemp:/capxmldb) +(cd $(realpath -m ${CAP_XML_HOST_DIR})/../cap-xml-db/cx && docker cp . capxmlliquibasetemp:/capxmldb) docker rm capxmlliquibasetemp echo Removed capxmlliquibasetemp container \ No newline at end of file diff --git a/docker/scripts/link-workspace-folder-on-host-to-local-repository.sh b/docker/scripts/link-workspace-folder-on-host-to-local-repository.sh index 252a131..5731885 100755 --- a/docker/scripts/link-workspace-folder-on-host-to-local-repository.sh +++ b/docker/scripts/link-workspace-folder-on-host-to-local-repository.sh @@ -2,31 +2,31 @@ # This script MUST be run on the host before attempting to create a development container. set -e -if [ `whoami` != root ]; then +if [ $(whoami) != root ]; then echo This script must be run as root exit 1 fi -if [ ! -d "$LOCAL_CAP_XML_DIR"/.git ] && [ x`echo $"$LOCAL_CAP_XML_DIR" | grep -E /cap-xml/?$` = "x" ]; then +if [ ! -d "$LOCAL_CAP_XML_DIR"/.git ] && [ x$(echo $"$LOCAL_CAP_XML_DIR" | grep -E /cap-xml/?$) = "x" ]; then echo LOCAL_CAP_XML_DIR must be set to the absolute path of the root of a local cap-xml repository exit 1 fi -if [ `uname` != "Linux" ] && [ `uname` != "Darwin" ]; then - echo "Unsupported operating system `uname` detected - Linux and macOS are supported" +if [ $(uname) != "Linux" ] && [ $(uname) != "Darwin" ]; then + echo "Unsupported operating system $(uname) detected - Linux and macOS are supported" exit 1 fi # The macOS version of realpath does not support the -m switch so the GNU version # is needed. -if [ `uname` = "Darwin" ] && [ x`command -v grealpath` = "x" ]; then +if [ $(uname) = "Darwin" ] && [ x$(command -v grealpath) = "x" ]; then echo "GNU coreutils need to be installed to use realpath with the -m switch" exit 1 fi # If running on macOS use the GNU version of realpath. -if [ `uname` = "Darwin" ]; then +if [ $(uname) = "Darwin" ]; then alias realpath="grealpath" fi @@ -65,11 +65,11 @@ CAP_XML_VOLUME_WORKSPACE_DIR=/workspaces/cap-xml # (see https://apple.stackexchange.com/questions/388236/unable-to-create-folder-in-root-of-macintosh-hd), # /workspaces/cap-xml cannot be created. Container volume based running/debugging is NOT supported using default # macOS configuration accordingly. -if [ `uname` = "Linux" ] && [ ! -L "$CAP_XML_VOLUME_WORKSPACE_DIR" ] && [ $(realpath -m "$CAP_XML_VOLUME_WORKSPACE_DIR") != $(realpath -m "$CAP_XML_WORKSPACE_DIR") ]; then +if [ $(uname) = "Linux" ] && [ ! -L "$CAP_XML_VOLUME_WORKSPACE_DIR" ] && [ $(realpath -m "$CAP_XML_VOLUME_WORKSPACE_DIR") != $(realpath -m "$CAP_XML_WORKSPACE_DIR") ]; then mkdir -p /workspaces ln -s "$CAP_XML_WORKSPACE_DIR" "$CAP_XML_VOLUME_WORKSPACE_DIR" echo Created symbolic link from "$CAP_XML_VOLUME_WORKSPACE_DIR" to "$CAP_XML_WORKSPACE_DIR" -elif [ `uname` = "Darwin" ]; then +elif [ $(uname) = "Darwin" ]; then echo "macOS detected - WARNING - Running/debugging is only supported when creating a development container from a local cap-xml repository" fi diff --git a/docker/scripts/load-dummy-data.sh b/docker/scripts/load-dummy-data.sh index 21fd070..da2b4d8 100755 --- a/docker/scripts/load-dummy-data.sh +++ b/docker/scripts/load-dummy-data.sh @@ -10,6 +10,9 @@ BASE_AREA="TESTAREA" DATA_FILE="test/lib/functions/data/nws-alert.xml" LAMBDA_URL=http://$(awslocal apigateway get-rest-apis | jq -r ".items[0].id").execute-api.localhost.localstack.cloud:4566/local/message +# Calculate tomorrow's date +TOMORROW=$(date -u -d "+1 day" +"%Y-%m-%dT%H:%M:%S+00:00") + # Loop 10 times i=1 while [ $i -le 10 ]; do @@ -21,7 +24,7 @@ while [ $i -le 10 ]; do # Perform find and replace, then send with curl curl -X POST "$LAMBDA_URL" \ -H "Content-Type: text/xml" \ - -d "$(sed -e "s/${BASE_GUID}/${NEW_GUID}/g" -e "s/${BASE_AREA}/${NEW_AREA}/g" "$DATA_FILE")" + -d "$(sed -e "s/${BASE_GUID}/${NEW_GUID}/g" -e "s/${BASE_AREA}/${NEW_AREA}/g" -e "s|2025-11-16T08:00:27+00:00|${TOMORROW}|g" "$DATA_FILE")" echo "Done with POST $i" i=$((i + 1)) diff --git a/docker/scripts/setup-for-rootless-docker-with-dev-container.sh b/docker/scripts/setup-for-rootless-docker-with-dev-container.sh index 959e3f5..f232a6d 100755 --- a/docker/scripts/setup-for-rootless-docker-with-dev-container.sh +++ b/docker/scripts/setup-for-rootless-docker-with-dev-container.sh @@ -2,15 +2,15 @@ # This script MUST be run on the host before attempting to create a dev container using rootless Docker. set -e -if [ `whoami` != root ]; then +if [ $(whoami) != root ]; then echo This script must be run as root exit 1 fi HOST_UID=$(id -u "$CAP_XML_HOST_USERNAME") HOST_GID=$(id -g "$CAP_XML_HOST_USERNAME") -HOST_SUBUID=$(echo $(cat /etc/subuid | grep `echo $CAP_XML_HOST_USERNAME` | cut -d ':' -f 2)) -HOST_SUBGID=$(echo $(cat /etc/subgid | grep `echo $CAP_XML_HOST_USERNAME` | cut -d ':' -f 2)) +HOST_SUBUID=$(echo $(cat /etc/subuid | grep $(echo $CAP_XML_HOST_USERNAME) | cut -d ':' -f 2)) +HOST_SUBGID=$(echo $(cat /etc/subgid | grep $(echo $CAP_XML_HOST_USERNAME) | cut -d ':' -f 2)) if [ x"$HOST_SUBUID" = "x" ]; then echo The host user $CAP_XML_HOST_USERNAME does not have a subuid entry in /etc/subuid @@ -22,9 +22,9 @@ if [ x"$HOST_SUBGID" = "x" ]; then exit 1 fi -DEV_CONTAINER_UID_ON_HOST=`echo $((($HOST_SUBUID + $HOST_UID) - 1))` -DEV_CONTAINER_GID_ON_HOST=`echo $((($HOST_SUBGID + $HOST_GID) - 1))` -DEV_CONTAINER_DOCKER_GID_ON_HOST=$((($HOST_SUBGID + `getent group docker | cut -d ':' -f 3`) - 1)) +DEV_CONTAINER_UID_ON_HOST=$(echo $((($HOST_SUBUID + $HOST_UID) - 1))) +DEV_CONTAINER_GID_ON_HOST=$(echo $((($HOST_SUBGID + $HOST_GID) - 1))) +DEV_CONTAINER_DOCKER_GID_ON_HOST=$((($HOST_SUBGID + $(getent group docker | cut -d ':' -f 3)) - 1)) DOCKER_SOCKET=/var/run/docker.sock ROOTLESS_DOCKER_SOCKET=/run/user/$HOST_UID/docker.sock CAP_XML_WORKSPACE_DIR=/workspaces/cap-xml/ @@ -32,7 +32,7 @@ CAP_XML_WORKSPACE_DOCKER_DIR=${CAP_XML_WORKSPACE_DIR}docker WORKSPACE_FOLDER_HOST_OWNERSHIP=$DEV_CONTAINER_UID_ON_HOST:$DEV_CONTAINER_GID_ON_HOST WORKSPACE_DOCKER_FOLDER_HOST_OWNERSHIP=$HOST_UID:$HOST_GID -if [ ! -d "$LOCAL_CAP_XML_DIR"/.git ] && [ x`echo $"$LOCAL_CAP_XML_DIR" | grep -E /cap-xml/?$` = "x" ]; then +if [ ! -d "$LOCAL_CAP_XML_DIR"/.git ] && [ x$(echo $"$LOCAL_CAP_XML_DIR" | grep -E /cap-xml/?$) = "x" ]; then echo LOCAL_CAP_XML_DIR must be set to the absolute path of the root of a local cap-xml repository exit 1 fi @@ -64,7 +64,7 @@ fi # # If creating a dev container by cloning the cap-xml repository into a container volume, the dev container user has ownership # of items in the volume without risk of git reporting dubious ownership. -if [ `stat -c "%u:%g" $CAP_XML_WORKSPACE_DIR` != $WORKSPACE_FOLDER_HOST_OWNERSHIP ]; then +if [ $(stat -c "%u:%g" $CAP_XML_WORKSPACE_DIR) != $WORKSPACE_FOLDER_HOST_OWNERSHIP ]; then chown -R $WORKSPACE_FOLDER_HOST_OWNERSHIP $CAP_XML_WORKSPACE_DIR echo Changed UID:GID for $CAP_XML_WORKSPACE_DIR to $WORKSPACE_FOLDER_HOST_OWNERSHIP else @@ -73,7 +73,7 @@ fi # Ensure the local cap-xml repository docker directory hierarchy UID:GID is set to HOST_UID:HOST_GID so that # named Docker volumes can be created. -if [ `stat -c "%u:%g" $CAP_XML_WORKSPACE_DOCKER_DIR` != $WORKSPACE_DOCKER_FOLDER_HOST_OWNERSHIP ]; then +if [ $(stat -c "%u:%g" $CAP_XML_WORKSPACE_DOCKER_DIR) != $WORKSPACE_DOCKER_FOLDER_HOST_OWNERSHIP ]; then chown -R $WORKSPACE_DOCKER_FOLDER_HOST_OWNERSHIP $CAP_XML_WORKSPACE_DOCKER_DIR echo Changed UID:GID for $CAP_XML_WORKSPACE_DOCKER_DIR to $WORKSPACE_DOCKER_FOLDER_HOST_OWNERSHIP else diff --git a/docker/scripts/setup-for-rootless-docker-without-dev-container.sh b/docker/scripts/setup-for-rootless-docker-without-dev-container.sh index a84f755..9ccd2cc 100755 --- a/docker/scripts/setup-for-rootless-docker-without-dev-container.sh +++ b/docker/scripts/setup-for-rootless-docker-without-dev-container.sh @@ -4,7 +4,7 @@ set -e -if [ `whoami` != root ]; then +if [ $(whoami) != root ]; then echo This script must be run as root exit 1 fi @@ -13,7 +13,7 @@ HOST_UID=$(id -u "$CAP_XML_HOST_USERNAME") DOCKER_SOCKET=/var/run/docker.sock ROOTLESS_DOCKER_SOCKET=/run/user/$HOST_UID/docker.sock -if [ ! -d "$LOCAL_CAP_XML_DIR"/.git ] && [ x`echo $"$LOCAL_CAP_XML_DIR" | grep -E /cap-xml/?$` = "x" ]; then +if [ ! -d "$LOCAL_CAP_XML_DIR"/.git ] && [ x$(echo $"$LOCAL_CAP_XML_DIR" | grep -E /cap-xml/?$) = "x" ]; then echo LOCAL_CAP_XML_DIR must be set to the absolute path of the root of a local cap-xml repository exit 1 fi From b594b00f032e35dea2c857b9904037b45b912bb9 Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Tue, 18 Nov 2025 10:38:49 +0000 Subject: [PATCH 3/6] additional test assert for message v2 parameters --- test/lib/functions/processMessage.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/lib/functions/processMessage.js b/test/lib/functions/processMessage.js index d63fe65..209a0ea 100644 --- a/test/lib/functions/processMessage.js +++ b/test/lib/functions/processMessage.js @@ -87,6 +87,8 @@ const expectMessageV1 = (message, severity, status, references, previousReferenc } const expectMessageV2 = (message, severity, status, references, previousReferences, quickdialNumber) => { + const normalize = s => s.replace(/\r\n/g, '\n') + const messageString = normalize(message.toString()) const mapping = v2MessageMapping[severity] // Test message fields updated for message V2 Code.expect(message.identifier).to.equal(identifierV2) @@ -109,6 +111,27 @@ const expectMessageV2 = (message, severity, status, references, previousReferenc Code.expect(message.instruction).not.to.contain('- call Floodline on 0345 988 1188, using quickdial code 210010') Code.expect(message.instruction).not.to.contain('- For access to flood warning information offline call Floodline on 0345 988 1188 using') } + // Test for parameters + Code.expect(messageString).to.contain(` + awareness_level + ${mapping.awarenessLevel} + `) + Code.expect(messageString).to.contain(` + awareness_type + 12; Flooding + `) + Code.expect(messageString).to.contain(` + impacts + ${mapping.headline}: Rivers Lowther and Eamont + `) + Code.expect(messageString).to.contain(` + use_polygon_over_geocode + true + `) + Code.expect(messageString).to.contain(` + uk_ea_ta_code + TESTAREA1 + `) } // *********************************************************** From 98c4490ce21364aff001d43a7f46747a84ffe692 Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Wed, 19 Nov 2025 08:28:34 +0000 Subject: [PATCH 4/6] Code review requests --- docker/.env | 2 +- docker/scripts/register-api-gateway.sh | 1 - docker/scripts/register-lambda-functions.sh | 2 +- lib/models/message.js | 6 +++--- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docker/.env b/docker/.env index d3fe79e..7a8df33 100644 --- a/docker/.env +++ b/docker/.env @@ -43,7 +43,7 @@ LAMBDA_IGNORE_ARCHITECTURE=1 # debugging when cloning the remote repository into a container volume. DEBUG_HOST_ADDRESS=192.168.0.5 CPX_DB_HOST=capxmldb -NODEJS_VERSION=20 +NODEJS_VERSION=22 PGADMIN_DEFAULT_EMAIL=ubuntu@localhost.localdomain # Database associated values including well known secrets for local development diff --git a/docker/scripts/register-api-gateway.sh b/docker/scripts/register-api-gateway.sh index 51b979e..4f2414a 100755 --- a/docker/scripts/register-api-gateway.sh +++ b/docker/scripts/register-api-gateway.sh @@ -11,7 +11,6 @@ main() { cap_xml_rest_api_root_resource_id=$(awslocal apigateway get-resources --rest-api-id $cap_xml_rest_api_id | jq -r '.items[0].id') lambda_functions_dir="lib/functions" - #for lambda_function in "$lambda_functions_dir"/*; do find "$lambda_functions_dir" -type f -name "*.js" | while read -r lambda_function; do relative_path="${lambda_function#$lambda_functions_dir/}" dir_prefix=$(dirname "$relative_path") diff --git a/docker/scripts/register-lambda-functions.sh b/docker/scripts/register-lambda-functions.sh index 0b6c73a..faf5ab5 100755 --- a/docker/scripts/register-lambda-functions.sh +++ b/docker/scripts/register-lambda-functions.sh @@ -40,7 +40,7 @@ find "$lambda_functions_dir" -type f -name "*.js" | while read -r lambda_functio awslocal lambda create-function \ --function-name "$function_name" \ --code S3Bucket="hot-reload",S3Key="$(pwd)/" \ - --runtime nodejs20.x \ + --runtime nodejs${NODEJS_VERSION}.x \ --timeout $LAMBDA_TIMEOUT \ --role arn:aws:iam::000000000000:role/lambda-role \ --handler "$handler_path" \ diff --git a/lib/models/message.js b/lib/models/message.js index 951795e..5408909 100644 --- a/lib/models/message.js +++ b/lib/models/message.js @@ -152,11 +152,11 @@ class Message { return this.doc.getElementsByTagName(tagName)[0] } - addElement (parentTag, elTag, elValue) { - const parentEl = this.doc.getElementsByTagName(parentTag)[0] + addElement (afterTag, elTag, elValue) { + const afterTagEl = this.doc.getElementsByTagName(afterTag)[0] const newEl = this.doc.createElement(elTag) newEl.textContent = elValue - return parentEl.parentNode.insertBefore(newEl, parentEl.nextSibling) + return afterTagEl.parentNode.insertBefore(newEl, afterTagEl.nextSibling) } addParameter (name, value) { From 13f35fc93cc43c1f847fc9f5fd35f859a09bafb3 Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Wed, 17 Dec 2025 14:30:26 +0000 Subject: [PATCH 5/6] Updating v2 impact mapping --- lib/functions/processMessage.js | 4 ++-- lib/models/v2MessageMapping.js | 3 +++ test/lib/functions/processMessage.js | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/functions/processMessage.js b/lib/functions/processMessage.js index b8b7cf8..37ba1b4 100644 --- a/lib/functions/processMessage.js +++ b/lib/functions/processMessage.js @@ -180,9 +180,9 @@ const processMessageV2 = (message, lastMessage) => { messageV2.instruction = instruction } - messageV2.addParameter('awareness_level', severityV2Mapping[message.severity]?.awarenessLevel) + messageV2.addParameter('awareness_level', severityV2Mapping[message.severity]?.awarenessLevel || '') messageV2.addParameter('awareness_type', '12; Flooding') - messageV2.addParameter('impacts', messageV2.headline) + messageV2.addParameter('impacts', severityV2Mapping[message.severity]?.impact || '') messageV2.addParameter('use_polygon_over_geocode', 'true') messageV2.addParameter('uk_ea_ta_code', message.fwisCode) diff --git a/lib/models/v2MessageMapping.js b/lib/models/v2MessageMapping.js index 0bb80c5..42bcc5f 100644 --- a/lib/models/v2MessageMapping.js +++ b/lib/models/v2MessageMapping.js @@ -5,6 +5,7 @@ module.exports = { severity: 'Minor', description: 'Flood Alert', headline: 'Flooding is possible', + impact: 'Flooding is possible - be prepared', instruction: `Be prepared You should: @@ -29,6 +30,7 @@ To get the latest flood information, you can: severity: 'Severe', description: 'Flood Warning', headline: 'Flooding is expected', + impact: 'Flooding is expected - act now', instruction: `Act now You should: @@ -53,6 +55,7 @@ To get the latest flood information, you can: severity: 'Extreme', description: 'Severe Flood Warning', headline: 'Danger to life', + impact: 'Danger to life - act now', instruction: `Act now - danger to life You should: diff --git a/test/lib/functions/processMessage.js b/test/lib/functions/processMessage.js index 209a0ea..e8aec78 100644 --- a/test/lib/functions/processMessage.js +++ b/test/lib/functions/processMessage.js @@ -122,7 +122,7 @@ const expectMessageV2 = (message, severity, status, references, previousReferenc `) Code.expect(messageString).to.contain(` impacts - ${mapping.headline}: Rivers Lowther and Eamont + ${mapping.impact} `) Code.expect(messageString).to.contain(` use_polygon_over_geocode From db661215bc88d821bb71aa1daf1d3b45791fdcd5 Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Thu, 18 Dec 2025 09:29:38 +0000 Subject: [PATCH 6/6] Updating extended schema severity allowed options whitelist, extreme to be treated same as severe, unknown to be rejected --- lib/models/v2MessageMapping.js | 57 ++++++++-------- lib/schemas/additionalCapMessageSchema.js | 1 + .../lib/functions/processMessageValidation.js | 67 ++++++++++--------- 3 files changed, 68 insertions(+), 57 deletions(-) diff --git a/lib/models/v2MessageMapping.js b/lib/models/v2MessageMapping.js index 42bcc5f..983cc41 100644 --- a/lib/models/v2MessageMapping.js +++ b/lib/models/v2MessageMapping.js @@ -1,5 +1,33 @@ const quickdialSentence = '- call Floodline on 0345 988 1188, using quickdial code {{ quickdialNumber }}' +const extreme = { + severity: 'Extreme', + description: 'Severe Flood Warning', + headline: 'Danger to life', + impact: 'Danger to life - act now', + instruction: `Act now - danger to life + +You should: + +- call 999 if you are in immediate danger + +- go to Check for flooding for a map of the area and to monitor up-to-date local flood information – https://check-for-flooding.service.gov.uk/target-area/{{ fwisCode }} +- act on your personal flood plan if you have one - https://www.gov.uk/government/publications/personal-flood-plan +- follow the guidance in 'What to do before or during a flood' - https://www.gov.uk/help-during-flood + +You can also read more about what severe flood warnings are – [https://www.gov.uk/guidance/flood-alerts-and-warnings-what-they-are-and-what-to-do#severe-flood-warning] + +Stay up to date + +To get the latest flood information, you can: + +- go to Check for flooding +- monitor local weather, news and travel updates +{{ quickdialSentence }}`, + awarenessLevel: '4; red; Extreme', + quickdialSentence +} + module.exports = { Minor: { severity: 'Minor', @@ -51,31 +79,6 @@ To get the latest flood information, you can: awarenessLevel: '3; orange; Severe', quickdialSentence }, - Severe: { - severity: 'Extreme', - description: 'Severe Flood Warning', - headline: 'Danger to life', - impact: 'Danger to life - act now', - instruction: `Act now - danger to life - -You should: - -- call 999 if you are in immediate danger - -- go to Check for flooding for a map of the area and to monitor up-to-date local flood information – https://check-for-flooding.service.gov.uk/target-area/{{ fwisCode }} -- act on your personal flood plan if you have one - https://www.gov.uk/government/publications/personal-flood-plan -- follow the guidance in 'What to do before or during a flood' - https://www.gov.uk/help-during-flood - -You can also read more about what severe flood warnings are – [https://www.gov.uk/guidance/flood-alerts-and-warnings-what-they-are-and-what-to-do#severe-flood-warning] - -Stay up to date - -To get the latest flood information, you can: - -- go to Check for flooding -- monitor local weather, news and travel updates -{{ quickdialSentence }}`, - awarenessLevel: '4; red; Extreme', - quickdialSentence - } + Severe: extreme, + Extreme: extreme } diff --git a/lib/schemas/additionalCapMessageSchema.js b/lib/schemas/additionalCapMessageSchema.js index 77ffef3..31c9bda 100644 --- a/lib/schemas/additionalCapMessageSchema.js +++ b/lib/schemas/additionalCapMessageSchema.js @@ -9,6 +9,7 @@ const areaSchema = Joi.object({ const infoSchema = Joi.object({ event: Joi.array().items(Joi.string().min(1)).max(1).required(), + severity: Joi.array().items(Joi.string().valid('Extreme', 'Severe', 'Moderate', 'Minor')).max(1).required(), senderName: Joi.array().items(Joi.string().min(1)).max(1).required(), area: Joi.array().items(areaSchema) }).unknown(true) diff --git a/test/lib/functions/processMessageValidation.js b/test/lib/functions/processMessageValidation.js index fa8f816..b288074 100644 --- a/test/lib/functions/processMessageValidation.js +++ b/test/lib/functions/processMessageValidation.js @@ -136,7 +136,7 @@ lab.experiment('processMessage validation logging', () => { .reject() const errors = JSON.parse(ret.message.replace('[500] ', '')) - Code.expect(errors.length).to.equal(31) + Code.expect(errors.length).to.equal(35) // Helper to generate message asserts below // errors.forEach((er, i) => { // console.log(`Code.expect(errors[${i}].message).to.equal('${er.message.replace(/'/g, "\\'")}')`) @@ -154,25 +154,29 @@ lab.experiment('processMessage validation logging', () => { Code.expect(errors[9].message).to.equal('"alert.sender[0]" must be [www.gov.uk/environment-agency]') Code.expect(errors[10].message).to.equal('"alert.sender[0]" is not allowed to be empty') Code.expect(errors[11].message).to.equal('"alert.source[0]" is not allowed to be empty') - Code.expect(errors[12].message).to.equal('"alert.info[0].senderName[0]" is not allowed to be empty') - Code.expect(errors[13].message).to.equal('"alert.info[0].area[0].areaDesc[0]" is not allowed to be empty') - Code.expect(errors[14].message).to.equal('"alert.info[0].area[0].polygon[0]" is not allowed to be empty') + Code.expect(errors[12].message).to.equal('"alert.info[0].severity[0]" must be one of [Extreme, Severe, Moderate, Minor]') + Code.expect(errors[13].message).to.equal('"alert.info[0].severity[0]" is not allowed to be empty') + Code.expect(errors[14].message).to.equal('"alert.info[0].senderName[0]" is not allowed to be empty') + Code.expect(errors[15].message).to.equal('"alert.info[0].area[0].areaDesc[0]" is not allowed to be empty') + Code.expect(errors[16].message).to.equal('"alert.info[0].area[0].polygon[0]" is not allowed to be empty') // v2 errors - Code.expect(errors[15].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}sent\': \'\' is not a valid value of the local atomic type.') - Code.expect(errors[16].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}msgType\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Alert\', \'Update\', \'Cancel\', \'Ack\', \'Error\'}.') - Code.expect(errors[17].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}scope\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Public\', \'Restricted\', \'Private\'}.') - Code.expect(errors[18].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}category\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Geo\', \'Met\', \'Safety\', \'Security\', \'Rescue\', \'Fire\', \'Health\', \'Env\', \'Transport\', \'Infra\', \'CBRNE\', \'Other\'}.') - Code.expect(errors[19].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}urgency\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Immediate\', \'Expected\', \'Future\', \'Past\', \'Unknown\'}.') - Code.expect(errors[20].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}severity\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Extreme\', \'Severe\', \'Moderate\', \'Minor\', \'Unknown\'}.') - Code.expect(errors[21].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}certainty\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Observed\', \'Likely\', \'Possible\', \'Unlikely\', \'Unknown\'}.') - Code.expect(errors[22].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}onset\': \'\' is not a valid value of the local atomic type.') - Code.expect(errors[23].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}expires\': \'\' is not a valid value of the local atomic type.') - Code.expect(errors[25].message).to.equal('"alert.sender[0]" must be [www.gov.uk/environment-agency]') - Code.expect(errors[26].message).to.equal('"alert.sender[0]" is not allowed to be empty') - Code.expect(errors[27].message).to.equal('"alert.source[0]" is not allowed to be empty') - Code.expect(errors[28].message).to.equal('"alert.info[0].senderName[0]" is not allowed to be empty') - Code.expect(errors[29].message).to.equal('"alert.info[0].area[0].areaDesc[0]" is not allowed to be empty') - Code.expect(errors[30].message).to.equal('"alert.info[0].area[0].polygon[0]" is not allowed to be empty') + Code.expect(errors[17].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}sent\': \'\' is not a valid value of the local atomic type.') + Code.expect(errors[18].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}msgType\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Alert\', \'Update\', \'Cancel\', \'Ack\', \'Error\'}.') + Code.expect(errors[19].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}scope\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Public\', \'Restricted\', \'Private\'}.') + Code.expect(errors[20].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}category\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Geo\', \'Met\', \'Safety\', \'Security\', \'Rescue\', \'Fire\', \'Health\', \'Env\', \'Transport\', \'Infra\', \'CBRNE\', \'Other\'}.') + Code.expect(errors[21].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}urgency\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Immediate\', \'Expected\', \'Future\', \'Past\', \'Unknown\'}.') + Code.expect(errors[22].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}severity\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Extreme\', \'Severe\', \'Moderate\', \'Minor\', \'Unknown\'}.') + Code.expect(errors[23].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}certainty\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Observed\', \'Likely\', \'Possible\', \'Unlikely\', \'Unknown\'}.') + Code.expect(errors[24].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}onset\': \'\' is not a valid value of the local atomic type.') + Code.expect(errors[25].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}expires\': \'\' is not a valid value of the local atomic type.') + Code.expect(errors[27].message).to.equal('"alert.sender[0]" must be [www.gov.uk/environment-agency]') + Code.expect(errors[28].message).to.equal('"alert.sender[0]" is not allowed to be empty') + Code.expect(errors[29].message).to.equal('"alert.source[0]" is not allowed to be empty') + Code.expect(errors[30].message).to.equal('"alert.info[0].severity[0]" must be one of [Extreme, Severe, Moderate, Minor]') + Code.expect(errors[31].message).to.equal('"alert.info[0].severity[0]" is not allowed to be empty') + Code.expect(errors[32].message).to.equal('"alert.info[0].senderName[0]" is not allowed to be empty') + Code.expect(errors[33].message).to.equal('"alert.info[0].area[0].areaDesc[0]" is not allowed to be empty') + Code.expect(errors[34].message).to.equal('"alert.info[0].area[0].polygon[0]" is not allowed to be empty') Code.expect(awsStub.email.publishMessage.callCount).to.equal(1) }) @@ -220,7 +224,7 @@ lab.experiment('processMessage validation logging', () => { .to .reject() const errors = JSON.parse(ret.message.replace('[500] ', '')) - Code.expect(errors.length).to.equal(19) + Code.expect(errors.length).to.equal(22) Code.expect(errors[0].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}sent\': \'2026-05-28\' is not a valid value of the local atomic type.') Code.expect(errors[1].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}msgType\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Alert\', \'Update\', \'Cancel\', \'Ack\', \'Error\'}.') Code.expect(errors[2].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}scope\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Public\', \'Restricted\', \'Private\'}.') @@ -230,17 +234,20 @@ lab.experiment('processMessage validation logging', () => { Code.expect(errors[6].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}certainty\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Observed\', \'Likely\', \'Possible\', \'Unlikely\', \'Unknown\'}.') Code.expect(errors[7].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}expires\': \'2026-05-29\' is not a valid value of the local atomic type.') Code.expect(errors[8].message).to.equal('"alert.sender[0]" must be [www.gov.uk/environment-agency]') + Code.expect(errors[9].message).to.equal('"alert.info[0].severity[0]" must be one of [Extreme, Severe, Moderate, Minor]') // v2 errors - Code.expect(errors[9].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}sent\': \'2026-05-28\' is not a valid value of the local atomic type.') - Code.expect(errors[10].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}msgType\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Alert\', \'Update\', \'Cancel\', \'Ack\', \'Error\'}.') - Code.expect(errors[11].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}scope\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Public\', \'Restricted\', \'Private\'}.') - Code.expect(errors[12].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}category\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Geo\', \'Met\', \'Safety\', \'Security\', \'Rescue\', \'Fire\', \'Health\', \'Env\', \'Transport\', \'Infra\', \'CBRNE\', \'Other\'}.') - Code.expect(errors[13].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}urgency\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Immediate\', \'Expected\', \'Future\', \'Past\', \'Unknown\'}.') - Code.expect(errors[14].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}severity\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Extreme\', \'Severe\', \'Moderate\', \'Minor\', \'Unknown\'}.') - Code.expect(errors[15].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}certainty\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Observed\', \'Likely\', \'Possible\', \'Unlikely\', \'Unknown\'}.') - Code.expect(errors[16].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}onset\': \'2026-05-28\' is not a valid value of the local atomic type.') - Code.expect(errors[17].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}expires\': \'2026-05-29\' is not a valid value of the local atomic type.') - Code.expect(errors[18].message).to.equal('"alert.sender[0]" must be [www.gov.uk/environment-agency]') + Code.expect(errors[10].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}sent\': \'2026-05-28\' is not a valid value of the local atomic type.') + Code.expect(errors[11].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}msgType\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Alert\', \'Update\', \'Cancel\', \'Ack\', \'Error\'}.') + Code.expect(errors[12].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}scope\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Public\', \'Restricted\', \'Private\'}.') + Code.expect(errors[13].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}category\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Geo\', \'Met\', \'Safety\', \'Security\', \'Rescue\', \'Fire\', \'Health\', \'Env\', \'Transport\', \'Infra\', \'CBRNE\', \'Other\'}.') + Code.expect(errors[14].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}urgency\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Immediate\', \'Expected\', \'Future\', \'Past\', \'Unknown\'}.') + Code.expect(errors[15].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}severity\': [facet \'enumeration\'] The value \'\' is not an element of the set {\'Extreme\', \'Severe\', \'Moderate\', \'Minor\', \'Unknown\'}.') + Code.expect(errors[16].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}certainty\': [facet \'enumeration\'] The value \'invalid\' is not an element of the set {\'Observed\', \'Likely\', \'Possible\', \'Unlikely\', \'Unknown\'}.') + Code.expect(errors[17].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}onset\': \'2026-05-28\' is not a valid value of the local atomic type.') + Code.expect(errors[18].message).to.equal('Schemas validity error : Element \'{urn:oasis:names:tc:emergency:cap:1.2}expires\': \'2026-05-29\' is not a valid value of the local atomic type.') + Code.expect(errors[19].message).to.equal('"alert.sender[0]" must be [www.gov.uk/environment-agency]') + Code.expect(errors[20].message).to.equal('"alert.info[0].severity[0]" must be one of [Extreme, Severe, Moderate, Minor]') + Code.expect(errors[21].message).to.equal('"alert.info[0].severity[0]" is not allowed to be empty') Code.expect(awsStub.email.publishMessage.callCount).to.equal(1) })