From 449b15f1efb655cf24b324011a00cb0803e36e5e Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Tue, 3 Feb 2026 13:06:33 +0000 Subject: [PATCH 1/6] Including wiremock for test double --- docker/dev-tools.yml | 9 ++ .../wiremock/mappings/third-party-api.json | 24 ++++ package-lock.json | 116 +++++++++++++++--- package.json | 1 + 4 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 docker/docker/wiremock/mappings/third-party-api.json diff --git a/docker/dev-tools.yml b/docker/dev-tools.yml index 8a8b3bb..fb36943 100644 --- a/docker/dev-tools.yml +++ b/docker/dev-tools.yml @@ -28,6 +28,15 @@ services: networks: ls: command: /bin/sh -c "lpm add postgresql && liquibase update" + mock-api: + image: wiremock/wiremock:latest + ports: + - "8081:8080" + volumes: + - ./docker/wiremock/mappings:/home/wiremock/mappings + command: ["--global-response-templating", "--verbose"] + networks: + ls: volumes: capxmlpgadmin: external: true diff --git a/docker/docker/wiremock/mappings/third-party-api.json b/docker/docker/wiremock/mappings/third-party-api.json new file mode 100644 index 0000000..e1159c7 --- /dev/null +++ b/docker/docker/wiremock/mappings/third-party-api.json @@ -0,0 +1,24 @@ +{ + "mappings": [ + { + "request": { + "method": "POST", + "urlPath": "/warnings" + }, + "response": { + "status": 200, + "body": "{\"warning\": {\"uuid\": \"{{randomValue type='UUID'}}\" } }" + } + }, + { + "request": { + "method": "POST", + "urlPath": "/tokens" + }, + "response": { + "status": 200, + "body": "{\"tokenType\": \"Bearer\", \"token\": \"{{randomValue length=64 type='ALPHANUMERIC'}}\", \"expiresIn\": \"300\" }" + } + } + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8fd5bc3..e6c2497 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@aws-sdk/client-sns": "3.981.0", "@xmldom/xmldom": "0.8.11", + "axios": "1.13.4", "feed": "5.2.0", "ioredis": "5.9.2", "joi": "18.0.2", @@ -2467,6 +2468,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2537,6 +2544,17 @@ "node": ">=8" } }, + "node_modules/axios": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2657,7 +2675,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2774,6 +2791,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2914,6 +2943,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -2962,7 +3000,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -3063,7 +3100,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3073,7 +3109,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3111,7 +3146,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3124,7 +3158,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3848,6 +3881,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3864,6 +3917,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3875,7 +3944,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3935,7 +4003,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3960,7 +4027,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -4089,7 +4155,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4197,7 +4262,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4210,7 +4274,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -4226,7 +4289,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -4992,7 +5054,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5030,6 +5091,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5737,6 +5819,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/proxyquire": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", diff --git a/package.json b/package.json index 9dfb8b5..5dfa0f0 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@aws-sdk/client-sns": "3.981.0", "@xmldom/xmldom": "0.8.11", "feed": "5.2.0", + "axios": "1.13.4", "ioredis": "5.9.2", "joi": "18.0.2", "moment": "2.30.1", From 6f3b45e6d404eef47531a37d4b95b88cc97a7693 Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Wed, 11 Feb 2026 13:07:19 +0000 Subject: [PATCH 2/6] Adding meteoalarm post to processMessage along with a couple of edits for the v2 message --- docker/.env | 3 + .../wiremock/mappings/third-party-api.json | 2 +- docker/scripts/register-lambda-functions.sh | 5 +- lib/functions/processMessage.js | 17 +- lib/helpers/meteoalarm.js | 110 ++++++ lib/models/message.js | 21 ++ readme.md | 3 + test/lib/functions/processMessage.js | 89 ++++- test/lib/helpers/messages.js | 10 +- test/lib/helpers/meteoalarm.js | 327 ++++++++++++++++++ test/lib/models/message.js | 108 ++++++ 11 files changed, 683 insertions(+), 12 deletions(-) create mode 100644 lib/helpers/meteoalarm.js create mode 100644 test/lib/helpers/meteoalarm.js diff --git a/docker/.env b/docker/.env index a013eee..708b7b1 100644 --- a/docker/.env +++ b/docker/.env @@ -55,6 +55,9 @@ CPX_DB_CONNECTION_STRING=postgres://${CPX_DB_USERNAME}:${CPX_DB_PASSWORD}@capxml CPX_REDIS_HOST=cap-xml-redis CPX_REDIS_PORT=6379 CPX_REDIS_TLS=false +CPX_METEOALARM_API_URL=http://mock-api:8080 # wiremock url +CPX_METEOALARM_API_USERNAME=username +CPX_METEOALARM_API_PASSWORD=password PGADMIN_DEFAULT_PASSWORD=pgadmin POSTGRES_PASSWORD=postgres LIQUIBASE_COMMAND_CHANGELOG_FILE=./changelog/db.changelog-master.xml diff --git a/docker/docker/wiremock/mappings/third-party-api.json b/docker/docker/wiremock/mappings/third-party-api.json index e1159c7..0c198b1 100644 --- a/docker/docker/wiremock/mappings/third-party-api.json +++ b/docker/docker/wiremock/mappings/third-party-api.json @@ -6,7 +6,7 @@ "urlPath": "/warnings" }, "response": { - "status": 200, + "status": 201, "body": "{\"warning\": {\"uuid\": \"{{randomValue type='UUID'}}\" } }" } }, diff --git a/docker/scripts/register-lambda-functions.sh b/docker/scripts/register-lambda-functions.sh index 7acde00..45ea2eb 100755 --- a/docker/scripts/register-lambda-functions.sh +++ b/docker/scripts/register-lambda-functions.sh @@ -16,7 +16,10 @@ cpx_agw_url=$(echo CPX_AGW_URL=$deployed_cpx_agw_url) cpx_redis_host=$(echo CPX_REDIS_HOST=$CPX_REDIS_HOST) cpx_redis_port=$(echo CPX_REDIS_PORT=$CPX_REDIS_PORT) cpx_redis_tls=$(echo CPX_REDIS_TLS=$CPX_REDIS_TLS) -set -- $cpx_db_username $cpx_db_password $cpx_db_name $cpx_db_host $cpx_agw_url $cpx_redis_host $cpx_redis_port $cpx_redis_tls +cpx_meteoalarm_api_url=$(echo CPX_METEOALARM_API_URL=$CPX_METEOALARM_API_URL) +cpx_meteoalarm_api_username=$(echo CPX_METEOALARM_API_USERNAME=$CPX_METEOALARM_API_USERNAME) +cpx_meteoalarm_api_password=$(echo CPX_METEOALARM_API_PASSWORD=$CPX_METEOALARM_API_PASSWORD) +set -- $cpx_db_username $cpx_db_password $cpx_db_name $cpx_db_host $cpx_agw_url $cpx_redis_host $cpx_redis_port $cpx_redis_tls $cpx_meteoalarm_api_url $cpx_meteoalarm_api_username $cpx_meteoalarm_api_password custom_environment_variables=$(printf '%s,' "$@" | sed 's/,*$//g') # Iterate over each file in lambda_functions_dir diff --git a/lib/functions/processMessage.js b/lib/functions/processMessage.js index ee04749..c16673a 100644 --- a/lib/functions/processMessage.js +++ b/lib/functions/processMessage.js @@ -11,10 +11,11 @@ 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 EA_WHO = '2.49.0.1.826.1' const CODE = 'MCP:v2.0' const severityV2Mapping = require('../models/v2MessageMapping') const redis = require('../helpers/redis') +const meteoalarm = require('../helpers/meteoalarm') module.exports.processMessage = async (event) => { try { @@ -58,8 +59,15 @@ module.exports.processMessage = async (event) => { } const { message: redisMessage, query: dbQuery } = message.putQuery(message, messageV2) - // store the message in database and redis/elasticache - await Promise.all([service.putMessage(dbQuery), redis.set(redisMessage.identifier, redisMessage)]) + // store the message in database, redis/elasticache, and post to Meteoalarm + await Promise.all([ + service.putMessage(dbQuery), + redis.set(redisMessage.identifier, redisMessage), + meteoalarm.postWarning(messageV2.toString(), message.identifier).catch(err => { + // Log error but don't fail the entire process if Meteoalarm post fails + console.error(`Failed to post to Meteoalarm: ${err.message}`) + }) + ]) console.log(`Finished processing CAP message: ${message.identifier} for ${message.fwisCode}`) return { @@ -167,6 +175,7 @@ const processMessageV2 = (message, lastMessage) => { messageV2.references = referencesV2 } messageV2.event = `${severityV2Mapping[message.severity]?.description}: ${messageV2.areaDesc}` + messageV2.responseType = 'Monitor' messageV2.severity = severityV2Mapping[message.severity]?.severity || '' messageV2.onset = message.sent messageV2.headline = `${severityV2Mapping[message.severity]?.headline}: ${messageV2.areaDesc}` @@ -188,5 +197,7 @@ const processMessageV2 = (message, lastMessage) => { messageV2.addParameter('use_polygon_over_geocode', 'true') messageV2.addParameter('uk_ea_ta_code', message.fwisCode) + messageV2.removeNode('geocode') + return messageV2 } diff --git a/lib/helpers/meteoalarm.js b/lib/helpers/meteoalarm.js new file mode 100644 index 0000000..849257a --- /dev/null +++ b/lib/helpers/meteoalarm.js @@ -0,0 +1,110 @@ +'use strict' + +const axios = require('axios') +const https = require('https') +let cachedToken = null +let tokenExpiry = null +const CPX_METEOALARM_API_URL = process.env.CPX_METEOALARM_API_URL +const CPX_METEOALARM_API_USERNAME = process.env.CPX_METEOALARM_API_USERNAME +const CPX_METEOALARM_API_PASSWORD = process.env.CPX_METEOALARM_API_PASSWORD +const MAX_RETRIES = 3 +const config = { + retryDelayMultiplier: 1000 // Default 1000ms, can be overridden for testing +} + +const getValidToken = async () => { + // Check if we have a cached token that hasn't expired + if (cachedToken && tokenExpiry && new Date() < tokenExpiry) { + return cachedToken + } + + try { + const response = await axios.post(`${CPX_METEOALARM_API_URL}/tokens`, { + username: CPX_METEOALARM_API_USERNAME, + password: CPX_METEOALARM_API_PASSWORD + }, { + headers: { + 'Content-Type': 'application/json' + }, + httpsAgent: new https.Agent({ + rejectUnauthorized: false + }) + }) + + if (response.status !== 200) { + throw new Error(`Failed to authenticate: ${response.status} ${response.statusText}`) + } + + cachedToken = response.data.token + // Set token expiry to 1 hour from now + tokenExpiry = new Date(Date.now() + 3600000) + return cachedToken + } catch (err) { + console.error('Error fetching bearer token:', err.message) + throw new Error(`Failed to authenticate with Meteoalarm: ${err.message}`) + } +} + +const postWarning = async (xmlMessage, identifier) => { + let lastError = null + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + const token = await getValidToken() + const response = await axios.post(`${CPX_METEOALARM_API_URL}/warnings`, xmlMessage, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/xml' + }, + timeout: 10000, + httpsAgent: new https.Agent({ + rejectUnauthorized: false + }) + }) + + if (response.status === 201) { + console.log(`Successfully posted warning to Meteoalarm: ${identifier}`) + console.log(response.data) + return response.data + } + throw new Error(`Received non-201 response: ${response.status}`) + } catch (err) { + lastError = err + console.error(`Meteoalarm post attempt ${attempt} failed: ${err.message}`) + if (err.response?.data) { + console.error(JSON.stringify(err.response.data)) + } + + // If it's a 401 error, clear the cached token and retry + if (err.response?.status === 401) { + console.log('Received 401, clearing cached token') + cachedToken = null + tokenExpiry = null + } + + // If this isn't the last attempt, wait before retrying + if (attempt < MAX_RETRIES) { + const delayMs = attempt * config.retryDelayMultiplier + console.log(`Waiting ${delayMs}ms before retry...`) + await new Promise(resolve => setTimeout(resolve, delayMs)) + } + } + } + throw new Error(`Failed to post warning to Meteoalarm after ${MAX_RETRIES} attempts: ${lastError.message}`) +} + +const clearTokenCache = () => { + cachedToken = null + tokenExpiry = null +} + +const setRetryDelayMultiplier = (multiplier) => { + config.retryDelayMultiplier = multiplier +} + +module.exports = { + postWarning, + clearTokenCache, + // Export for testing + getValidToken, + setRetryDelayMultiplier +} diff --git a/lib/models/message.js b/lib/models/message.js index f3e2b71..290c9ae 100644 --- a/lib/models/message.js +++ b/lib/models/message.js @@ -89,6 +89,19 @@ class Message { this.getFirstElement('event').textContent = value } + get responseType () { + return this.getFirstElement('responseType')?.textContent || '' + } + + set responseType (value) { + const responseTypeEl = this.getFirstElement('responseType') + if (responseTypeEl) { + responseTypeEl.textContent = value + } else { + this.addElement('event', 'responseType', value) + } + } + get severity () { return this.getFirstElement('severity')?.textContent || '' } @@ -176,6 +189,14 @@ class Message { } } + removeNode (name) { + const nodes = this.doc.getElementsByTagName(name) + for (let i = nodes.length - 1; i >= 0; i--) { + const node = nodes[i] + node.parentNode.removeChild(node) + } + } + toString () { return xmlFormat(new xmldom.XMLSerializer().serializeToString(this.doc), { indentation: ' ', collapseContent: true }) } diff --git a/readme.md b/readme.md index 1d732cf..a61afc9 100644 --- a/readme.md +++ b/readme.md @@ -20,6 +20,9 @@ This project provides CAP XML services through the use of AWS Lambda. | CPX_REDIS_HOST | Redis/Elasticache host| yes | | | | CPX_REDIS_PORT | Redis/Elasticache port| yes | | | | CPX_REDIS_TLS | Redis/Elasticache tls | yes | | | +| CPX_METEOALARM_API_URL | Meteoalarm url | yes | | | +| CPX_METEOALARM_API_USERNAME | Meteoalarm username | yes | | | +| CPX_METEOALARM_API_PASSWORD | Meteoalarm password | yes | | | ## Prerequisites diff --git a/test/lib/functions/processMessage.js b/test/lib/functions/processMessage.js index 170cf52..12c3477 100644 --- a/test/lib/functions/processMessage.js +++ b/test/lib/functions/processMessage.js @@ -11,6 +11,7 @@ const processMessage = require('../../../lib/functions/processMessage').processM const service = require('../../../lib/helpers/service') const aws = require('../../../lib/helpers/aws') const redis = require('../../../lib/helpers/redis') +const meteoalarm = require('../../../lib/helpers/meteoalarm') 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') } @@ -18,10 +19,10 @@ const ORIGINAL_ENV = process.env let clock const tomorrow = new Date(new Date().getTime() + (24 * 60 * 60 * 1000)) const identifier = '4eb3b7350ab7aa443650fc9351f02940E' -const identifierV2 = `2.49.0.0.826.1.20251106080027.${identifier}` +const identifierV2 = `2.49.0.1.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' +const referencesV2 = 'www.gov.uk/environment-agency,2.49.0.1.826.1.20251106080027.4eb3b7350ab7aa443650fc9351f02940E,2020-01-01T00:00:00+00:00' // *********************************************************** // Helper functions @@ -31,6 +32,7 @@ const expectResponse = (response, putQuery, severity = 'Minor', status = 'Test', expectMessageV1(new Message(putQuery.values[3]), severity, status, references, previousReferences, quickdialNumber) expectMessageV2(new Message(putQuery.values[10]), severity, status, references, previousReferences, quickdialNumber) expectRedisSet(identifier) + expectMeteoalarmPost(putQuery.values[10]) } const expectRedisSet = (identifier) => { @@ -43,6 +45,13 @@ const expectRedisSet = (identifier) => { Code.expect(value.alert_v2).to.not.be.empty() } +const expectMeteoalarmPost = (messageV2Xml) => { + Code.expect(meteoalarm.postWarning.calledOnce).to.be.true() + const [xmlMessage, messageIdentifier] = meteoalarm.postWarning.firstCall.args + Code.expect(xmlMessage).to.equal(messageV2Xml) + Code.expect(messageIdentifier).to.equal(identifier) +} + const expectResponseAndPutQuery = (response, putQuery, status, msgType, references, previousReferences) => { // test response Code.expect(response.statusCode).to.equal(200) @@ -165,6 +174,8 @@ lab.experiment('processMessage', () => { }) // mock redis sinon.stub(redis, 'set').resolves('OK') + // mock meteoalarm + sinon.stub(meteoalarm, 'postWarning').resolves({ id: 'meteoalarm-warning-id' }) }) lab.afterEach(() => { @@ -184,16 +195,19 @@ lab.experiment('processMessage', () => { // do alert and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() let response = await processMessage(nwsAlert) expectResponse(response, putQuery, 'Minor') // do warning and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') }) expectResponse(response, putQuery, 'Moderate') // do severe warning and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) expectResponse(response, putQuery, 'Severe') }) @@ -210,16 +224,19 @@ lab.experiment('processMessage', () => { }) // do alert and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() let response = await processMessage(nwsAlert) expectResponse(response, putQuery, 'Minor') // do warning and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') }) expectResponse(response, putQuery, 'Moderate') // do severe warning and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) expectResponse(response, putQuery, 'Severe') }) @@ -233,16 +250,19 @@ lab.experiment('processMessage', () => { // do alert and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() let response = await processMessage(nwsAlert) expectResponse(response, putQuery, 'Minor', 'Actual') // do warning and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') }) expectResponse(response, putQuery, 'Moderate', 'Actual') // do severe warning and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) expectResponse(response, putQuery, 'Severe', 'Actual') }) @@ -266,16 +286,19 @@ lab.experiment('processMessage', () => { // do alert and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() let response = await processMessage(nwsAlert) expectResponse(response, putQuery, 'Minor', 'Test', 'Update', true) // do warning and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') }) expectResponse(response, putQuery, 'Moderate', 'Test', 'Update', true) // do severe warning and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) expectResponse(response, putQuery, 'Severe', 'Test', 'Update', true) }) @@ -300,16 +323,19 @@ lab.experiment('processMessage', () => { // do alert and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() let response = await processMessage(nwsAlert) expectResponse(response, putQuery, 'Minor', 'Actual', 'Update', true) // do warning and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') }) expectResponse(response, putQuery, 'Moderate', 'Actual', 'Update', true) // do severe warning and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) expectResponse(response, putQuery, 'Severe', 'Actual', 'Update', true) }) @@ -338,16 +364,19 @@ lab.experiment('processMessage', () => { // do alert and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() let response = await processMessage(alert) expectResponse(response, putQuery, 'Minor', 'Test', 'Update', true, true, false) // do warning and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() 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 redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() response = await processMessage({ bodyXml: alert.bodyXml.replace('Minor', 'Severe') }) expectResponse(response, putQuery, 'Severe', 'Test', 'Update', true, true, false) }) @@ -374,16 +403,19 @@ lab.experiment('processMessage', () => { // do alert and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() let response = await processMessage(nwsAlert) expectResponse(response, putQuery, 'Minor', 'Actual', 'Update', true, true) // do warning and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') }) expectResponse(response, putQuery, 'Moderate', 'Actual', 'Update', true, true) // do severe warning and test output xml redis.set.resetHistory() + meteoalarm.postWarning.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) expectResponse(response, putQuery, 'Severe', 'Actual', 'Update', true, true) }) @@ -430,4 +462,57 @@ lab.experiment('processMessage', () => { }) await Code.expect(processMessage(nwsAlert)).to.reject() }) + + lab.test('Throws error when pre/post validation has errors with no SNS message', async () => { + const consoleLogStub = sinon.stub(console, 'log') + const badAlert = { bodyXml: nwsAlert.bodyXml.replace('4eb3b7350ab7aa443650fc9351f02940E', '') } + await Code.expect(processMessage(badAlert)).to.reject() + Code.expect(consoleLogStub.calledWith(badAlert.bodyXml)).to.be.true() + consoleLogStub.restore() + }) + + lab.test('Throws error when pre/post validation has errors with SNS message sent', async () => { + sinon.stub(aws.email, 'publishMessage').resolves() + process.env.CPX_SNS_TOPIC = 'arn:aws:sns:region:account:topic' + const consoleLogStub = sinon.stub(console, 'log') + const badAlert = { bodyXml: nwsAlert.bodyXml.replace('4eb3b7350ab7aa443650fc9351f02940E', '') } + const err = await Code.expect(processMessage(badAlert)).to.reject() + Code.expect(err.message).to.contain('[500]') + Code.expect(aws.email.publishMessage.calledOnce).to.be.true() + Code.expect(consoleLogStub.calledWith(badAlert.bodyXml)).to.be.true() + consoleLogStub.restore() + }) + + lab.test('does not log when validator has no errors', async () => { + const consoleLogStub = sinon.stub(console, 'log') + service.putMessage = (query) => Promise.resolve() + const response = await processMessage(nwsAlert) + Code.expect(response.statusCode).to.equal(200) + // Check that the error logging for validation didn't occur + // (processMessage itself logs processing messages, so we check it doesn't log the bodyXml) + Code.expect(consoleLogStub.calledWith(nwsAlert.bodyXml)).to.be.false() + consoleLogStub.restore() + }) + + lab.test('Meteoalarm failure does not fail message processing', async () => { + const consoleErrorStub = sinon.stub(console, 'error') + meteoalarm.postWarning.rejects(new Error('Meteoalarm API unavailable')) + + const putMessageStub = sinon.stub(service, 'putMessage').resolves() + + const response = await processMessage(nwsAlert) + + // Should still succeed + Code.expect(response.statusCode).to.equal(200) + Code.expect(response.body.identifier).to.equal(identifier) + + // Should have logged the error + Code.expect(consoleErrorStub.calledWith('Failed to post to Meteoalarm: Meteoalarm API unavailable')).to.be.true() + + // Should have still called the other services + Code.expect(putMessageStub.calledOnce).to.be.true() + Code.expect(redis.set.calledOnce).to.be.true() + + consoleErrorStub.restore() + }) }) diff --git a/test/lib/helpers/messages.js b/test/lib/helpers/messages.js index 15b0761..fbac803 100644 --- a/test/lib/helpers/messages.js +++ b/test/lib/helpers/messages.js @@ -123,7 +123,7 @@ lab.experiment('messages helper', () => { alert: 'test', sent: new Date(), identifier: '4eb3b7350ab7aa443650fc9351f', - identifier_v2: '2.49.0.0.826.1.YYYYMMDDHHMMSS.4eb3b7350ab7aa443650fc9351f' + identifier_v2: '2.49.0.1.826.1.YYYYMMDDHHMMSS.4eb3b7350ab7aa443650fc9351f' }] }) }) @@ -189,14 +189,14 @@ lab.experiment('messages helper', () => { alert: 'test1', sent: new Date('2025-01-01'), identifier: 'id1', - identifier_v2: '2.49.0.0.826.1.20250101000000.id1' + identifier_v2: '2.49.0.1.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' + identifier_v2: '2.49.0.1.826.1.20250102000000.id2' } ] }) @@ -218,7 +218,7 @@ lab.experiment('messages helper', () => { alert: 'test', sent: new Date('2025-01-01T12:00:00Z'), identifier: 'test_id', - identifier_v2: '2.49.0.0.826.1.20250101120000.test_id' + identifier_v2: '2.49.0.1.826.1.20250101120000.test_id' }] }) } @@ -263,7 +263,7 @@ lab.experiment('messages helper', () => { 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}` + identifier_v2: `2.49.0.1.826.1.2025010${i + 1}000000.id${i}` })) }) } diff --git a/test/lib/helpers/meteoalarm.js b/test/lib/helpers/meteoalarm.js new file mode 100644 index 0000000..925aa3e --- /dev/null +++ b/test/lib/helpers/meteoalarm.js @@ -0,0 +1,327 @@ +'use strict' + +const Lab = require('@hapi/lab') +const Code = require('@hapi/code') +const sinon = require('sinon') +const Proxyquire = require('proxyquire') + +const lab = exports.lab = Lab.script() + +const ORIGINAL_ENV = process.env + +lab.experiment('meteoalarm helper', () => { + let meteoalarm + let axiosStub + + lab.beforeEach(() => { + // Mock environment - must be set before loading module + process.env = { ...ORIGINAL_ENV } + process.env.CPX_METEOALARM_API_URL = 'https://test-meteoalarm.example.com' + process.env.CPX_METEOALARM_API_USERNAME = 'test-user' + process.env.CPX_METEOALARM_API_PASSWORD = 'test-password' + + // Create axios stub + axiosStub = { + post: sinon.stub() + } + + // Clear the require cache to force fresh module load + delete require.cache[require.resolve('../../../lib/helpers/meteoalarm')] + + // Load module with mocked axios + meteoalarm = Proxyquire('../../../lib/helpers/meteoalarm', { + axios: axiosStub + }) + + // Clear any cached token before each test + meteoalarm.clearTokenCache() + }) + + lab.afterEach(() => { + sinon.restore() + process.env = ORIGINAL_ENV + }) + + lab.experiment('getValidToken', () => { + lab.test('fetches a new token successfully', async () => { + axiosStub.post.resolves({ + status: 200, + data: { token: 'test-bearer-token-123' } + }) + + const token = await meteoalarm.getValidToken() + + Code.expect(token).to.equal('test-bearer-token-123') + Code.expect(axiosStub.post.calledOnce).to.be.true() + Code.expect(axiosStub.post.firstCall.args[0]).to.equal('https://test-meteoalarm.example.com/tokens') + Code.expect(axiosStub.post.firstCall.args[1]).to.equal({ + username: 'test-user', + password: 'test-password' + }) + Code.expect(axiosStub.post.firstCall.args[2].headers['Content-Type']).to.equal('application/json') + }) + + lab.test('returns cached token if still valid', async () => { + // First call to get token + axiosStub.post.resolves({ + status: 200, + data: { token: 'cached-token' } + }) + + const token1 = await meteoalarm.getValidToken() + Code.expect(token1).to.equal('cached-token') + Code.expect(axiosStub.post.calledOnce).to.be.true() + + // Second call should return cached token without making another API call + const token2 = await meteoalarm.getValidToken() + Code.expect(token2).to.equal('cached-token') + Code.expect(axiosStub.post.calledOnce).to.be.true() // Still only called once + }) + + lab.test('throws error when authentication fails with non-200 status', async () => { + axiosStub.post.resolves({ + status: 401, + statusText: 'Unauthorized' + }) + + await Code.expect(meteoalarm.getValidToken()).to.reject(Error, 'Failed to authenticate with Meteoalarm: Failed to authenticate: 401 Unauthorized') + }) + + lab.test('throws error when axios request fails', async () => { + axiosStub.post.rejects(new Error('Network error')) + + await Code.expect(meteoalarm.getValidToken()).to.reject(Error, 'Failed to authenticate with Meteoalarm: Network error') + }) + }) + + lab.experiment('postWarning', () => { + lab.beforeEach(() => { + // Set a very short retry delay for testing (10ms instead of 1000ms) + meteoalarm.setRetryDelayMultiplier(10) + }) + + lab.afterEach(() => { + // Reset to default + meteoalarm.setRetryDelayMultiplier(1000) + }) + + lab.test('successfully posts warning on first attempt', async () => { + const xmlMessage = 'test-id' + const identifier = 'test-id' + + // Mock token request + axiosStub.post.onFirstCall().resolves({ + status: 200, + data: { token: 'test-token' } + }) + + // Mock warning post + axiosStub.post.onSecondCall().resolves({ + status: 201, + data: { id: 'warning-123', status: 'created' } + }) + + const result = await meteoalarm.postWarning(xmlMessage, identifier) + + Code.expect(result).to.equal({ id: 'warning-123', status: 'created' }) + Code.expect(axiosStub.post.calledTwice).to.be.true() + + // Verify warning post call + const warningCall = axiosStub.post.secondCall + Code.expect(warningCall.args[0]).to.equal('https://test-meteoalarm.example.com/warnings') + Code.expect(warningCall.args[1]).to.equal(xmlMessage) + Code.expect(warningCall.args[2].headers.Authorization).to.equal('Bearer test-token') + Code.expect(warningCall.args[2].headers['Content-Type']).to.equal('application/xml') + Code.expect(warningCall.args[2].timeout).to.equal(10000) + }) + + lab.test('retries on failure and succeeds on second attempt', async () => { + const xmlMessage = 'test-id' + const identifier = 'test-id' + + // Mock token request using withArgs for better matching + axiosStub.post.withArgs('https://test-meteoalarm.example.com/tokens').resolves({ + status: 200, + data: { token: 'test-token' } + }) + + // Track warning post attempts + let attemptCount = 0 + axiosStub.post.withArgs('https://test-meteoalarm.example.com/warnings').callsFake(() => { + attemptCount++ + if (attemptCount === 1) { + const error = new Error('Timeout') + error.response = { data: { error: 'timeout' } } + return Promise.reject(error) + } else { + return Promise.resolve({ + status: 201, + data: { id: 'warning-123' } + }) + } + }) + + const result = await meteoalarm.postWarning(xmlMessage, identifier) + + Code.expect(result).to.equal({ id: 'warning-123' }) + Code.expect(attemptCount).to.equal(2) + }) + + lab.test('clears cached token on 401 and retries', async () => { + const xmlMessage = 'test-id' + const identifier = 'test-id' + + // Track token requests + let tokenRequestCount = 0 + axiosStub.post.withArgs('https://test-meteoalarm.example.com/tokens').callsFake(() => { + tokenRequestCount++ + return Promise.resolve({ + status: 200, + data: { token: tokenRequestCount === 1 ? 'expired-token' : 'fresh-token' } + }) + }) + + // Track warning post attempts + let warningAttemptCount = 0 + axiosStub.post.withArgs('https://test-meteoalarm.example.com/warnings').callsFake(() => { + warningAttemptCount++ + if (warningAttemptCount === 1) { + const error = new Error('Unauthorized') + error.response = { status: 401, data: { error: 'token expired' } } + return Promise.reject(error) + } else { + return Promise.resolve({ + status: 201, + data: { id: 'warning-456' } + }) + } + }) + + const result = await meteoalarm.postWarning(xmlMessage, identifier) + + Code.expect(result).to.equal({ id: 'warning-456' }) + // Should have fetched token twice because of 401 + Code.expect(tokenRequestCount).to.equal(2) + }) + + lab.test('throws error after max retries exceeded', async () => { + const xmlMessage = 'test-id' + const identifier = 'test-id' + + // Mock token request + axiosStub.post.withArgs('https://test-meteoalarm.example.com/tokens').resolves({ + status: 200, + data: { token: 'test-token' } + }) + + // All warning posts fail + const error = new Error('Service unavailable') + error.response = { data: { error: 'service down' } } + axiosStub.post.withArgs('https://test-meteoalarm.example.com/warnings').rejects(error) + + await Code.expect(meteoalarm.postWarning(xmlMessage, identifier)).to.reject(Error, 'Failed to post warning to Meteoalarm after 3 attempts: Service unavailable') + }) + + lab.test('throws error when non-201 status is received', async () => { + const xmlMessage = 'test-id' + const identifier = 'test-id' + + // Mock token request + axiosStub.post.withArgs('https://test-meteoalarm.example.com/tokens').resolves({ + status: 200, + data: { token: 'test-token' } + }) + + // Warning post returns non-201 status and will retry + axiosStub.post.withArgs('https://test-meteoalarm.example.com/warnings').resolves({ + status: 200, + data: { message: 'accepted but not created' } + }) + + await Code.expect(meteoalarm.postWarning(xmlMessage, identifier)).to.reject(Error, 'Failed to post warning to Meteoalarm after 3 attempts: Received non-201 response: 200') + }) + + lab.test('handles error without response object', async () => { + const xmlMessage = 'test-id' + const identifier = 'test-id' + + // Mock token request + axiosStub.post.withArgs('https://test-meteoalarm.example.com/tokens').resolves({ + status: 200, + data: { token: 'test-token' } + }) + + // All warning posts fail without response object + axiosStub.post.withArgs('https://test-meteoalarm.example.com/warnings').rejects(new Error('Network error')) + + await Code.expect(meteoalarm.postWarning(xmlMessage, identifier)).to.reject(Error, 'Failed to post warning to Meteoalarm after 3 attempts: Network error') + }) + }) + + lab.experiment('clearTokenCache', () => { + lab.test('clears cached token requiring new fetch', async () => { + // First call to get token + axiosStub.post.resolves({ + status: 200, + data: { token: 'first-token' } + }) + + const token1 = await meteoalarm.getValidToken() + Code.expect(token1).to.equal('first-token') + Code.expect(axiosStub.post.calledOnce).to.be.true() + + // Clear the cache + meteoalarm.clearTokenCache() + + // Mock a different token for next call + axiosStub.post.resolves({ + status: 200, + data: { token: 'second-token' } + }) + + // Next call should fetch a new token + const token2 = await meteoalarm.getValidToken() + Code.expect(token2).to.equal('second-token') + Code.expect(axiosStub.post.calledTwice).to.be.true() + }) + }) + + lab.experiment('integration scenarios', () => { + lab.test('uses cached token across multiple warning posts', async () => { + const xmlMessage1 = 'test-id-1' + const xmlMessage2 = 'test-id-2' + + // Mock token request once - should only be called once + let tokenCallCount = 0 + axiosStub.post.callsFake((url, data, config) => { + if (url.includes('/tokens')) { + tokenCallCount++ + return Promise.resolve({ + status: 200, + data: { token: 'shared-token' } + }) + } else if (url.includes('/warnings')) { + // Return different results for different messages + if (data === xmlMessage1) { + return Promise.resolve({ + status: 201, + data: { id: 'warning-1' } + }) + } else if (data === xmlMessage2) { + return Promise.resolve({ + status: 201, + data: { id: 'warning-2' } + }) + } + } + return Promise.reject(new Error('Unexpected call')) + }) + + await meteoalarm.postWarning(xmlMessage1, 'test-id-1') + await meteoalarm.postWarning(xmlMessage2, 'test-id-2') + + // Token should only be fetched once + Code.expect(tokenCallCount).to.equal(1) + }) + }) +}) diff --git a/test/lib/models/message.js b/test/lib/models/message.js index e881793..8307019 100644 --- a/test/lib/models/message.js +++ b/test/lib/models/message.js @@ -181,6 +181,30 @@ lab.experiment('Message class', () => { Code.expect(message.toString()).to.include('REF2') }) + lab.test('responseType defaults to empty string when missing', () => { + Code.expect(message.responseType).to.equal('') + }) + + lab.test('setting responseType adds element when not present', () => { + message.responseType = 'Prepare' + Code.expect(message.responseType).to.equal('Prepare') + Code.expect(message.toString()).to.include('Prepare') + }) + + lab.test('setting responseType updates existing element', () => { + // First set to add the element + message.responseType = 'Monitor' + Code.expect(message.responseType).to.equal('Monitor') + + // Second set should update existing element + message.responseType = 'Evacuate' + Code.expect(message.responseType).to.equal('Evacuate') + Code.expect(message.toString()).to.include('Evacuate') + // Should only have one responseType element + const responseTypeCount = (message.toString().match(//g) || []).length + Code.expect(responseTypeCount).to.equal(1) + }) + lab.test('parses quickdial number from instruction', () => { Code.expect(message.quickdialNumber).to.equal('210010') }) @@ -239,6 +263,7 @@ lab.experiment('Message class', () => { Code.expect(messageBlank.sent).to.equal('') Code.expect(messageBlank.code).to.equal('') Code.expect(messageBlank.event).to.equal('') + Code.expect(messageBlank.responseType).to.equal('') Code.expect(messageBlank.severity).to.equal('') Code.expect(messageBlank.onset).to.equal('') Code.expect(messageBlank.headline).to.equal('') @@ -253,6 +278,7 @@ lab.experiment('Message class', () => { messageBlank.status = 'Actual' messageBlank.code = 'CODE123' messageBlank.event = 'Test Event' + messageBlank.responseType = 'Shelter' messageBlank.severity = 'Severe' messageBlank.onset = '2026-06-01T10:00:00-00:00' messageBlank.headline = 'Test Headline' @@ -264,6 +290,7 @@ lab.experiment('Message class', () => { 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.responseType).to.equal('Shelter') 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') @@ -277,6 +304,7 @@ lab.experiment('Message class', () => { messageBlank.status = 'Actual' messageBlank.code = 'CODE123' messageBlank.event = 'Test Event' + messageBlank.responseType = 'AllClear' messageBlank.severity = 'Severe' messageBlank.onset = '2026-06-01T10:00:00-00:00' messageBlank.headline = 'Test Headline' @@ -288,6 +316,7 @@ lab.experiment('Message class', () => { 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.responseType).to.equal('AllClear') 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') @@ -324,4 +353,83 @@ lab.experiment('Message class', () => { fwisCode `)) }) + + lab.test('removeNode removes a single node from the document', () => { + // Verify instruction exists before removal + Code.expect(message.instruction).to.not.be.empty() + Code.expect(message.toString()).to.include('') + + // Remove instruction node + message.removeNode('instruction') + + // Verify instruction is removed + Code.expect(message.instruction).to.equal('') + Code.expect(message.toString()).to.not.include('') + }) + + lab.test('removeNode removes multiple nodes of the same type', () => { + // Add multiple parameters + message.addParameter('param1', 'value1') + message.addParameter('param2', 'value2') + message.addParameter('param3', 'value3') + + // Verify parameters exist + const xmlBefore = message.toString() + Code.expect(xmlBefore).to.include('') + Code.expect(xmlBefore).to.include('param1') + Code.expect(xmlBefore).to.include('param2') + Code.expect(xmlBefore).to.include('param3') + + // Remove all parameter nodes + message.removeNode('parameter') + + // Verify all parameters are removed + const xmlAfter = message.toString() + Code.expect(xmlAfter).to.not.include('') + Code.expect(xmlAfter).to.not.include('param1') + Code.expect(xmlAfter).to.not.include('param2') + Code.expect(xmlAfter).to.not.include('param3') + }) + + lab.test('removeNode handles non-existent nodes gracefully', () => { + const xmlBefore = message.toString() + + // Try to remove a node that doesn't exist + message.removeNode('nonExistentNode') + + // XML should remain unchanged + const xmlAfter = message.toString() + Code.expect(xmlAfter).to.equal(xmlBefore) + }) + + lab.test('removeNode removes code node when present', () => { + const messageWithCode = new Message(blankXml2) + messageWithCode.code = 'TEST_CODE' + + // Verify code exists + Code.expect(messageWithCode.code).to.equal('TEST_CODE') + Code.expect(messageWithCode.toString()).to.include('TEST_CODE') + + // Remove code node + messageWithCode.removeNode('code') + + // Verify code is removed + Code.expect(messageWithCode.code).to.equal('') + Code.expect(messageWithCode.toString()).to.not.include('TEST_CODE') + }) + + lab.test('removeNode removes references node', () => { + message.references = 'REF123' + + // Verify references exists + Code.expect(message.references).to.equal('REF123') + Code.expect(message.toString()).to.include('REF123') + + // Remove references node + message.removeNode('references') + + // Verify references is removed + Code.expect(message.references).to.equal('') + Code.expect(message.toString()).to.not.include('') + }) }) From 3774e057343ae178b5136cee9da45414ef1d25a3 Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Wed, 11 Feb 2026 13:25:53 +0000 Subject: [PATCH 3/6] sorting magic numbers for sonar --- lib/helpers/meteoalarm.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/helpers/meteoalarm.js b/lib/helpers/meteoalarm.js index 849257a..f5b12d8 100644 --- a/lib/helpers/meteoalarm.js +++ b/lib/helpers/meteoalarm.js @@ -1,15 +1,21 @@ 'use strict' const axios = require('axios') -const https = require('https') +const https = require('node:https') let cachedToken = null let tokenExpiry = null const CPX_METEOALARM_API_URL = process.env.CPX_METEOALARM_API_URL const CPX_METEOALARM_API_USERNAME = process.env.CPX_METEOALARM_API_USERNAME const CPX_METEOALARM_API_PASSWORD = process.env.CPX_METEOALARM_API_PASSWORD const MAX_RETRIES = 3 +const TOKEN_EXPIRY_MS = 3600000 // 1 hour in milliseconds +const API_REQUEST_TIMEOUT_MS = 10000 // 10 seconds +const DEFAULT_RETRY_DELAY_MULTIPLIER = 1000 // 1 second base delay +const HTTP_STATUS_OK = 200 +const HTTP_STATUS_CREATED = 201 +const HTTP_STATUS_UNAUTHORIZED = 401 const config = { - retryDelayMultiplier: 1000 // Default 1000ms, can be overridden for testing + retryDelayMultiplier: DEFAULT_RETRY_DELAY_MULTIPLIER // Can be overridden for testing } const getValidToken = async () => { @@ -31,13 +37,13 @@ const getValidToken = async () => { }) }) - if (response.status !== 200) { + if (response.status !== HTTP_STATUS_OK) { throw new Error(`Failed to authenticate: ${response.status} ${response.statusText}`) } cachedToken = response.data.token // Set token expiry to 1 hour from now - tokenExpiry = new Date(Date.now() + 3600000) + tokenExpiry = new Date(Date.now() + TOKEN_EXPIRY_MS) return cachedToken } catch (err) { console.error('Error fetching bearer token:', err.message) @@ -55,13 +61,13 @@ const postWarning = async (xmlMessage, identifier) => { Authorization: `Bearer ${token}`, 'Content-Type': 'application/xml' }, - timeout: 10000, + timeout: API_REQUEST_TIMEOUT_MS, httpsAgent: new https.Agent({ rejectUnauthorized: false }) }) - if (response.status === 201) { + if (response.status === HTTP_STATUS_CREATED) { console.log(`Successfully posted warning to Meteoalarm: ${identifier}`) console.log(response.data) return response.data @@ -75,7 +81,7 @@ const postWarning = async (xmlMessage, identifier) => { } // If it's a 401 error, clear the cached token and retry - if (err.response?.status === 401) { + if (err.response?.status === HTTP_STATUS_UNAUTHORIZED) { console.log('Received 401, clearing cached token') cachedToken = null tokenExpiry = null From 413552a7f46abf2250c36db5554d5d0774df0fd9 Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Wed, 11 Feb 2026 13:31:51 +0000 Subject: [PATCH 4/6] addressing sonar issue for node.remove() --- lib/models/message.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/models/message.js b/lib/models/message.js index 290c9ae..e48c903 100644 --- a/lib/models/message.js +++ b/lib/models/message.js @@ -191,9 +191,10 @@ class Message { removeNode (name) { const nodes = this.doc.getElementsByTagName(name) + // Using parentNode.removeChild() because @xmldom/xmldom doesn't support node.remove() for (let i = nodes.length - 1; i >= 0; i--) { const node = nodes[i] - node.parentNode.removeChild(node) + node.parentNode.removeChild(node) // NOSONAR - remove() not available in xmldom } } From 87aaa86c15f940446b77631d81cda33d0ac53609 Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Thu, 12 Feb 2026 08:03:19 +0000 Subject: [PATCH 5/6] package updates --- package-lock.json | 376 ++++++++++++++++++++-------------------------- package.json | 4 +- 2 files changed, 166 insertions(+), 214 deletions(-) diff --git a/package-lock.json b/package-lock.json index e6c2497..54d730e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,9 @@ "version": "3.0.0", "license": "OGL", "dependencies": { - "@aws-sdk/client-sns": "3.981.0", + "@aws-sdk/client-sns": "3.988.0", "@xmldom/xmldom": "0.8.11", - "axios": "1.13.4", + "axios": "1.13.5", "feed": "5.2.0", "ioredis": "5.9.2", "joi": "18.0.2", @@ -174,45 +174,45 @@ } }, "node_modules/@aws-sdk/client-sns": { - "version": "3.981.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sns/-/client-sns-3.981.0.tgz", - "integrity": "sha512-vz4KUqG7yrjDwFok9B4nQylyNvLb1D+OVywmDzMzLG3t1nIPiSunMMHOK8NltwhUxU+Tp/exPgPUmgt/0arcQA==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sns/-/client-sns-3.988.0.tgz", + "integrity": "sha512-cUTrbWsnAevr93QJ+ayHeTI+u0T5Jc8Bx+NaRmBs1C+IsVC9t6krH+hs4jzNrvI6Ti5M3tNe7dSMw3pk/wcxkg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.5", - "@aws-sdk/credential-provider-node": "^3.972.4", + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/credential-provider-node": "^3.972.7", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/middleware-user-agent": "^3.972.8", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.981.0", + "@aws-sdk/util-endpoints": "3.988.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.6", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", + "@smithy/core": "^3.23.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", + "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@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.28", - "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-defaults-mode-browser": "^4.3.30", + "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -224,44 +224,44 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.980.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.980.0.tgz", - "integrity": "sha512-AhNXQaJ46C1I+lQ+6Kj+L24il5K9lqqIanJd8lMszPmP7bLnmX0wTKK0dxywcvrLdij3zhWttjAKEBNgLtS8/A==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.988.0.tgz", + "integrity": "sha512-ThqQ7aF1k0Zz4yJRwegHw+T1rM3a7ZPvvEUSEdvn5Z8zTeWgJAbtqW/6ejPsMLmFOlHgNcwDQN/e69OvtEOoIQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.5", + "@aws-sdk/core": "^3.973.8", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/middleware-user-agent": "^3.972.8", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.980.0", + "@aws-sdk/util-endpoints": "3.988.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.6", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", + "@smithy/core": "^3.23.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", + "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@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.28", - "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-defaults-mode-browser": "^4.3.30", + "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -272,36 +272,20 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { - "version": "3.980.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz", - "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/core": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.5.tgz", - "integrity": "sha512-IMM7xGfLGW6lMvubsA4j6BHU5FPgGAxoQ/NA63KqNLMwTS+PeMBcx8DPHL12Vg6yqOZnqok9Mu4H2BdQyq7gSA==", + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.8.tgz", + "integrity": "sha512-WeYJ2sfvRLbbUIrjGMUXcEHGu5SJk53jz3K9F8vFP42zWyROzPJ2NB6lMu9vWl5hnMwzwabX7pJc9Euh3JyMGw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", - "@aws-sdk/xml-builder": "^3.972.2", - "@smithy/core": "^3.22.0", + "@aws-sdk/xml-builder": "^3.972.4", + "@smithy/core": "^3.23.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", @@ -313,12 +297,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.3.tgz", - "integrity": "sha512-OBYNY4xQPq7Rx+oOhtyuyO0AQvdJSpXRg7JuPNBJH4a1XXIzJQl4UHQTPKZKwfJXmYLpv4+OkcFen4LYmDPd3g==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.6.tgz", + "integrity": "sha512-+dYEBWgTqkQQHFUllvBL8SLyXyLKWdxLMD1LmKJRvmb0NMJuaJFG/qg78C+LE67eeGbipYcE+gJ48VlLBGHlMw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.5", + "@aws-sdk/core": "^3.973.8", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", @@ -329,20 +313,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.5.tgz", - "integrity": "sha512-GpvBgEmSZPvlDekd26Zi+XsI27Qz7y0utUx0g2fSTSiDzhnd1FSa1owuodxR0BcUKNL7U2cOVhhDxgZ4iSoPVg==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.8.tgz", + "integrity": "sha512-z3QkozMV8kOFisN2pgRag/f0zPDrw96mY+ejAM0xssV/+YQ2kklbylRNI/TcTQUDnGg0yPxNjyV6F2EM2zPTwg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.5", + "@aws-sdk/core": "^3.973.8", "@aws-sdk/types": "^3.973.1", "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.8", + "@smithy/node-http-handler": "^4.4.10", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.10", + "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" }, "engines": { @@ -350,19 +334,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.3.tgz", - "integrity": "sha512-rMQAIxstP7cLgYfsRGrGOlpyMl0l8JL2mcke3dsIPLWke05zKOFyR7yoJzWCsI/QiIxjRbxpvPiAeKEA6CoYkg==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.6.tgz", + "integrity": "sha512-6tkIYFv3sZH1XsjQq+veOmx8XWRnyqTZ5zx/sMtdu/xFRIzrJM1Y2wAXeCJL1rhYSB7uJSZ1PgALI2WVTj78ow==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.5", - "@aws-sdk/credential-provider-env": "^3.972.3", - "@aws-sdk/credential-provider-http": "^3.972.5", - "@aws-sdk/credential-provider-login": "^3.972.3", - "@aws-sdk/credential-provider-process": "^3.972.3", - "@aws-sdk/credential-provider-sso": "^3.972.3", - "@aws-sdk/credential-provider-web-identity": "^3.972.3", - "@aws-sdk/nested-clients": "3.980.0", + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/credential-provider-env": "^3.972.6", + "@aws-sdk/credential-provider-http": "^3.972.8", + "@aws-sdk/credential-provider-login": "^3.972.6", + "@aws-sdk/credential-provider-process": "^3.972.6", + "@aws-sdk/credential-provider-sso": "^3.972.6", + "@aws-sdk/credential-provider-web-identity": "^3.972.6", + "@aws-sdk/nested-clients": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", @@ -375,13 +359,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.3.tgz", - "integrity": "sha512-Gc3O91iVvA47kp2CLIXOwuo5ffo1cIpmmyIewcYjAcvurdFHQ8YdcBe1KHidnbbBO4/ZtywGBACsAX5vr3UdoA==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.6.tgz", + "integrity": "sha512-LXsoBoaTSGHdRCQXlWSA0CHHh05KWncb592h9ElklnPus++8kYn1Ic6acBR4LKFQ0RjjMVgwe5ypUpmTSUOjPA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.5", - "@aws-sdk/nested-clients": "3.980.0", + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/nested-clients": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", @@ -394,17 +378,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.4.tgz", - "integrity": "sha512-UwerdzosMSY7V5oIZm3NsMDZPv2aSVzSkZxYxIOWHBeKTZlUqW7XpHtJMZ4PZpJ+HMRhgP+MDGQx4THndgqJfQ==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.7.tgz", + "integrity": "sha512-PuJ1IkISG7ZDpBFYpGotaay6dYtmriBYuHJ/Oko4VHxh8YN5vfoWnMNYFEWuzOfyLmP7o9kDVW0BlYIpb3skvw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.3", - "@aws-sdk/credential-provider-http": "^3.972.5", - "@aws-sdk/credential-provider-ini": "^3.972.3", - "@aws-sdk/credential-provider-process": "^3.972.3", - "@aws-sdk/credential-provider-sso": "^3.972.3", - "@aws-sdk/credential-provider-web-identity": "^3.972.3", + "@aws-sdk/credential-provider-env": "^3.972.6", + "@aws-sdk/credential-provider-http": "^3.972.8", + "@aws-sdk/credential-provider-ini": "^3.972.6", + "@aws-sdk/credential-provider-process": "^3.972.6", + "@aws-sdk/credential-provider-sso": "^3.972.6", + "@aws-sdk/credential-provider-web-identity": "^3.972.6", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", @@ -417,12 +401,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.3.tgz", - "integrity": "sha512-xkSY7zjRqeVc6TXK2xr3z1bTLm0wD8cj3lAkproRGaO4Ku7dPlKy843YKnHrUOUzOnMezdZ4xtmFc0eKIDTo2w==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.6.tgz", + "integrity": "sha512-Yf34cjIZJHVnD92jnVYy3tNjM+Q4WJtffLK2Ehn0nKpZfqd1m7SI0ra22Lym4C53ED76oZENVSS2wimoXJtChQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.5", + "@aws-sdk/core": "^3.973.8", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -434,14 +418,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.3.tgz", - "integrity": "sha512-8Ww3F5Ngk8dZ6JPL/V5LhCU1BwMfQd3tLdoEuzaewX8FdnT633tPr+KTHySz9FK7fFPcz5qG3R5edVEhWQD4AA==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.6.tgz", + "integrity": "sha512-2+5UVwUYdD4BBOkLpKJ11MQ8wQeyJGDVMDRH5eWOULAh9d6HJq07R69M/mNNMC9NTjr3mB1T0KGDn4qyQh5jzg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.980.0", - "@aws-sdk/core": "^3.973.5", - "@aws-sdk/token-providers": "3.980.0", + "@aws-sdk/client-sso": "3.988.0", + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/token-providers": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -453,13 +437,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.3.tgz", - "integrity": "sha512-62VufdcH5rRfiRKZRcf1wVbbt/1jAntMj1+J0qAd+r5pQRg2t0/P9/Rz16B1o5/0Se9lVL506LRjrhIJAhYBfA==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.6.tgz", + "integrity": "sha512-pdJzwKtlDxBnvZ04pWMqttijmkUIlwOsS0GcxCjzEVyUMpARysl0S0ks74+gs2Pdev3Ujz+BTAjOc1tQgAxGqA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.5", - "@aws-sdk/nested-clients": "3.980.0", + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/nested-clients": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -516,15 +500,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.5.tgz", - "integrity": "sha512-TVZQ6PWPwQbahUI8V+Er+gS41ctIawcI/uMNmQtQ7RMcg3JYn6gyKAFKUb3HFYx2OjYlx1u11sETSwwEUxVHTg==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.8.tgz", + "integrity": "sha512-3PGL+Kvh1PhB0EeJeqNqOWQgipdqFheO4OUKc6aYiFwEpM5t9AyE5hjjxZ5X6iSj8JiduWFZLPwASzF6wQRgFg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.5", + "@aws-sdk/core": "^3.973.8", "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.980.0", - "@smithy/core": "^3.22.0", + "@aws-sdk/util-endpoints": "3.988.0", + "@smithy/core": "^3.23.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" @@ -533,61 +517,45 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": { - "version": "3.980.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz", - "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.980.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.980.0.tgz", - "integrity": "sha512-/dONY5xc5/CCKzOqHZCTidtAR4lJXWkGefXvTRKdSKMGaYbbKsxDckisd6GfnvPSLxWtvQzwgRGRutMRoYUApQ==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.988.0.tgz", + "integrity": "sha512-OgYV9k1oBCQ6dOM+wWAMNNehXA8L4iwr7ydFV+JDHyuuu0Ko7tDXnLEtEmeQGYRcAFU3MGasmlBkMB8vf4POrg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.5", + "@aws-sdk/core": "^3.973.8", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/middleware-user-agent": "^3.972.8", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.980.0", + "@aws-sdk/util-endpoints": "3.988.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.6", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", + "@smithy/core": "^3.23.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", + "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@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.28", - "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-defaults-mode-browser": "^4.3.30", + "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -598,22 +566,6 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": { - "version": "3.980.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz", - "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/region-config-resolver": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz", @@ -631,13 +583,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.980.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.980.0.tgz", - "integrity": "sha512-1nFileg1wAgDmieRoj9dOawgr2hhlh7xdvcH57b1NnqfPaVlcqVJyPc6k3TLDUFPY69eEwNxdGue/0wIz58vjA==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.988.0.tgz", + "integrity": "sha512-xvXVlRVKHnF2h6fgWBm64aPP5J+58aJyGfRrQa/uFh8a9mcK68mLfJOYq+ZSxQy/UN3McafJ2ILAy7IWzT9kRw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.5", - "@aws-sdk/nested-clients": "3.980.0", + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/nested-clients": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -662,9 +614,9 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.981.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.981.0.tgz", - "integrity": "sha512-a8nXh/H3/4j+sxhZk+N3acSDlgwTVSZbX9i55dx41gI1H+geuonuRG+Shv3GZsCb46vzc08RK2qC78ypO8uRlg==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", + "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -702,12 +654,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.3.tgz", - "integrity": "sha512-gqG+02/lXQtO0j3US6EVnxtwwoXQC5l2qkhLCrqUrqdtcQxV7FDMbm9wLjKqoronSHyELGTjbFKK/xV5q1bZNA==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.6.tgz", + "integrity": "sha512-966xH8TPqkqOXP7EwnEThcKKz0SNP9kVJBKd9M8bNXE4GSqVouMKKnFBwYnzbWVKuLXubzX5seokcX4a0JLJIA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/middleware-user-agent": "^3.972.8", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", @@ -726,9 +678,9 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.3.tgz", - "integrity": "sha512-bCk63RsBNCWW4tt5atv5Sbrh+3J3e8YzgyF6aZb1JeXcdzG4k5SlPLeTMFOIXFuuFHIwgphUhn4i3uS/q49eww==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz", + "integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -1598,9 +1550,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.22.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.1.tgz", - "integrity": "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==", + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.0.tgz", + "integrity": "sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg==", "license": "Apache-2.0", "dependencies": { "@smithy/middleware-serde": "^4.2.9", @@ -1609,7 +1561,7 @@ "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.11", + "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" @@ -1705,12 +1657,12 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.13.tgz", - "integrity": "sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==", + "version": "4.4.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.14.tgz", + "integrity": "sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.22.1", + "@smithy/core": "^3.23.0", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -1724,15 +1676,15 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.30", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.30.tgz", - "integrity": "sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg==", + "version": "4.4.31", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.31.tgz", + "integrity": "sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -1786,9 +1738,9 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz", - "integrity": "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==", + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.10.tgz", + "integrity": "sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.2.8", @@ -1899,17 +1851,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.11.2", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.2.tgz", - "integrity": "sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==", + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.3.tgz", + "integrity": "sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.22.1", - "@smithy/middleware-endpoint": "^4.4.13", + "@smithy/core": "^3.23.0", + "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.11", + "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" }, "engines": { @@ -2006,13 +1958,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.29", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.29.tgz", - "integrity": "sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q==", + "version": "4.3.30", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.30.tgz", + "integrity": "sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -2021,16 +1973,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.32", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.32.tgz", - "integrity": "sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q==", + "version": "4.2.33", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.33.tgz", + "integrity": "sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -2092,13 +2044,13 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.11", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.11.tgz", - "integrity": "sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==", + "version": "4.5.12", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.12.tgz", + "integrity": "sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg==", "license": "Apache-2.0", "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.9", + "@smithy/node-http-handler": "^4.4.10", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", @@ -2545,13 +2497,13 @@ } }, "node_modules/axios": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", - "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -2562,9 +2514,9 @@ "dev": true }, "node_modules/bowser": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", - "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, "node_modules/brace-expansion": { diff --git a/package.json b/package.json index 5dfa0f0..7d739b4 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,10 @@ "author": "The Environment Agency", "license": "OGL", "dependencies": { - "@aws-sdk/client-sns": "3.981.0", + "@aws-sdk/client-sns": "3.988.0", "@xmldom/xmldom": "0.8.11", "feed": "5.2.0", - "axios": "1.13.4", + "axios": "1.13.5", "ioredis": "5.9.2", "joi": "18.0.2", "moment": "2.30.1", From 6b04a64469179b9cdb83f9486e8e468addd451df Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Thu, 12 Feb 2026 10:09:07 +0000 Subject: [PATCH 6/6] Updating the processMessage so that a failed meteoalarm post fails the process as it is integral to future of the service --- lib/functions/processMessage.js | 5 +- test/lib/functions/processMessage.js | 50 +++++++++++++++---- .../lib/functions/processMessageValidation.js | 12 ++++- 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/lib/functions/processMessage.js b/lib/functions/processMessage.js index c16673a..bfe0851 100644 --- a/lib/functions/processMessage.js +++ b/lib/functions/processMessage.js @@ -63,10 +63,7 @@ module.exports.processMessage = async (event) => { await Promise.all([ service.putMessage(dbQuery), redis.set(redisMessage.identifier, redisMessage), - meteoalarm.postWarning(messageV2.toString(), message.identifier).catch(err => { - // Log error but don't fail the entire process if Meteoalarm post fails - console.error(`Failed to post to Meteoalarm: ${err.message}`) - }) + meteoalarm.postWarning(messageV2.toString(), message.identifier) ]) console.log(`Finished processing CAP message: ${message.identifier} for ${message.fwisCode}`) diff --git a/test/lib/functions/processMessage.js b/test/lib/functions/processMessage.js index 12c3477..f0ec86c 100644 --- a/test/lib/functions/processMessage.js +++ b/test/lib/functions/processMessage.js @@ -494,25 +494,55 @@ lab.experiment('processMessage', () => { consoleLogStub.restore() }) - lab.test('Meteoalarm failure does not fail message processing', async () => { - const consoleErrorStub = sinon.stub(console, 'error') + lab.test('Meteoalarm failure triggers error with no SNS notification (no SNS configured)', async () => { + const consoleLogStub = sinon.stub(console, 'log') meteoalarm.postWarning.rejects(new Error('Meteoalarm API unavailable')) const putMessageStub = sinon.stub(service, 'putMessage').resolves() - const response = await processMessage(nwsAlert) + const err = await Code.expect(processMessage(nwsAlert)).to.reject() - // Should still succeed - Code.expect(response.statusCode).to.equal(200) - Code.expect(response.body.identifier).to.equal(identifier) + // Should throw the meteoalarm error + Code.expect(err.message).to.equal('Meteoalarm API unavailable') - // Should have logged the error - Code.expect(consoleErrorStub.calledWith('Failed to post to Meteoalarm: Meteoalarm API unavailable')).to.be.true() + // Should have logged the bodyXml + Code.expect(consoleLogStub.calledWith(nwsAlert.bodyXml)).to.be.true() - // Should have still called the other services + // Should have attempted other services before meteoalarm failed Code.expect(putMessageStub.calledOnce).to.be.true() Code.expect(redis.set.calledOnce).to.be.true() - consoleErrorStub.restore() + consoleLogStub.restore() + }) + + lab.test('Meteoalarm failure triggers error with SNS notification', async () => { + sinon.stub(aws.email, 'publishMessage').resolves() + process.env.CPX_SNS_TOPIC = 'arn:aws:sns:region:account:topic' + const consoleLogStub = sinon.stub(console, 'log') + meteoalarm.postWarning.rejects(new Error('Meteoalarm API unavailable')) + + const putMessageStub = sinon.stub(service, 'putMessage').resolves() + + const err = await Code.expect(processMessage(nwsAlert)).to.reject() + + // Should throw the error with [500] prefix + Code.expect(err.message).to.contain('[500]') + Code.expect(err.message).to.contain('Meteoalarm API unavailable') + + // Should have sent SNS notification + Code.expect(aws.email.publishMessage.calledOnce).to.be.true() + const publishArgs = aws.email.publishMessage.firstCall.args[0] + Code.expect(publishArgs.receivedMessage).to.equal(JSON.stringify(nwsAlert.bodyXml)) + Code.expect(publishArgs.errorMessage).to.equal('Meteoalarm API unavailable') + Code.expect(publishArgs.dateCreated).to.exist() + + // Should have logged the bodyXml + Code.expect(consoleLogStub.calledWith(nwsAlert.bodyXml)).to.be.true() + + // Should have attempted other services before meteoalarm failed + Code.expect(putMessageStub.calledOnce).to.be.true() + Code.expect(redis.set.calledOnce).to.be.true() + + consoleLogStub.restore() }) }) diff --git a/test/lib/functions/processMessageValidation.js b/test/lib/functions/processMessageValidation.js index 828b3d6..3d7e69d 100644 --- a/test/lib/functions/processMessageValidation.js +++ b/test/lib/functions/processMessageValidation.js @@ -17,6 +17,7 @@ const fakeService = { const fakeSchema = { validateAsync: async () => ({ error: null }) } const fakeAws = { email: { publishMessage: sinon.stub() } } const fakeRedis = { set: sinon.stub().resolves('OK') } +const fakeMeteoalarm = { postWarning: sinon.stub().resolves({ id: 'meteoalarm-warning-id' }) } const loadWithValidateMock = (validateMock) => { return Proxyquire('../../../lib/functions/processMessage', { @@ -24,7 +25,8 @@ const loadWithValidateMock = (validateMock) => { '../helpers/service': fakeService, '../schemas/processMessageEventSchema': fakeSchema, '../helpers/aws': fakeAws, - '../helpers/redis': fakeRedis + '../helpers/redis': fakeRedis, + '../helpers/meteoalarm': fakeMeteoalarm }).processMessage } @@ -33,8 +35,12 @@ const CPX_SNS_TOPIC = process.env.CPX_SNS_TOPIC lab.experiment('processMessage validation logging', () => { lab.afterEach(() => { process.env.CPX_SNS_TOPIC = CPX_SNS_TOPIC + fakeAws.email.publishMessage.resetHistory() + fakeRedis.set.resetHistory() + fakeMeteoalarm.postWarning.resetHistory() }) lab.test('Throws error when pre/post validation has errors with no SNS message', async () => { + delete process.env.CPX_SNS_TOPIC const validateMock = async () => ({ errors: [{ message: 'oops' }] }) const processMessage = loadWithValidateMock(validateMock) @@ -77,11 +83,13 @@ lab.experiment('processMessage validation logging', () => { process.env.CPX_SNS_TOPIC = true const awsStub = { email: { publishMessage: sinon.stub() } } const redisStub = { set: sinon.stub().resolves('OK') } + const meteoalarmStub = { postWarning: sinon.stub().resolves({ id: 'meteoalarm-warning-id' }) } const processMessage = Proxyquire('../../../lib/functions/processMessage', { '../helpers/service': fakeService, '../schemas/processMessageEventSchema': fakeSchema, '../helpers/aws': awsStub, - '../helpers/redis': redisStub + '../helpers/redis': redisStub, + '../helpers/meteoalarm': meteoalarmStub }).processMessage const ret = await processMessage(nwsAlert)