From 3af507bce4d08246e6f9812b2a015f36d10ffebb Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Fri, 23 Jan 2026 07:47:20 +0000 Subject: [PATCH 1/5] Redis integration --- docker/.env | 1 + docker/infrastructure.yml | 15 ++ docker/scripts/register-lambda-functions.sh | 3 +- lib/functions/processMessage.js | 6 +- lib/helpers/message.js | 25 +- lib/helpers/redis.js | 47 ++++ lib/models/message.js | 2 +- package-lock.json | 92 ++++++- package.json | 1 + readme.md | 1 + test/lib/functions/processMessage.js | 35 +++ .../lib/functions/processMessageValidation.js | 17 +- test/lib/helpers/message.js | 120 +++++++++ test/lib/helpers/redis.js | 252 ++++++++++++++++++ test/lib/models/message.js | 17 +- 15 files changed, 615 insertions(+), 19 deletions(-) create mode 100644 lib/helpers/redis.js create mode 100644 test/lib/helpers/redis.js diff --git a/docker/.env b/docker/.env index 7a8df33..67ed773 100644 --- a/docker/.env +++ b/docker/.env @@ -52,6 +52,7 @@ CPX_DB_PASSWORD=cx CPX_DB_URL=jdbc:postgresql://capxmldb:5432/cx CPX_DB_USERNAME=cx CPX_DB_CONNECTION_STRING=postgres://${CPX_DB_USERNAME}:${CPX_DB_PASSWORD}@capxmldb/${CPX_DB_NAME} +CPX_REDIS_HOST=cap-xml-redis PGADMIN_DEFAULT_PASSWORD=pgadmin POSTGRES_PASSWORD=postgres LIQUIBASE_COMMAND_CHANGELOG_FILE=./changelog/db.changelog-master.xml diff --git a/docker/infrastructure.yml b/docker/infrastructure.yml index e3110f4..81a9b03 100644 --- a/docker/infrastructure.yml +++ b/docker/infrastructure.yml @@ -39,6 +39,21 @@ services: interval: 10s timeout: 5s retries: 5 + cap-xml-redis: + container_name: cap-xml-redis + image: redis:8.4-alpine + ports: + - "6379:6379" + deploy: + restart_policy: + condition: on-failure + networks: + ls: + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 10 volumes: capxmlpgdata: external: true diff --git a/docker/scripts/register-lambda-functions.sh b/docker/scripts/register-lambda-functions.sh index faf5ab5..fa9a9a1 100755 --- a/docker/scripts/register-lambda-functions.sh +++ b/docker/scripts/register-lambda-functions.sh @@ -13,7 +13,8 @@ cpx_db_password=$(echo CPX_DB_PASSWORD=$CPX_DB_PASSWORD) cpx_db_name=$(echo CPX_DB_NAME=$CPX_DB_NAME) cpx_db_host=$(echo CPX_DB_HOST=$CPX_DB_HOST) cpx_agw_url=$(echo CPX_AGW_URL=$deployed_cpx_agw_url) -set -- $cpx_db_username $cpx_db_password $cpx_db_name $cpx_db_host $cpx_agw_url +cpx_redis_host=$(echo CPX_REDIS_HOST=$CPX_REDIS_HOST) +set -- $cpx_db_username $cpx_db_password $cpx_db_name $cpx_db_host $cpx_agw_url $cpx_redis_host 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 37ba1b4..5f1a1fe 100644 --- a/lib/functions/processMessage.js +++ b/lib/functions/processMessage.js @@ -14,6 +14,7 @@ const Message = require('../models/message') const EA_WHO = '2.49.0.0.826.1' const CODE = 'MCP:v2.0' const severityV2Mapping = require('../models/v2MessageMapping') +const redis = require('../helpers/redis') module.exports.processMessage = async (event) => { try { @@ -56,8 +57,9 @@ module.exports.processMessage = async (event) => { throw new Error(JSON.stringify(errors)) } - // store the message in database - await service.putMessage(message.putQuery(message, messageV2)) + const { message: redisMessage, query: rdsMessage } = message.putQuery(message, messageV2) + // store the message in database and redis/elasticache + await Promise.all([service.putMessage(rdsMessage), redis.set(redisMessage.identifier, redisMessage)]) console.log(`Finished processing CAP message: ${message.identifier} for ${message.fwisCode}`) return { diff --git a/lib/helpers/message.js b/lib/helpers/message.js index 9b2fe64..e832357 100644 --- a/lib/helpers/message.js +++ b/lib/helpers/message.js @@ -6,6 +6,7 @@ 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') +const redis = require('../helpers/redis') module.exports.getMessage = async (event, v2) => { const { error } = eventSchema.validate(event) @@ -14,15 +15,25 @@ module.exports.getMessage = async (event, v2) => { 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') + // Fetch message from redis, else get from postgres + let body + const key = event.pathParameters.id + const cachedMessage = await redis.get(key) + + if (cachedMessage) { + body = v2 ? cachedMessage.alert_v2 : cachedMessage.alert + } else { + const ret = await service.getMessage(key) + if (!ret?.rows || !Array.isArray(ret.rows) || ret.rows.length < 1 || !ret.rows[0].getmessage) { + console.log('No message found for ' + key) + throw new Error('No message found') + } + const message = ret.rows[0].getmessage + body = v2 ? message.alert_v2 : message.alert + // Cache the message in redis + await redis.set(key, message) } - const body = v2 ? ret.rows[0].getmessage.alert_v2 : ret.rows[0].getmessage.alert - const validationResult = await validateXML({ xml: [{ fileName: 'message.xml', diff --git a/lib/helpers/redis.js b/lib/helpers/redis.js new file mode 100644 index 0000000..a403fd7 --- /dev/null +++ b/lib/helpers/redis.js @@ -0,0 +1,47 @@ +'use strict' + +const Redis = require('ioredis') +let client + +const getClient = () => { + if (!client || client.status === 'end' || client.status === 'close') { + client = new Redis({ + host: process.env.CPX_REDIS_HOST, + port: process.env.CPX_REDIS_PORT || 6379, + connectTimeout: 10000 + }) + + client.on('error', (error) => { + console.error('Redis connection error:', error) + }) + + client.on('connect', () => { + console.log('Redis connected successfully') + }) + } + return client +} + +module.exports = { + get: async (key) => { + const redisClient = getClient() + const value = await redisClient.get(key) + if (value === null) { + return null + } + try { + return JSON.parse(value) + } catch (error) { + // If parsing fails, return raw string value + return value + } + }, + set: async (key, value, ttl = null) => { + const redisClient = getClient() + const serializedValue = typeof value === 'object' ? JSON.stringify(value) : value + if (ttl) { + return redisClient.setex(key, ttl, serializedValue) + } + return redisClient.set(key, serializedValue) + } +} diff --git a/lib/models/message.js b/lib/models/message.js index 5408909..f3e2b71 100644 --- a/lib/models/message.js +++ b/lib/models/message.js @@ -195,7 +195,7 @@ class Message { references_v2: messageV2.references, alert_v2: messageV2.toString() } - return messages.insert(message).toQuery() + return { message, query: messages.insert(message).toQuery() } } } diff --git a/package-lock.json b/package-lock.json index 441dd30..5172a43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@aws-sdk/client-sns": "3.932.0", "@xmldom/xmldom": "0.8.11", "feed": "5.1.0", + "ioredis": "^5.9.2", "joi": "18.0.1", "moment": "2.30.1", "pg": "8.16.3", @@ -1547,6 +1548,12 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@ioredis/commands": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", + "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", + "license": "MIT" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2805,6 +2812,15 @@ "node": ">=8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2908,7 +2924,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -2961,6 +2976,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -4315,6 +4339,30 @@ "node": ">= 0.4" } }, + "node_modules/ioredis": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz", + "integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -4885,6 +4933,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -4893,6 +4947,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5004,8 +5064,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -5656,6 +5715,27 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -6056,6 +6136,12 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/standard-engine": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-15.1.0.tgz", diff --git a/package.json b/package.json index 8adb999..1f56362 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@aws-sdk/client-sns": "3.932.0", "@xmldom/xmldom": "0.8.11", "feed": "5.1.0", + "ioredis": "^5.9.2", "joi": "18.0.1", "moment": "2.30.1", "pg": "8.16.3", diff --git a/readme.md b/readme.md index 460dc91..1743282 100644 --- a/readme.md +++ b/readme.md @@ -17,6 +17,7 @@ This project provides CAP XML services through the use of AWS Lambda. | CPX_DB_NAME | Database name | yes | | | | CPX_DB_HOST | Database host | yes | | | | CPX_AGW_URL | API Gateway URL | yes | | | +| CPX_REDIS_HOST | Redis/Elasticache host| no | localhost | | ## Prerequisites diff --git a/test/lib/functions/processMessage.js b/test/lib/functions/processMessage.js index e8aec78..170cf52 100644 --- a/test/lib/functions/processMessage.js +++ b/test/lib/functions/processMessage.js @@ -10,6 +10,7 @@ const xml2js = require('xml2js') const processMessage = require('../../../lib/functions/processMessage').processMessage const service = require('../../../lib/helpers/service') const aws = require('../../../lib/helpers/aws') +const redis = require('../../../lib/helpers/redis') 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') } @@ -29,6 +30,17 @@ const expectResponse = (response, putQuery, severity = 'Minor', status = 'Test', 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) + expectRedisSet(identifier) +} + +const expectRedisSet = (identifier) => { + Code.expect(redis.set.calledOnce).to.be.true() + const [key, value] = redis.set.firstCall.args + Code.expect(key).to.equal(identifier) + Code.expect(value).to.be.an.object() + Code.expect(value.identifier).to.equal(identifier) + Code.expect(value.alert).to.not.be.empty() + Code.expect(value.alert_v2).to.not.be.empty() } const expectResponseAndPutQuery = (response, putQuery, status, msgType, references, previousReferences) => { @@ -151,6 +163,8 @@ lab.experiment('processMessage', () => { identifier: '4eb3b7350ab7aa443650fc9351f2' }] }) + // mock redis + sinon.stub(redis, 'set').resolves('OK') }) lab.afterEach(() => { @@ -169,14 +183,17 @@ lab.experiment('processMessage', () => { }) // do alert and test output xml + redis.set.resetHistory() let response = await processMessage(nwsAlert) expectResponse(response, putQuery, 'Minor') // do warning and test output xml + redis.set.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') }) expectResponse(response, putQuery, 'Moderate') // do severe warning and test output xml + redis.set.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) expectResponse(response, putQuery, 'Severe') }) @@ -192,14 +209,17 @@ lab.experiment('processMessage', () => { putQuery = query }) // do alert and test output xml + redis.set.resetHistory() let response = await processMessage(nwsAlert) expectResponse(response, putQuery, 'Minor') // do warning and test output xml + redis.set.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Moderate') }) expectResponse(response, putQuery, 'Moderate') // do severe warning and test output xml + redis.set.resetHistory() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) expectResponse(response, putQuery, 'Severe') }) @@ -212,14 +232,17 @@ lab.experiment('processMessage', () => { }) // do alert and test output xml + redis.set.resetHistory() let response = await processMessage(nwsAlert) expectResponse(response, putQuery, 'Minor', 'Actual') // do warning and test output xml + redis.set.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() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) expectResponse(response, putQuery, 'Severe', 'Actual') }) @@ -242,14 +265,17 @@ lab.experiment('processMessage', () => { }) // do alert and test output xml + redis.set.resetHistory() let response = await processMessage(nwsAlert) expectResponse(response, putQuery, 'Minor', 'Test', 'Update', true) // do warning and test output xml + redis.set.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() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) expectResponse(response, putQuery, 'Severe', 'Test', 'Update', true) }) @@ -273,14 +299,17 @@ lab.experiment('processMessage', () => { }) // do alert and test output xml + redis.set.resetHistory() let response = await processMessage(nwsAlert) expectResponse(response, putQuery, 'Minor', 'Actual', 'Update', true) // do warning and test output xml + redis.set.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() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) expectResponse(response, putQuery, 'Severe', 'Actual', 'Update', true) }) @@ -308,14 +337,17 @@ lab.experiment('processMessage', () => { const alert = { bodyXml: nwsAlert.bodyXml.replace('quickdial code: 210010.', '') } // do alert and test output xml + redis.set.resetHistory() let response = await processMessage(alert) expectResponse(response, putQuery, 'Minor', 'Test', 'Update', true, true, false) // do warning and test output xml + redis.set.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() response = await processMessage({ bodyXml: alert.bodyXml.replace('Minor', 'Severe') }) expectResponse(response, putQuery, 'Severe', 'Test', 'Update', true, true, false) }) @@ -341,14 +373,17 @@ lab.experiment('processMessage', () => { }) // do alert and test output xml + redis.set.resetHistory() let response = await processMessage(nwsAlert) expectResponse(response, putQuery, 'Minor', 'Actual', 'Update', true, true) // do warning and test output xml + redis.set.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() response = await processMessage({ bodyXml: nwsAlert.bodyXml.replace('Minor', 'Severe') }) expectResponse(response, putQuery, 'Severe', 'Actual', 'Update', true, true) }) diff --git a/test/lib/functions/processMessageValidation.js b/test/lib/functions/processMessageValidation.js index b288074..828b3d6 100644 --- a/test/lib/functions/processMessageValidation.js +++ b/test/lib/functions/processMessageValidation.js @@ -16,13 +16,15 @@ const fakeService = { const fakeSchema = { validateAsync: async () => ({ error: null }) } const fakeAws = { email: { publishMessage: sinon.stub() } } +const fakeRedis = { set: sinon.stub().resolves('OK') } const loadWithValidateMock = (validateMock) => { return Proxyquire('../../../lib/functions/processMessage', { 'xmllint-wasm': { validateXML: validateMock }, '../helpers/service': fakeService, '../schemas/processMessageEventSchema': fakeSchema, - '../helpers/aws': fakeAws + '../helpers/aws': fakeAws, + '../helpers/redis': fakeRedis }).processMessage } @@ -74,10 +76,12 @@ lab.experiment('processMessage validation logging', () => { lab.test('Processes a message correctly if valid with actual xmllint', async () => { process.env.CPX_SNS_TOPIC = true const awsStub = { email: { publishMessage: sinon.stub() } } + const redisStub = { set: sinon.stub().resolves('OK') } const processMessage = Proxyquire('../../../lib/functions/processMessage', { '../helpers/service': fakeService, '../schemas/processMessageEventSchema': fakeSchema, - '../helpers/aws': awsStub + '../helpers/aws': awsStub, + '../helpers/redis': redisStub }).processMessage const ret = await processMessage(nwsAlert) @@ -89,6 +93,15 @@ lab.experiment('processMessage validation logging', () => { Code.expect(ret.body.status).to.equal('Test') Code.expect(awsStub.email.publishMessage.callCount).to.equal(0) + + // Verify redis.set was called correctly + Code.expect(redisStub.set.calledOnce).to.be.true() + const [key, value] = redisStub.set.firstCall.args + Code.expect(key).to.equal('4eb3b7350ab7aa443650fc9351f02940E') + Code.expect(value).to.be.an.object() + Code.expect(value.identifier).to.equal('4eb3b7350ab7aa443650fc9351f02940E') + Code.expect(value.alert).to.not.be.empty() + Code.expect(value.alert_v2).to.not.be.empty() }) lab.test('Throws validation errors for empty fields', async () => { diff --git a/test/lib/helpers/message.js b/test/lib/helpers/message.js index 71e68f6..9ac0e05 100644 --- a/test/lib/helpers/message.js +++ b/test/lib/helpers/message.js @@ -8,6 +8,7 @@ const fs = require('fs') const path = require('path') const { getMessage } = require('../../../lib/helpers/message') const service = require('../../../lib/helpers/service') +const redis = require('../../../lib/helpers/redis') 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 @@ -19,6 +20,13 @@ lab.experiment('getMessage helper', () => { id: '4eb3b7350ab7aa443650fc9351f' } } + // Mock redis by default to return null (cache miss) + sinon.stub(redis, 'get').resolves(null) + sinon.stub(redis, 'set').resolves('OK') + }) + + lab.afterEach(() => { + sinon.restore() }) lab.experiment('getMessage v1 (v2=false)', () => { @@ -332,4 +340,116 @@ lab.experiment('getMessage helper', () => { } }) }) + + lab.experiment('Redis caching behavior', () => { + const mockMessage = { + alert: 'cached v1', + alert_v2: 'cached v2' + } + + lab.test('Uses cached message from redis when available (v1)', async () => { + redis.get.resolves(mockMessage) + const serviceStub = sinon.stub(service, 'getMessage') + + const ret = await getMessage(event, false) + + Code.expect(redis.get.calledOnce).to.be.true() + Code.expect(redis.get.calledWith('4eb3b7350ab7aa443650fc9351f')).to.be.true() + Code.expect(serviceStub.called).to.be.false() + Code.expect(redis.set.called).to.be.false() + Code.expect(ret.body).to.equal('cached v1') + }) + + lab.test('Uses cached message from redis when available (v2)', async () => { + redis.get.resolves(mockMessage) + const serviceStub = sinon.stub(service, 'getMessage') + + const ret = await getMessage(event, true) + + Code.expect(redis.get.calledOnce).to.be.true() + Code.expect(redis.get.calledWith('4eb3b7350ab7aa443650fc9351f')).to.be.true() + Code.expect(serviceStub.called).to.be.false() + Code.expect(redis.set.called).to.be.false() + Code.expect(ret.body).to.equal('cached v2') + }) + + lab.test('Fetches from database and caches in redis on cache miss', async () => { + redis.get.resolves(null) + service.getMessage = () => Promise.resolve({ + rows: [{ + getmessage: { + alert: 'db v1', + alert_v2: 'db v2' + } + }] + }) + + const ret = await getMessage(event, false) + + Code.expect(redis.get.calledOnce).to.be.true() + Code.expect(redis.get.calledWith('4eb3b7350ab7aa443650fc9351f')).to.be.true() + Code.expect(redis.set.calledOnce).to.be.true() + const [key, value] = redis.set.firstCall.args + Code.expect(key).to.equal('4eb3b7350ab7aa443650fc9351f') + Code.expect(value).to.equal({ + alert: 'db v1', + alert_v2: 'db v2' + }) + Code.expect(ret.body).to.equal('db v1') + }) + + lab.test('Caches entire message object with both alert versions', async () => { + redis.get.resolves(null) + const dbMessage = { + alert: 'full v1', + alert_v2: 'full v2', + extraField: 'should be cached too' + } + service.getMessage = () => Promise.resolve({ + rows: [{ getmessage: dbMessage }] + }) + + await getMessage(event, true) + + Code.expect(redis.set.calledOnce).to.be.true() + const [key, cachedValue] = redis.set.firstCall.args + Code.expect(key).to.equal('4eb3b7350ab7aa443650fc9351f') + Code.expect(cachedValue).to.equal(dbMessage) + }) + + lab.test('Does not cache when database returns no results', async () => { + redis.get.resolves(null) + service.getMessage = () => Promise.resolve({ rows: [] }) + + try { + await getMessage(event, false) + } catch (err) { + Code.expect(redis.set.called).to.be.false() + Code.expect(err.message).to.equal('No message found') + } + }) + + lab.test('Does not cache when database throws error', async () => { + redis.get.resolves(null) + service.getMessage = () => Promise.reject(new Error('DB error')) + + try { + await getMessage(event, false) + } catch (err) { + Code.expect(redis.set.called).to.be.false() + Code.expect(err.message).to.equal('DB error') + } + }) + + lab.test('Uses correct cache key from event pathParameters', async () => { + event.pathParameters.id = 'abc123def456' + redis.get.resolves(mockMessage) + const serviceStub = sinon.stub(service, 'getMessage') + + await getMessage(event, false) + + Code.expect(redis.get.calledWith('abc123def456')).to.be.true() + Code.expect(serviceStub.called).to.be.false() + }) + }) }) diff --git a/test/lib/helpers/redis.js b/test/lib/helpers/redis.js new file mode 100644 index 0000000..41f9c3d --- /dev/null +++ b/test/lib/helpers/redis.js @@ -0,0 +1,252 @@ +'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('redis helper', () => { + let redis + let mockRedisInstance + let mockRedis + + lab.beforeEach(() => { + // Mock environment + process.env = { ...ORIGINAL_ENV } + process.env.CPX_REDIS_HOST = 'mock-redis-host' + process.env.CPX_REDIS_PORT = '6379' + + // Mock ioredis client + mockRedisInstance = { + get: sinon.stub(), + set: sinon.stub(), + setex: sinon.stub(), + on: sinon.stub(), + status: 'ready' + } + + mockRedis = sinon.stub().returns(mockRedisInstance) + + // Load module with mocked ioredis + redis = Proxyquire('../../../lib/helpers/redis', { + ioredis: mockRedis + }) + }) + + lab.afterEach(() => { + sinon.restore() + process.env = ORIGINAL_ENV + }) + + lab.test('get initializes redis client with correct config', async () => { + mockRedisInstance.get.resolves(null) + + await redis.get('test-key') + + Code.expect(mockRedis.calledOnce).to.be.true() + Code.expect(mockRedis.firstCall.args[0]).to.equal({ + host: 'mock-redis-host', + port: '6379', + connectTimeout: 10000 + }) + }) + + lab.test('uses default port when CPX_REDIS_PORT is not set', async () => { + delete process.env.CPX_REDIS_PORT + + redis = Proxyquire('../../../lib/helpers/redis', { + ioredis: mockRedis + }) + + mockRedisInstance.get.resolves(null) + await redis.get('test-key') + + Code.expect(mockRedis.firstCall.args[0].port).to.equal(6379) + }) + + lab.test('get retrieves and parses JSON value', async () => { + const mockValue = { foo: 'bar', count: 42 } + mockRedisInstance.get.resolves(JSON.stringify(mockValue)) + + const result = await redis.get('test-key') + + Code.expect(mockRedisInstance.get.calledWith('test-key')).to.be.true() + Code.expect(result).to.equal(mockValue) + }) + + lab.test('get returns string value when not JSON', async () => { + mockRedisInstance.get.resolves('plain-string-value') + + const result = await redis.get('test-key') + + Code.expect(result).to.equal('plain-string-value') + }) + + lab.test('get returns string value when JSON.parse fails', async () => { + mockRedisInstance.get.resolves('{invalid json') + + const result = await redis.get('test-key') + + Code.expect(result).to.equal('{invalid json') + }) + + lab.test('get returns null when key does not exist', async () => { + mockRedisInstance.get.resolves(null) + + const result = await redis.get('non-existent-key') + + Code.expect(result).to.be.null() + }) + + lab.test('set stores object as JSON string', async () => { + const mockObject = { foo: 'bar', count: 42 } + mockRedisInstance.set.resolves('OK') + + await redis.set('test-key', mockObject) + + Code.expect(mockRedisInstance.set.calledWith('test-key', JSON.stringify(mockObject))).to.be.true() + }) + + lab.test('set stores string value directly', async () => { + mockRedisInstance.set.resolves('OK') + + await redis.set('test-key', 'string-value') + + Code.expect(mockRedisInstance.set.calledWith('test-key', 'string-value')).to.be.true() + }) + + lab.test('set uses setex when ttl is provided', async () => { + const mockObject = { foo: 'bar' } + mockRedisInstance.setex.resolves('OK') + + await redis.set('test-key', mockObject, 300) + + Code.expect(mockRedisInstance.setex.calledWith('test-key', 300, JSON.stringify(mockObject))).to.be.true() + Code.expect(mockRedisInstance.set.called).to.be.false() + }) + + lab.test('set uses setex when ttl is 0', async () => { + mockRedisInstance.setex.resolves('OK') + + await redis.set('test-key', 'value', 0) + + Code.expect(mockRedisInstance.set.called).to.be.true() + Code.expect(mockRedisInstance.setex.called).to.be.false() + }) + + lab.test('set stores number value directly', async () => { + mockRedisInstance.set.resolves('OK') + + await redis.set('test-key', 12345) + + Code.expect(mockRedisInstance.set.calledWith('test-key', 12345)).to.be.true() + }) + + lab.test('recreates client when status is end', async () => { + // First call creates client + mockRedisInstance.get.resolves('value1') + await redis.get('key1') + + Code.expect(mockRedis.calledOnce).to.be.true() + + // Simulate client ending + mockRedisInstance.status = 'end' + + // Second call should recreate client + await redis.get('key2') + + Code.expect(mockRedis.calledTwice).to.be.true() + }) + + lab.test('recreates client when status is close', async () => { + // First call creates client + mockRedisInstance.get.resolves('value1') + await redis.get('key1') + + Code.expect(mockRedis.calledOnce).to.be.true() + + // Simulate client closing + mockRedisInstance.status = 'close' + + // Second call should recreate client + await redis.get('key2') + + Code.expect(mockRedis.calledTwice).to.be.true() + }) + + lab.test('registers error event handler on client', async () => { + mockRedisInstance.get.resolves(null) + + await redis.get('test-key') + + Code.expect(mockRedisInstance.on.calledWith('error')).to.be.true() + }) + + lab.test('registers connect event handler on client', async () => { + mockRedisInstance.get.resolves(null) + + await redis.get('test-key') + + Code.expect(mockRedisInstance.on.calledWith('connect')).to.be.true() + }) + + lab.test('error handler logs errors', async () => { + const consoleErrorStub = sinon.stub(console, 'error') + mockRedisInstance.get.resolves(null) + + await redis.get('test-key') + + const errorHandler = mockRedisInstance.on.getCalls().find(call => call.args[0] === 'error') + const mockError = new Error('Connection failed') + errorHandler.args[1](mockError) + + Code.expect(consoleErrorStub.calledWith('Redis connection error:', mockError)).to.be.true() + + consoleErrorStub.restore() + }) + + lab.test('connect handler logs success', async () => { + const consoleLogStub = sinon.stub(console, 'log') + mockRedisInstance.get.resolves(null) + + await redis.get('test-key') + + const connectHandler = mockRedisInstance.on.getCalls().find(call => call.args[0] === 'connect') + connectHandler.args[1]() + + Code.expect(consoleLogStub.calledWith('Redis connected successfully')).to.be.true() + + consoleLogStub.restore() + }) + + lab.test('get initializes client automatically on first call', async () => { + mockRedisInstance.get.resolves('"test-value"') + + await redis.get('test-key') + + Code.expect(mockRedis.called).to.be.true() + }) + + lab.test('set initializes client automatically on first call', async () => { + mockRedisInstance.set.resolves('OK') + + await redis.set('test-key', 'value') + + Code.expect(mockRedis.called).to.be.true() + }) + + lab.test('reuses existing client across multiple calls', async () => { + mockRedisInstance.get.resolves(null) + mockRedisInstance.set.resolves('OK') + + await redis.get('key1') + await redis.set('key2', 'value') + await redis.get('key3') + + Code.expect(mockRedis.calledOnce).to.be.true() + }) +}) diff --git a/test/lib/models/message.js b/test/lib/models/message.js index 66ea022..e881793 100644 --- a/test/lib/models/message.js +++ b/test/lib/models/message.js @@ -197,10 +197,11 @@ lab.experiment('Message class', () => { Code.expect(xmlOut).to.include('4eb3b7350ab7aa443650fc9351f02940E') }) - lab.test('putQuery generates SQL insert with correct values', () => { - const sql = message.putQuery(message, messageV2) + lab.test('putQuery generates message for redis and SQL insert with correct values', () => { + const { message: redisMessage, query: sql } = message.putQuery(message, messageV2) + + // Verify SQL query 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() @@ -212,6 +213,16 @@ lab.experiment('Message class', () => { 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() + + // Verify redis message object + Code.expect(redisMessage).to.be.an.object() + Code.expect(redisMessage.identifier).to.equal('4eb3b7350ab7aa443650fc9351f02940E') + Code.expect(redisMessage.alert).to.not.be.empty() + Code.expect(redisMessage.alert_v2).to.not.be.empty() + Code.expect(redisMessage.alert).to.be.a.string() + Code.expect(redisMessage.alert_v2).to.be.a.string() + Code.expect(redisMessage.alert).to.include('4eb3b7350ab7aa443650fc9351f02940E') + Code.expect(redisMessage.alert_v2).to.include('4eb3b7350ab7aa443650fc9351f02940E') }) lab.test('blank message results in blank fields', () => { From 1106564a25086c8e57ff70946f9f3cdb11645134 Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Fri, 23 Jan 2026 09:01:27 +0000 Subject: [PATCH 2/5] made redis-port mandatory as is technically for aws build --- docker/.env | 1 + lib/helpers/redis.js | 4 ++-- readme.md | 3 ++- test/lib/helpers/redis.js | 13 ------------- 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/docker/.env b/docker/.env index 67ed773..5671210 100644 --- a/docker/.env +++ b/docker/.env @@ -53,6 +53,7 @@ CPX_DB_URL=jdbc:postgresql://capxmldb:5432/cx CPX_DB_USERNAME=cx CPX_DB_CONNECTION_STRING=postgres://${CPX_DB_USERNAME}:${CPX_DB_PASSWORD}@capxmldb/${CPX_DB_NAME} CPX_REDIS_HOST=cap-xml-redis +CPX_REDIS_HOST=6379 PGADMIN_DEFAULT_PASSWORD=pgadmin POSTGRES_PASSWORD=postgres LIQUIBASE_COMMAND_CHANGELOG_FILE=./changelog/db.changelog-master.xml diff --git a/lib/helpers/redis.js b/lib/helpers/redis.js index a403fd7..1760e58 100644 --- a/lib/helpers/redis.js +++ b/lib/helpers/redis.js @@ -7,7 +7,7 @@ const getClient = () => { if (!client || client.status === 'end' || client.status === 'close') { client = new Redis({ host: process.env.CPX_REDIS_HOST, - port: process.env.CPX_REDIS_PORT || 6379, + port: process.env.CPX_REDIS_PORT, connectTimeout: 10000 }) @@ -32,7 +32,7 @@ module.exports = { try { return JSON.parse(value) } catch (error) { - // If parsing fails, return raw string value + console.error(error) return value } }, diff --git a/readme.md b/readme.md index 1743282..4dac974 100644 --- a/readme.md +++ b/readme.md @@ -17,7 +17,8 @@ This project provides CAP XML services through the use of AWS Lambda. | CPX_DB_NAME | Database name | yes | | | | CPX_DB_HOST | Database host | yes | | | | CPX_AGW_URL | API Gateway URL | yes | | | -| CPX_REDIS_HOST | Redis/Elasticache host| no | localhost | | +| CPX_REDIS_HOST | Redis/Elasticache host| yes | | | +| CPX_REDIS_PORT | Redis/Elasticache port| yes | | | ## Prerequisites diff --git a/test/lib/helpers/redis.js b/test/lib/helpers/redis.js index 41f9c3d..3c3d65d 100644 --- a/test/lib/helpers/redis.js +++ b/test/lib/helpers/redis.js @@ -55,19 +55,6 @@ lab.experiment('redis helper', () => { }) }) - lab.test('uses default port when CPX_REDIS_PORT is not set', async () => { - delete process.env.CPX_REDIS_PORT - - redis = Proxyquire('../../../lib/helpers/redis', { - ioredis: mockRedis - }) - - mockRedisInstance.get.resolves(null) - await redis.get('test-key') - - Code.expect(mockRedis.firstCall.args[0].port).to.equal(6379) - }) - lab.test('get retrieves and parses JSON value', async () => { const mockValue = { foo: 'bar', count: 42 } mockRedisInstance.get.resolves(JSON.stringify(mockValue)) From be0183a505cfa5125c7a1ac156224ab9fc5893fc Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Fri, 23 Jan 2026 15:44:14 +0000 Subject: [PATCH 3/5] =?UTF-8?q?adding=20tls=20to=20redis=20connection?= =?UTF-8?q?=C2=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/.env | 3 ++- docker/scripts/register-lambda-functions.sh | 4 +++- lib/helpers/redis.js | 1 + readme.md | 1 + test/lib/helpers/redis.js | 17 ++++++++++++++++- 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docker/.env b/docker/.env index 5671210..a013eee 100644 --- a/docker/.env +++ b/docker/.env @@ -53,7 +53,8 @@ CPX_DB_URL=jdbc:postgresql://capxmldb:5432/cx CPX_DB_USERNAME=cx CPX_DB_CONNECTION_STRING=postgres://${CPX_DB_USERNAME}:${CPX_DB_PASSWORD}@capxmldb/${CPX_DB_NAME} CPX_REDIS_HOST=cap-xml-redis -CPX_REDIS_HOST=6379 +CPX_REDIS_PORT=6379 +CPX_REDIS_TLS=false PGADMIN_DEFAULT_PASSWORD=pgadmin POSTGRES_PASSWORD=postgres LIQUIBASE_COMMAND_CHANGELOG_FILE=./changelog/db.changelog-master.xml diff --git a/docker/scripts/register-lambda-functions.sh b/docker/scripts/register-lambda-functions.sh index fa9a9a1..7acde00 100755 --- a/docker/scripts/register-lambda-functions.sh +++ b/docker/scripts/register-lambda-functions.sh @@ -14,7 +14,9 @@ cpx_db_name=$(echo CPX_DB_NAME=$CPX_DB_NAME) cpx_db_host=$(echo CPX_DB_HOST=$CPX_DB_HOST) cpx_agw_url=$(echo CPX_AGW_URL=$deployed_cpx_agw_url) cpx_redis_host=$(echo CPX_REDIS_HOST=$CPX_REDIS_HOST) -set -- $cpx_db_username $cpx_db_password $cpx_db_name $cpx_db_host $cpx_agw_url $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 custom_environment_variables=$(printf '%s,' "$@" | sed 's/,*$//g') # Iterate over each file in lambda_functions_dir diff --git a/lib/helpers/redis.js b/lib/helpers/redis.js index 1760e58..2db1b75 100644 --- a/lib/helpers/redis.js +++ b/lib/helpers/redis.js @@ -8,6 +8,7 @@ const getClient = () => { client = new Redis({ host: process.env.CPX_REDIS_HOST, port: process.env.CPX_REDIS_PORT, + tls: process.env.CPX_REDIS_TLS === 'true' ? { checkServerIdentity: () => undefined } : undefined, connectTimeout: 10000 }) diff --git a/readme.md b/readme.md index 4dac974..1d732cf 100644 --- a/readme.md +++ b/readme.md @@ -19,6 +19,7 @@ This project provides CAP XML services through the use of AWS Lambda. | CPX_AGW_URL | API Gateway URL | yes | | | | CPX_REDIS_HOST | Redis/Elasticache host| yes | | | | CPX_REDIS_PORT | Redis/Elasticache port| yes | | | +| CPX_REDIS_TLS | Redis/Elasticache tls | yes | | | ## Prerequisites diff --git a/test/lib/helpers/redis.js b/test/lib/helpers/redis.js index 3c3d65d..50795cd 100644 --- a/test/lib/helpers/redis.js +++ b/test/lib/helpers/redis.js @@ -51,7 +51,22 @@ lab.experiment('redis helper', () => { Code.expect(mockRedis.firstCall.args[0]).to.equal({ host: 'mock-redis-host', port: '6379', - connectTimeout: 10000 + connectTimeout: 10000, + tls: undefined + }) + }) + + lab.test('get initializes redis client with correct config with tls', async () => { + mockRedisInstance.get.resolves(null) + process.env.CPX_REDIS_TLS = 'true' + await redis.get('test-key') + + Code.expect(mockRedis.calledOnce).to.be.true() + Code.expect(mockRedis.firstCall.args[0]).to.equal({ + host: 'mock-redis-host', + port: '6379', + connectTimeout: 10000, + tls: { checkServerIdentity: () => undefined } }) }) From 24e4c134e4815760b0a8a71c86dc0f963723010d Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Tue, 27 Jan 2026 15:28:30 +0000 Subject: [PATCH 4/5] removing ttl functionality for redis key, and fixing ioredis package --- lib/helpers/redis.js | 5 +---- package.json | 2 +- test/lib/helpers/redis.js | 10 ---------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/lib/helpers/redis.js b/lib/helpers/redis.js index 2db1b75..4cd0b55 100644 --- a/lib/helpers/redis.js +++ b/lib/helpers/redis.js @@ -37,12 +37,9 @@ module.exports = { return value } }, - set: async (key, value, ttl = null) => { + set: async (key, value) => { const redisClient = getClient() const serializedValue = typeof value === 'object' ? JSON.stringify(value) : value - if (ttl) { - return redisClient.setex(key, ttl, serializedValue) - } return redisClient.set(key, serializedValue) } } diff --git a/package.json b/package.json index 1f56362..6abcde2 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@aws-sdk/client-sns": "3.932.0", "@xmldom/xmldom": "0.8.11", "feed": "5.1.0", - "ioredis": "^5.9.2", + "ioredis": "5.9.2", "joi": "18.0.1", "moment": "2.30.1", "pg": "8.16.3", diff --git a/test/lib/helpers/redis.js b/test/lib/helpers/redis.js index 50795cd..977f634 100644 --- a/test/lib/helpers/redis.js +++ b/test/lib/helpers/redis.js @@ -121,16 +121,6 @@ lab.experiment('redis helper', () => { Code.expect(mockRedisInstance.set.calledWith('test-key', 'string-value')).to.be.true() }) - lab.test('set uses setex when ttl is provided', async () => { - const mockObject = { foo: 'bar' } - mockRedisInstance.setex.resolves('OK') - - await redis.set('test-key', mockObject, 300) - - Code.expect(mockRedisInstance.setex.calledWith('test-key', 300, JSON.stringify(mockObject))).to.be.true() - Code.expect(mockRedisInstance.set.called).to.be.false() - }) - lab.test('set uses setex when ttl is 0', async () => { mockRedisInstance.setex.resolves('OK') From c4649454a2b847235afbc9572fd8284a3c8c8202 Mon Sep 17 00:00:00 2001 From: Tedd Mason Date: Tue, 27 Jan 2026 15:39:24 +0000 Subject: [PATCH 5/5] review feedback --- lib/functions/processMessage.js | 4 ++-- lib/helpers/redis.js | 3 ++- test/lib/helpers/redis.js | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/functions/processMessage.js b/lib/functions/processMessage.js index 5f1a1fe..ee04749 100644 --- a/lib/functions/processMessage.js +++ b/lib/functions/processMessage.js @@ -57,9 +57,9 @@ module.exports.processMessage = async (event) => { throw new Error(JSON.stringify(errors)) } - const { message: redisMessage, query: rdsMessage } = message.putQuery(message, messageV2) + const { message: redisMessage, query: dbQuery } = message.putQuery(message, messageV2) // store the message in database and redis/elasticache - await Promise.all([service.putMessage(rdsMessage), redis.set(redisMessage.identifier, redisMessage)]) + await Promise.all([service.putMessage(dbQuery), redis.set(redisMessage.identifier, redisMessage)]) console.log(`Finished processing CAP message: ${message.identifier} for ${message.fwisCode}`) return { diff --git a/lib/helpers/redis.js b/lib/helpers/redis.js index 4cd0b55..906afcb 100644 --- a/lib/helpers/redis.js +++ b/lib/helpers/redis.js @@ -9,6 +9,7 @@ const getClient = () => { host: process.env.CPX_REDIS_HOST, port: process.env.CPX_REDIS_PORT, tls: process.env.CPX_REDIS_TLS === 'true' ? { checkServerIdentity: () => undefined } : undefined, + maxRetriesPerRequest: 3, connectTimeout: 10000 }) @@ -33,7 +34,7 @@ module.exports = { try { return JSON.parse(value) } catch (error) { - console.error(error) + console.error(`Failed to parse Redis value for key ${key}:`, error) return value } }, diff --git a/test/lib/helpers/redis.js b/test/lib/helpers/redis.js index 977f634..f3d95ab 100644 --- a/test/lib/helpers/redis.js +++ b/test/lib/helpers/redis.js @@ -50,6 +50,7 @@ lab.experiment('redis helper', () => { Code.expect(mockRedis.calledOnce).to.be.true() Code.expect(mockRedis.firstCall.args[0]).to.equal({ host: 'mock-redis-host', + maxRetriesPerRequest: 3, port: '6379', connectTimeout: 10000, tls: undefined @@ -64,6 +65,7 @@ lab.experiment('redis helper', () => { Code.expect(mockRedis.calledOnce).to.be.true() Code.expect(mockRedis.firstCall.args[0]).to.equal({ host: 'mock-redis-host', + maxRetriesPerRequest: 3, port: '6379', connectTimeout: 10000, tls: { checkServerIdentity: () => undefined }