diff --git a/lambdas/integrationTestRunner/src/integrationTests/as/modicaCustomChannelChatbot.test.ts b/lambdas/integrationTestRunner/src/integrationTests/as/modicaCustomChannelChatbot.test.ts new file mode 100644 index 0000000000..760456f4a5 --- /dev/null +++ b/lambdas/integrationTestRunner/src/integrationTests/as/modicaCustomChannelChatbot.test.ts @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2021-2025 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { startWebhookReceiverSession } from '../../webhookReceiver/client'; +import { verifyModicaMessageExchange } from '../../modica'; +import { verifyMessageExchange } from '../../verify'; + +const HELPLINE_CODE = 'AS'; +const TEST_TIMEOUT_MILLISECONDS = 5 * 60 * 1000; + +let webhookReceiverSession: ReturnType; +let verifyExchange: ReturnType; + +beforeEach(async () => { + webhookReceiverSession = startWebhookReceiverSession(HELPLINE_CODE); + verifyExchange = verifyModicaMessageExchange(webhookReceiverSession, HELPLINE_CODE); + jest.setTimeout(TEST_TIMEOUT_MILLISECONDS); +}); + +afterEach(async () => { + await webhookReceiverSession.end(); +}); + +test('AS_DEV modica custom channel chatbot integration test', async () => { + await verifyExchange([ + { + sender: 'service-user', + text: `Hello from integration test ${webhookReceiverSession.sessionId}`, + }, + { + sender: 'flex', + text: `Welcome to the helpline. Please answer the following questions.`, + }, + { sender: 'flex', text: `Are you calling about yourself? Please answer Yes or No.` }, + { + sender: 'service-user', + text: `Y`, + }, + { + sender: 'flex', + text: `How old are you?`, + }, + { + sender: 'service-user', + text: `17`, + }, + { + sender: 'flex', + text: `What is your gender?`, + }, + { + sender: 'service-user', + text: `F`, + }, + { + sender: 'flex', + text: `We will transfer you now. Please hold for a counsellor.`, + }, + { + sender: 'flex', + text: `Integration test run completed successfully. 🚀`, + }, + ]); +}); diff --git a/lambdas/integrationTestRunner/src/integrationTests/nz/modicaCustomChannelChatbot.test.ts b/lambdas/integrationTestRunner/src/integrationTests/nz/modicaCustomChannelChatbot.test.ts new file mode 100644 index 0000000000..a42f76a676 --- /dev/null +++ b/lambdas/integrationTestRunner/src/integrationTests/nz/modicaCustomChannelChatbot.test.ts @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2021-2025 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { startWebhookReceiverSession } from '../../webhookReceiver/client'; +import { verifyModicaMessageExchange } from '../../modica'; +import { verifyMessageExchange } from '../../verify'; + +const HELPLINE_CODE = 'NZ'; +const TEST_TIMEOUT_MILLISECONDS = 5 * 60 * 1000; + +let webhookReceiverSession: ReturnType; +let verifyExchange: ReturnType; + +beforeEach(async () => { + webhookReceiverSession = startWebhookReceiverSession(HELPLINE_CODE); + verifyExchange = verifyModicaMessageExchange(webhookReceiverSession, HELPLINE_CODE); + jest.setTimeout(TEST_TIMEOUT_MILLISECONDS); +}); + +afterEach(async () => { + await webhookReceiverSession.end(); +}); + +test('NZ/staging modica custom channel chatbot integration test', async () => { + await verifyExchange([ + { + sender: 'service-user', + text: `Hello from integration test ${webhookReceiverSession.sessionId}`, + }, + { + sender: 'flex', + text: `Kia ora, you've reached Youthline.`, + }, + { + sender: 'flex', + text: `Your conversation is confidential, but if we feel that you or someone else is at serious risk of harm, we may have to link in with other services. We will let you know if that becomes necessary. \nDo you need urgent support? \n1. Yes \n2. No`, + }, + { + sender: 'service-user', + text: `2`, + }, + { + sender: 'flex', + text: `We'll connect you with someone soon. Your conversation will be recorded and may be monitored for quality purposes. For more information, here is a link to our privacy statement: https://youthline.co.nz/privacy-statements/`, + }, + ]); +}); diff --git a/lambdas/integrationTestRunner/src/modica.ts b/lambdas/integrationTestRunner/src/modica.ts new file mode 100644 index 0000000000..aea6e69e03 --- /dev/null +++ b/lambdas/integrationTestRunner/src/modica.ts @@ -0,0 +1,106 @@ +/** + * Copyright (C) 2021-2025 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { WebhookReceiverSession } from './webhookReceiver/client'; +import { getSsmParameter } from '@tech-matters/ssm-cache'; +import { verifyMessageExchange } from './verify'; + +const { NODE_ENV, HRM_URL } = process.env; + +/** + * Derives a unique test phone number from a session ID. + * The number starts with '111' to comply with NZ staging's allowed test number regex: + * modica:111\d{1,20} + * Session IDs include a timestamp (e.g. 2024-01-01T12:00:00.000Z) which always + * provides sufficient digits after filtering. + */ +const deriveSenderNumber = (sessionId: string): string => { + const digits = sessionId.replace(/[^0-9]/g, '').slice(0, 15); + if (digits.length === 0) { + throw new Error( + `Cannot derive a valid modica test sender number from session ID '${sessionId}': no digits found`, + ); + } + return `111${digits}`; +}; + +/** + * Adds a '+' prefix if missing (mirrors Modica's sanitizeRecipientId function in flexToModica.ts) + */ +const sanitizeSenderNumber = (senderNumber: string): string => + senderNumber.charAt(0) !== '+' ? `+${senderNumber}` : senderNumber; + +export const sendModicaMessage = + (helplineCode: string, { sessionId }: WebhookReceiverSession) => + async (messageText: string) => { + console.debug(`[${sessionId}] Sending Modica message: '${messageText}'`); + const accountSidSsmKey = `/${NODE_ENV}/twilio/${helplineCode.toUpperCase()}/account_sid`; + console.debug( + `[${sessionId}] Looking up account sid via '${accountSidSsmKey}' to send: '${messageText}'`, + ); + const accountSid = await getSsmParameter(accountSidSsmKey); + const destination = await getSsmParameter( + `/${NODE_ENV}/modica/${accountSid}/app_name`, + ); + const webhookUrl = `${HRM_URL}/lambda/twilio/account-scoped/${accountSid}/customChannels/modica/modicaToFlex`; + const senderNumber = deriveSenderNumber(sessionId); + + const body = { + source: senderNumber, + destination, + content: messageText, + testSessionId: sessionId, + }; + + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + console.warn( + `[${sessionId}] Error sending Modica message to API: '${messageText}'`, + response.status, + await response.text(), + ); + } + expect(response.ok).toBe(true); + console.debug(`[${sessionId}] Successfully Sent Modica message: '${messageText}'`); + }; + +export const expectModicaMessageReceived = + ({ sessionId, expectRequestToBeReceived }: WebhookReceiverSession) => + async (messageText: string) => { + console.debug(`[${sessionId}] Expecting to receive Modica message: '${messageText}'`); + const sanitizedSender = sanitizeSenderNumber(deriveSenderNumber(sessionId)); + return expectRequestToBeReceived( + record => { + const { destination, content } = JSON.parse(record.body); + return destination === sanitizedSender && content === messageText; + }, + { timeoutMilliseconds: 30 * 1000 }, + ); + }; + +export const verifyModicaMessageExchange = ( + session: WebhookReceiverSession, + helplineCode: string, +) => + verifyMessageExchange( + sendModicaMessage(helplineCode, session), + expectModicaMessageReceived(session), + );