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/.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/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/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 3f3171a..da2b4d8 100755 --- a/docker/scripts/load-dummy-data.sh +++ b/docker/scripts/load-dummy-data.sh @@ -7,9 +7,12 @@ 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 +# 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/register-api-gateway.sh b/docker/scripts/register-api-gateway.sh index cb43da7..4f2414a 100755 --- a/docker/scripts/register-api-gateway.sh +++ b/docker/scripts/register-api-gateway.sh @@ -11,16 +11,27 @@ 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") 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 +58,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 +120,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 +134,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 +157,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..faf5ab5 100755 --- a/docker/scripts/register-lambda-functions.sh +++ b/docker/scripts/register-lambda-functions.sh @@ -17,18 +17,33 @@ 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 \ --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 lib/functions/$function_name.$function_name \ + --handler "$handler_path" \ --environment "Variables={$custom_environment_variables}" \ --no-cli-pager sleep 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 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..37ba1b4 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', severityV2Mapping[message.severity]?.impact || '') + 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..5408909 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,38 +61,139 @@ 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) { 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) { + 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..983cc41 --- /dev/null +++ b/lib/models/v2MessageMapping.js @@ -0,0 +1,84 @@ +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', + description: 'Flood Alert', + headline: 'Flooding is possible', + impact: 'Flooding is possible - be prepared', + 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', + impact: 'Flooding is expected - act now', + 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: 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/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..e8aec78 100644 --- a/test/lib/functions/processMessage.js +++ b/test/lib/functions/processMessage.js @@ -4,24 +4,141 @@ 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 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) + 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') + } + // 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.impact} + `) + Code.expect(messageString).to.contain(` + use_polygon_over_geocode + true + `) + Code.expect(messageString).to.contain(` + uk_ea_ta_code + TESTAREA1 + `) +} +// *********************************************************** 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 +154,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 +372,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 +389,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..b288074 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(35) // Helper to generate message asserts below // errors.forEach((er, i) => { // console.log(`Code.expect(errors[${i}].message).to.equal('${er.message.replace(/'/g, "\\'")}')`) @@ -152,9 +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[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) }) @@ -202,7 +224,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(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\'}.') @@ -212,6 +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[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) }) 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 + `)) }) })