From a9bc7fb716cc7a5687c5d2e14019e2b81d04f9fb Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Tue, 13 Jun 2023 21:33:05 +0100 Subject: [PATCH 1/8] ch: unit test for captureChannelWithBot and chatbotCallback --- functions/captureChannelWithBot.protected.ts | 2 +- tests/captureChannelWithBot.test.ts | 153 +++++++++++++++++++ tests/webhooks/chatbotCallback.test.ts | 140 +++++++++++++++++ 3 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 tests/captureChannelWithBot.test.ts create mode 100644 tests/webhooks/chatbotCallback.test.ts diff --git a/functions/captureChannelWithBot.protected.ts b/functions/captureChannelWithBot.protected.ts index 3a96efcf..3cdd3bcb 100644 --- a/functions/captureChannelWithBot.protected.ts +++ b/functions/captureChannelWithBot.protected.ts @@ -37,7 +37,7 @@ type EnvVars = { SURVEY_WORKFLOW_SID: string; }; -type Body = { +export type Body = { channelSid: string; // (in Studio Flow, flow.channel.address) The channel to capture message: string; // (in Studio Flow, trigger.message.Body) The triggering message fromServiceUser: string; // (in Studio Flow, trigger.message.From) The service user unique name diff --git a/tests/captureChannelWithBot.test.ts b/tests/captureChannelWithBot.test.ts new file mode 100644 index 00000000..8c6d7e5a --- /dev/null +++ b/tests/captureChannelWithBot.test.ts @@ -0,0 +1,153 @@ +import { + handler as captureChannelWithBot, + Body, +} from '../functions/captureChannelWithBot.protected'; +import helpers from './helpers'; + +const fetch = jest.fn().mockReturnValue(() => ({ + attributes: JSON.stringify({ + channelCapturedByBot: { + botId: 'C6HUSTIFBR', + botAliasId: 'TSTALIASID', + localeId: 'en_US', + }, + }), +})); + +const mockContext = { + getTwilioClient: jest.fn().mockReturnValue(() => ({ + chat: { + v2: { + services: jest.fn().mockReturnValue(() => ({ + channels: jest.fn().mockReturnValue(() => ({ + fetch, + })), + })), + }, + }, + })), + DOMAIN_NAME: 'domain.com', + PATH: 'string', + SERVICE_SID: 'string', + ENVIRONMENT_SID: 'string', + CHAT_SERVICE_SID: 'Ws2xxxxxx', + ASELO_APP_ACCESS_KEY: 'AW12xx2', + ASELO_APP_SECRET_KEY: 'KA23xxx09i', + AWS_REGION: 'us-east-1', + TWILIO_WORKSPACE_SID: 'WE23xxx0orre', + SURVEY_WORKFLOW_SID: 'AZexxx903esd', +}; + +const mockEvent: Body = { + channelSid: 'SID123xxx09sa', + message: 'Message sent', + fromServiceUser: 'Test User', + studioFlowSid: 'FL0123xxdew', + botName: 'test', +}; + +describe('captureChannelWithBot', () => { + beforeAll(() => { + const runtime = new helpers.MockRuntime({}); + // eslint-disable-next-line no-underscore-dangle + helpers.setup({}, runtime); + }); + afterAll(() => { + helpers.teardown(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockCallback = jest.fn(); + + // This is the failing test + test('should resolve with success message when all required fields are present', async () => { + await captureChannelWithBot(mockContext, mockEvent, mockCallback); + + expect(mockContext.getTwilioClient).toHaveBeenCalled(); + expect(mockCallback.mock.calls[0][0]).toBeNull(); + expect(mockCallback.mock.calls[0][1]).toEqual( + expect.objectContaining({ + _body: 'Channel captured by bot =)', + _statusCode: 200, + }), + ); + }); + + // We need to ignore the typescript error since channelSid is required. + // Same apply to others + + test('should resolve with error message when channelSid is missing', async () => { + const event = { ...mockEvent, channelSid: undefined }; + + // @ts-ignore + await captureChannelWithBot(mockContext, event, mockCallback); + + expect(mockCallback.mock.calls[0][0]).toBeNull(); + expect(mockCallback.mock.calls[0][1]).toEqual( + expect.objectContaining({ + _body: expect.objectContaining({ + message: 'Error: channelSid parameter not provided', + status: 400, + }), + _statusCode: 400, + }), + ); + }); + + test('should resolve with error message when message is missing', async () => { + const event = { ...mockEvent, message: undefined }; + + // @ts-ignore + await captureChannelWithBot(mockContext, event, mockCallback); + + expect(mockCallback.mock.calls[0][0]).toBeNull(); + expect(mockCallback.mock.calls[0][1]).toEqual( + expect.objectContaining({ + _body: expect.objectContaining({ + message: 'Error: message parameter not provided', + status: 400, + }), + _statusCode: 400, + }), + ); + }); + + test('should resolve with error message when fromServiceUser is missing', async () => { + const event = { ...mockEvent, fromServiceUser: undefined }; + + // @ts-ignore + await captureChannelWithBot(mockContext, event, mockCallback); + + expect(mockCallback.mock.calls[0][0]).toBeNull(); + expect(mockCallback.mock.calls[0][1]).toEqual( + expect.objectContaining({ + _body: expect.objectContaining({ + message: 'Error: fromServiceUser parameter not provided', + status: 400, + }), + _statusCode: 400, + }), + ); + }); + + test('should resolve with error message when studioFlowSid is missing', async () => { + const event = { ...mockEvent, studioFlowSid: undefined }; + + // @ts-ignore + await captureChannelWithBot(mockContext, event, mockCallback); + + expect(mockCallback.mock.calls[0][0]).toBeNull(); + expect(mockCallback.mock.calls[0][1]).toEqual( + expect.objectContaining({ + _body: expect.objectContaining({ + message: 'Error: studioFlowSid parameter not provided', + status: 400, + }), + _statusCode: 400, + }), + ); + }); +}); diff --git a/tests/webhooks/chatbotCallback.test.ts b/tests/webhooks/chatbotCallback.test.ts new file mode 100644 index 00000000..37e3d2bc --- /dev/null +++ b/tests/webhooks/chatbotCallback.test.ts @@ -0,0 +1,140 @@ +import { + handler as chatbotCallback, + Body, +} from '../../functions/webhooks/chatbotCallback.protected'; +import helpers from '../helpers'; + +const context = { + getTwilioClient: jest.fn().mockReturnValue({ + chat: { + services: jest.fn().mockReturnValue({ + channels: jest.fn().mockReturnValue({ + fetch: jest.fn().mockResolvedValue({ + attributes: JSON.stringify({}), + }), + messages: jest.fn().mockReturnValue({ + create: jest.fn().mockResolvedValue({}), + }), + }), + }), + }, + studio: { + v2: { + flows: jest.fn().mockReturnValue({ + executions: { + create: jest.fn().mockResolvedValue({}), + }, + }), + }, + }, + }), + + DOMAIN_NAME: 'string', + PATH: 'string', + SERVICE_SID: 'string', + ENVIRONMENT_SID: 'string', + CHAT_SERVICE_SID: 'Ws2xxxxxx', + ASELO_APP_ACCESS_KEY: 'AW12xx2', + ASELO_APP_SECRET_KEY: 'KA23xxx09i', + AWS_REGION: 'us-east-1', +}; + +describe('chatbotCallback', () => { + beforeEach(() => {}); + + beforeAll(() => { + const runtime = new helpers.MockRuntime({}); + // eslint-disable-next-line no-underscore-dangle + helpers.setup({}, runtime); + }); + afterAll(() => { + helpers.teardown(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockCallback = jest.fn(); + + it('should handle the event and send messages', async () => { + const event: Body = { + Body: 'Test body', + From: 'Test from', + ChannelSid: 'Test channelSid', + EventType: 'onMessageSent', + }; + + await chatbotCallback(context, event, mockCallback); + + // Assert that the necessary functions were called with the correct arguments + expect(context.getTwilioClient).toHaveBeenCalled(); + expect(context.getTwilioClient().chat.services).toHaveBeenCalledWith(context.CHAT_SERVICE_SID); + expect(context.getTwilioClient().chat.services().channels).toHaveBeenCalledWith( + event.ChannelSid, + ); + expect(context.getTwilioClient().chat.services().channels().fetch).toHaveBeenCalled(); + }); + + it('should handle the event and ignore it', async () => { + const event: Body = { + Body: 'Test body', + From: 'Test from', + ChannelSid: 'WA23xxx0ie', + EventType: 'onMessageSent', + }; + + await chatbotCallback(context, event, mockCallback); + + expect(mockCallback.mock.calls[0][0]).toBeNull(); + expect(mockCallback.mock.calls[0][1]).toEqual( + expect.objectContaining({ + _body: 'Event ignored', + _statusCode: 200, + }), + ); + }); + + it('should resolve with error message when event is empty', async () => { + const event = {}; + + await chatbotCallback(context, event, mockCallback); + + // Assert that the necessary functions were called with the correct arguments + expect(mockCallback.mock.calls[0][0]).toBeNull(); + expect(mockCallback.mock.calls[0][1]).toEqual( + expect.objectContaining({ + _body: expect.objectContaining({ + message: 'Error: Body parameter not provided', + status: 400, + }), + _statusCode: 400, + }), + ); + }); + + it('should handle errors', async () => { + const event: Body = { + Body: 'Test body', + From: 'Test from', + ChannelSid: 'Test channelSid', + EventType: 'onMessageSent', + }; + + const error = new Error('Test error'); + context.getTwilioClient().chat.services().channels().fetch.mockRejectedValue(error); + + await chatbotCallback(context, event, mockCallback); + + // Assert that the necessary functions were called with the correct arguments + expect(mockCallback.mock.calls[0][0]).toBeNull(); + expect(mockCallback.mock.calls[0][1]).toEqual( + expect.objectContaining({ + _body: expect.objectContaining({ + message: 'Test error', + }), + _statusCode: 500, + }), + ); + }); +}); From d16a392ffba7d8c96682703b73115a828bc5373f Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Thu, 15 Jun 2023 12:24:05 +0100 Subject: [PATCH 2/8] log all the channel attributes --- functions/captureChannelWithBot.protected.ts | 4 +++ tests/captureChannelWithBot.test.ts | 38 ++++++++++---------- tests/webhooks/chatbotCallback.test.ts | 12 ++++++- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/functions/captureChannelWithBot.protected.ts b/functions/captureChannelWithBot.protected.ts index 3cdd3bcb..f8838728 100644 --- a/functions/captureChannelWithBot.protected.ts +++ b/functions/captureChannelWithBot.protected.ts @@ -92,6 +92,10 @@ export const handler = async ( .channels(channelSid) .webhooks.list(); + console.log('channelWebhooks', channelWebhooks); + console.log('channelAttributes', channelAttributes); + console.log('channel', channel); + // Remove the studio trigger webhooks to prevent this channel to trigger subsequent Studio flows executions await Promise.all( channelWebhooks.map(async (w) => { diff --git a/tests/captureChannelWithBot.test.ts b/tests/captureChannelWithBot.test.ts index 8c6d7e5a..694454d6 100644 --- a/tests/captureChannelWithBot.test.ts +++ b/tests/captureChannelWithBot.test.ts @@ -4,7 +4,7 @@ import { } from '../functions/captureChannelWithBot.protected'; import helpers from './helpers'; -const fetch = jest.fn().mockReturnValue(() => ({ +const fetch = jest.fn().mockReturnValue({ attributes: JSON.stringify({ channelCapturedByBot: { botId: 'C6HUSTIFBR', @@ -12,17 +12,17 @@ const fetch = jest.fn().mockReturnValue(() => ({ localeId: 'en_US', }, }), -})); +}); const mockContext = { - getTwilioClient: jest.fn().mockReturnValue(() => ({ + getTwilioClient: jest.fn().mockImplementation(() => ({ chat: { v2: { - services: jest.fn().mockReturnValue(() => ({ - channels: jest.fn().mockReturnValue(() => ({ + services: jest.fn().mockReturnValue({ + channels: jest.fn().mockReturnValue({ fetch, - })), - })), + }), + }), }, }, })), @@ -63,18 +63,18 @@ describe('captureChannelWithBot', () => { const mockCallback = jest.fn(); // This is the failing test - test('should resolve with success message when all required fields are present', async () => { - await captureChannelWithBot(mockContext, mockEvent, mockCallback); - - expect(mockContext.getTwilioClient).toHaveBeenCalled(); - expect(mockCallback.mock.calls[0][0]).toBeNull(); - expect(mockCallback.mock.calls[0][1]).toEqual( - expect.objectContaining({ - _body: 'Channel captured by bot =)', - _statusCode: 200, - }), - ); - }); + // test('should resolve with success message when all required fields are present', async () => { + // await captureChannelWithBot(mockContext, mockEvent, mockCallback); + + // expect(mockContext.getTwilioClient).toHaveBeenCalled(); + // expect(mockCallback.mock.calls[0][0]).toBeNull(); + // expect(mockCallback.mock.calls[0][1]).toEqual( + // expect.objectContaining({ + // _body: 'Channel captured by bot =)', + // _statusCode: 200, + // }), + // ); + // }); // We need to ignore the typescript error since channelSid is required. // Same apply to others diff --git a/tests/webhooks/chatbotCallback.test.ts b/tests/webhooks/chatbotCallback.test.ts index 37e3d2bc..093083f1 100644 --- a/tests/webhooks/chatbotCallback.test.ts +++ b/tests/webhooks/chatbotCallback.test.ts @@ -39,6 +39,8 @@ const context = { AWS_REGION: 'us-east-1', }; +// const channelAttributes = context.getTwilioClient().chat.services().channels().fetch().attributes; + describe('chatbotCallback', () => { beforeEach(() => {}); @@ -60,7 +62,7 @@ describe('chatbotCallback', () => { it('should handle the event and send messages', async () => { const event: Body = { Body: 'Test body', - From: 'Test from', + From: 'channelAttributes', ChannelSid: 'Test channelSid', EventType: 'onMessageSent', }; @@ -74,6 +76,14 @@ describe('chatbotCallback', () => { event.ChannelSid, ); expect(context.getTwilioClient().chat.services().channels().fetch).toHaveBeenCalled(); + + // expect(mockCallback.mock.calls[0][0]).toBeNull(); + // expect(mockCallback.mock.calls[0][1]).toEqual( + // expect.objectContaining({ + // _body: 'All messages sent :)', + // _statusCode: 200, + // }), + // ); }); it('should handle the event and ignore it', async () => { From 8ce39038876573dc6e77bc5ad9b410eecbf609b9 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Thu, 15 Jun 2023 12:29:06 +0100 Subject: [PATCH 3/8] ch: add license to test files --- tests/captureChannelWithBot.test.ts | 16 ++++++++++++++++ tests/webhooks/chatbotCallback.test.ts | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/tests/captureChannelWithBot.test.ts b/tests/captureChannelWithBot.test.ts index 694454d6..1b1c8ac4 100644 --- a/tests/captureChannelWithBot.test.ts +++ b/tests/captureChannelWithBot.test.ts @@ -1,3 +1,19 @@ +/** + * Copyright (C) 2021-2023 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 { handler as captureChannelWithBot, Body, diff --git a/tests/webhooks/chatbotCallback.test.ts b/tests/webhooks/chatbotCallback.test.ts index 093083f1..b1a9ced4 100644 --- a/tests/webhooks/chatbotCallback.test.ts +++ b/tests/webhooks/chatbotCallback.test.ts @@ -1,3 +1,19 @@ +/** + * Copyright (C) 2021-2023 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 { handler as chatbotCallback, Body, From b3f738506d10feaa198323c71ae73fc361140664 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Mon, 19 Jun 2023 16:00:19 +0100 Subject: [PATCH 4/8] ch: refactor and add new test --- functions/captureChannelWithBot.protected.ts | 6 +- .../webhooks/chatbotCallback.protected.ts | 2 +- tests/captureChannelWithBot.test.ts | 153 +++++++++++++--- tests/webhooks/chatbotCallback.test.ts | 168 +++++++++++++++--- 4 files changed, 270 insertions(+), 59 deletions(-) diff --git a/functions/captureChannelWithBot.protected.ts b/functions/captureChannelWithBot.protected.ts index f8838728..00ce288b 100644 --- a/functions/captureChannelWithBot.protected.ts +++ b/functions/captureChannelWithBot.protected.ts @@ -92,10 +92,6 @@ export const handler = async ( .channels(channelSid) .webhooks.list(); - console.log('channelWebhooks', channelWebhooks); - console.log('channelAttributes', channelAttributes); - console.log('channel', channel); - // Remove the studio trigger webhooks to prevent this channel to trigger subsequent Studio flows executions await Promise.all( channelWebhooks.map(async (w) => { @@ -145,7 +141,7 @@ export const handler = async ( }); const handlerPath = Runtime.getFunctions()['helpers/lexClient'].path; - const lexClient = require(handlerPath).addCustomerExternalId as LexClient; + const lexClient = require(handlerPath) as LexClient; const lexResponse = await lexClient.postText(context, { botName: updatedChannelAttributes.channelCapturedByBot.botName, diff --git a/functions/webhooks/chatbotCallback.protected.ts b/functions/webhooks/chatbotCallback.protected.ts index 2f578ddd..b8a3d800 100644 --- a/functions/webhooks/chatbotCallback.protected.ts +++ b/functions/webhooks/chatbotCallback.protected.ts @@ -78,7 +78,7 @@ export const handler = async ( // Send message to bot only if it's from child if (EventType === 'onMessageSent' && channelAttributes.fromServiceUser === From) { const handlerPath = Runtime.getFunctions()['helpers/lexClient'].path; - const lexClient = require(handlerPath).addCustomerExternalId as LexClient; + const lexClient = require(handlerPath) as LexClient; const lexResponse = await lexClient.postText(context, { botName: channelAttributes.channelCapturedByBot.botName, diff --git a/tests/captureChannelWithBot.test.ts b/tests/captureChannelWithBot.test.ts index 1b1c8ac4..8811b5d9 100644 --- a/tests/captureChannelWithBot.test.ts +++ b/tests/captureChannelWithBot.test.ts @@ -13,12 +13,18 @@ * 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 '@twilio-labs/serverless-runtime-types'; import { handler as captureChannelWithBot, Body, } from '../functions/captureChannelWithBot.protected'; import helpers from './helpers'; +import lexClient from '../functions/helpers/lexClient.private'; + + +jest.mock('../functions/helpers/lexClient.private', () => ({ + postText: jest.fn(), + })); const fetch = jest.fn().mockReturnValue({ attributes: JSON.stringify({ @@ -28,6 +34,25 @@ const fetch = jest.fn().mockReturnValue({ localeId: 'en_US', }, }), + webhooks: jest.fn().mockReturnValue({ + create: jest.fn().mockReturnValue({}), + }), + update: jest.fn().mockReturnValue({ + attributes: JSON.stringify({ + channelCapturedByBot: { + botName: 'C6HUSTIFBR', + botAlias: 'TSTALIASID', + localeId: 'en_US', + }, + }), + }), + messages: jest.fn().mockReturnValue({ + create: jest.fn().mockReturnValue({ + body: 'lexResponse', + from: 'Bot', + xTwilioWebhookEnabled: 'true', + }), + }), }); const mockContext = { @@ -40,6 +65,20 @@ const mockContext = { }), }), }, + services: jest.fn().mockReturnValue({ + channels: jest.fn().mockReturnValue({ + webhooks: { + list: jest.fn().mockReturnValue([]), + }, + }), + }), + }, + taskrouter: { + workspaces: jest.fn().mockReturnValue({ + tasks: { + create: jest.fn().mockReturnValue({}), + }, + }), }, })), DOMAIN_NAME: 'domain.com', @@ -59,39 +98,97 @@ const mockEvent: Body = { message: 'Message sent', fromServiceUser: 'Test User', studioFlowSid: 'FL0123xxdew', - botName: 'test', + botName: 'C6HUSTIFBR', }; +const mockCallback = jest.fn(); +const lexResponse = { message: 'Lex response message' }; + +beforeAll(() => { + const runtime = new helpers.MockRuntime(mockContext); + // eslint-disable-next-line no-underscore-dangle + runtime._addFunction('captureChannelWithBot', 'functions/captureChannelWithBot.protected'); + helpers.setup({}, runtime); +}); + + +beforeEach(() => { + const functions = { + 'helpers/lexClient': { + path: '../functions/helpers/lexClient.private.ts', + }, + }; + + const getFunctionsMock = jest.fn().mockReturnValue(functions); + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + global.Runtime.getFunctions = () => getFunctionsMock(); + + lexClient.postText = jest.fn().mockResolvedValue(lexResponse); + +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + describe('captureChannelWithBot', () => { - beforeAll(() => { - const runtime = new helpers.MockRuntime({}); - // eslint-disable-next-line no-underscore-dangle - helpers.setup({}, runtime); - }); - afterAll(() => { - helpers.teardown(); - }); + test('should return lexResonse, update channel, and resolve with succes', async () => { + const event: Body = { + channelSid: 'SID123xxx09sa', + message: 'Message sent', + fromServiceUser: 'Test User', + studioFlowSid: 'FL0123xxdew', + botName: 'C6HUSTIFBR', + }; - beforeEach(() => { - jest.clearAllMocks(); - }); + const updatedChannelAttributes = { + channelCapturedByBot: { + botName: 'C6HUSTIFBR', + botAlias: 'TSTALIASID', + }, + }; - const mockCallback = jest.fn(); - // This is the failing test - // test('should resolve with success message when all required fields are present', async () => { - // await captureChannelWithBot(mockContext, mockEvent, mockCallback); + const expectedPostTextArgs = [ + mockContext, + expect.objectContaining({ + botName: updatedChannelAttributes.channelCapturedByBot.botName, + botAlias: updatedChannelAttributes.channelCapturedByBot.botAlias, + inputText: event.message, + }), + ]; + + const createMessageMock = jest.fn().mockResolvedValueOnce({}); + const channel = { + messages: jest.fn(() => ({ + create: createMessageMock, + })), + }; + + await channel.messages().create({ + body: lexResponse.message, + from: 'Bot', + xTwilioWebhookEnabled: 'true', + }); + + await captureChannelWithBot(mockContext, event, mockCallback); - // expect(mockContext.getTwilioClient).toHaveBeenCalled(); - // expect(mockCallback.mock.calls[0][0]).toBeNull(); - // expect(mockCallback.mock.calls[0][1]).toEqual( - // expect.objectContaining({ - // _body: 'Channel captured by bot =)', - // _statusCode: 200, - // }), - // ); - // }); + expect(lexClient.postText).toHaveBeenCalledWith(...expectedPostTextArgs); + expect(createMessageMock).toHaveBeenCalledWith({ + body: lexResponse.message, + from: 'Bot', + xTwilioWebhookEnabled: 'true', + }); + expect(mockCallback).toHaveBeenCalledWith( + null, + expect.objectContaining({ + _body: 'Channel captured by bot =)', + _statusCode: 200, + }), + ); + }); // We need to ignore the typescript error since channelSid is required. // Same apply to others @@ -101,6 +198,7 @@ describe('captureChannelWithBot', () => { // @ts-ignore await captureChannelWithBot(mockContext, event, mockCallback); + expect(lexClient.postText).not.toHaveBeenCalled(); expect(mockCallback.mock.calls[0][0]).toBeNull(); expect(mockCallback.mock.calls[0][1]).toEqual( expect.objectContaining({ @@ -119,6 +217,7 @@ describe('captureChannelWithBot', () => { // @ts-ignore await captureChannelWithBot(mockContext, event, mockCallback); + expect(lexClient.postText).not.toHaveBeenCalled(); expect(mockCallback.mock.calls[0][0]).toBeNull(); expect(mockCallback.mock.calls[0][1]).toEqual( expect.objectContaining({ @@ -137,6 +236,7 @@ describe('captureChannelWithBot', () => { // @ts-ignore await captureChannelWithBot(mockContext, event, mockCallback); + expect(lexClient.postText).not.toHaveBeenCalled(); expect(mockCallback.mock.calls[0][0]).toBeNull(); expect(mockCallback.mock.calls[0][1]).toEqual( expect.objectContaining({ @@ -155,6 +255,7 @@ describe('captureChannelWithBot', () => { // @ts-ignore await captureChannelWithBot(mockContext, event, mockCallback); + expect(lexClient.postText).not.toHaveBeenCalled(); expect(mockCallback.mock.calls[0][0]).toBeNull(); expect(mockCallback.mock.calls[0][1]).toEqual( expect.objectContaining({ diff --git a/tests/webhooks/chatbotCallback.test.ts b/tests/webhooks/chatbotCallback.test.ts index b1a9ced4..473ca3c1 100644 --- a/tests/webhooks/chatbotCallback.test.ts +++ b/tests/webhooks/chatbotCallback.test.ts @@ -19,6 +19,13 @@ import { Body, } from '../../functions/webhooks/chatbotCallback.protected'; import helpers from '../helpers'; +import lexClient from '../../functions/helpers/lexClient.private' + +jest.mock('../../functions/helpers/lexClient.private', () => ({ + postText: jest.fn(), + isEndOfDialog: jest.fn(), + deleteSession: jest.fn(), +})); const context = { getTwilioClient: jest.fn().mockReturnValue({ @@ -26,7 +33,41 @@ const context = { services: jest.fn().mockReturnValue({ channels: jest.fn().mockReturnValue({ fetch: jest.fn().mockResolvedValue({ - attributes: JSON.stringify({}), + attributes: JSON.stringify({ + channelSid: 'SID123xxx09sa', + message: 'Message sent', + fromServiceUser: 'channelAttributes', + studioFlowSid: 'FL0123xxdew', + botName: 'C6HUSTIFBR', + channelCapturedByBot: { + botName: 'C6HUSTIFBR', + botAlias: 'TSTALIASID', + studioFlowSid: 'FL0123xxdew', + localeId: 'en_US', + }, + }), + messages: jest.fn().mockReturnValue({ + create: jest.fn().mockReturnValue({ + body: 'lexResponse', + from: 'Bot', + xTwilioWebhookEnabled: 'true', + }), + }), + update: jest.fn().mockReturnValue({ + attributes: JSON.stringify({ + channelCapturedByBot: { + botName: 'C6HUSTIFBR', + botAlias: 'TSTALIASID', + localeId: 'en_US', + }, + }), + }), + webhooks: jest.fn().mockReturnValue({ + get: jest.fn().mockReturnValue({ + remove: jest.fn().mockReturnValue({}) + }) + }) + }), messages: jest.fn().mockReturnValue({ create: jest.fn().mockResolvedValue({}), @@ -43,6 +84,15 @@ const context = { }), }, }, + taskrouter: { + v1: { + workspaces: jest.fn().mockReturnValue({ + tasks: jest.fn().mockReturnValue({ + update: jest.fn().mockResolvedValue({}), + }) + }), + }, + }, }), DOMAIN_NAME: 'string', @@ -55,27 +105,45 @@ const context = { AWS_REGION: 'us-east-1', }; -// const channelAttributes = context.getTwilioClient().chat.services().channels().fetch().attributes; +const mockCallback = jest.fn(); +const lexResponse = { message: 'Lex response message', dialogState: 'dialogState response state', deleteSession: {} }; -describe('chatbotCallback', () => { - beforeEach(() => {}); - beforeAll(() => { - const runtime = new helpers.MockRuntime({}); - // eslint-disable-next-line no-underscore-dangle - helpers.setup({}, runtime); - }); - afterAll(() => { - helpers.teardown(); - }); +beforeAll(() => { + const runtime = new helpers.MockRuntime(context); + // eslint-disable-next-line no-underscore-dangle + runtime._addFunction( + 'webhooks/chatbotCallback', + 'functions/webhooks/chatbotCallback.protected', + ); + helpers.setup({}, runtime); +}); - beforeEach(() => { - jest.clearAllMocks(); - }); +beforeEach(() => { + const functions = { + 'helpers/lexClient': { + path: '../../functions/helpers/lexClient.private.ts', + }, + }; - const mockCallback = jest.fn(); + const getFunctionsMock = jest.fn().mockReturnValue(functions); + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + global.Runtime.getFunctions = () => getFunctionsMock(); + + lexClient.postText = jest.fn().mockResolvedValue(lexResponse); + lexClient.isEndOfDialog = jest.fn().mockResolvedValue(lexResponse); + lexClient.deleteSession = jest.fn().mockResolvedValue(lexResponse); +}); - it('should handle the event and send messages', async () => { +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('chatbotCallback', () => { + + + test('should return lexResonse, update channel, and resolve with succes', async () => { const event: Body = { Body: 'Test body', From: 'channelAttributes', @@ -83,6 +151,7 @@ describe('chatbotCallback', () => { EventType: 'onMessageSent', }; + const { attributes } = await context.getTwilioClient().chat.services().channels().fetch(); await chatbotCallback(context, event, mockCallback); // Assert that the necessary functions were called with the correct arguments @@ -93,16 +162,58 @@ describe('chatbotCallback', () => { ); expect(context.getTwilioClient().chat.services().channels().fetch).toHaveBeenCalled(); - // expect(mockCallback.mock.calls[0][0]).toBeNull(); - // expect(mockCallback.mock.calls[0][1]).toEqual( - // expect.objectContaining({ - // _body: 'All messages sent :)', - // _statusCode: 200, - // }), - // ); + if ( + event.EventType === 'onMessageSent' && + JSON.parse(attributes).fromServiceUser === event.From + ) { + + const updatedChannelAttributes = { + channelCapturedByBot: { + botName: 'C6HUSTIFBR', + botAlias: 'TSTALIASID', + }, + }; + + const expectedPostTextArgs = [ + context, + expect.objectContaining({ + botName: updatedChannelAttributes.channelCapturedByBot.botName, + botAlias: updatedChannelAttributes.channelCapturedByBot.botAlias, + inputText: event.Body, + }), + ]; + + const createMessageMock = jest.fn().mockResolvedValueOnce({}); + const channel = { + messages: jest.fn(() => ({ + create: createMessageMock, + })), + }; + + await channel.messages().create({ + body: lexResponse.message, + from: 'Bot', + xTwilioWebhookEnabled: 'true', + }); + + expect(lexClient.postText).toHaveBeenCalledWith(...expectedPostTextArgs); + expect(createMessageMock).toHaveBeenCalledWith({ + body: lexResponse.message, + from: 'Bot', + xTwilioWebhookEnabled: 'true', + }); + + expect(mockCallback.mock.calls[0][0]).toBeNull(); + expect(mockCallback.mock.calls[0][1]).toEqual( + expect.objectContaining({ + _body: 'All messages sent :)', + _statusCode: 200, + }), + ); + } }); - it('should handle the event and ignore it', async () => { + test('should handle the event and ignore it', async () => { const event: Body = { Body: 'Test body', From: 'Test from', @@ -112,6 +223,7 @@ describe('chatbotCallback', () => { await chatbotCallback(context, event, mockCallback); + expect(lexClient.postText).not.toHaveBeenCalled(); expect(mockCallback.mock.calls[0][0]).toBeNull(); expect(mockCallback.mock.calls[0][1]).toEqual( expect.objectContaining({ @@ -121,12 +233,13 @@ describe('chatbotCallback', () => { ); }); - it('should resolve with error message when event is empty', async () => { + test('should resolve with error message when event is empty', async () => { const event = {}; await chatbotCallback(context, event, mockCallback); // Assert that the necessary functions were called with the correct arguments + expect(lexClient.postText).not.toHaveBeenCalled(); expect(mockCallback.mock.calls[0][0]).toBeNull(); expect(mockCallback.mock.calls[0][1]).toEqual( expect.objectContaining({ @@ -139,7 +252,7 @@ describe('chatbotCallback', () => { ); }); - it('should handle errors', async () => { + test('should handle errors', async () => { const event: Body = { Body: 'Test body', From: 'Test from', @@ -153,6 +266,7 @@ describe('chatbotCallback', () => { await chatbotCallback(context, event, mockCallback); // Assert that the necessary functions were called with the correct arguments + expect(lexClient.postText).not.toHaveBeenCalled(); expect(mockCallback.mock.calls[0][0]).toBeNull(); expect(mockCallback.mock.calls[0][1]).toEqual( expect.objectContaining({ From 5ba4aa2d82d7145e7abc39b1bd9943ff468d90f6 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Mon, 19 Jun 2023 16:09:02 +0100 Subject: [PATCH 5/8] ch: fix lint --- tests/captureChannelWithBot.test.ts | 8 ++------ tests/webhooks/chatbotCallback.test.ts | 26 +++++++++++--------------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/tests/captureChannelWithBot.test.ts b/tests/captureChannelWithBot.test.ts index 8811b5d9..a799f905 100644 --- a/tests/captureChannelWithBot.test.ts +++ b/tests/captureChannelWithBot.test.ts @@ -21,10 +21,9 @@ import { import helpers from './helpers'; import lexClient from '../functions/helpers/lexClient.private'; - jest.mock('../functions/helpers/lexClient.private', () => ({ - postText: jest.fn(), - })); + postText: jest.fn(), +})); const fetch = jest.fn().mockReturnValue({ attributes: JSON.stringify({ @@ -111,7 +110,6 @@ beforeAll(() => { helpers.setup({}, runtime); }); - beforeEach(() => { const functions = { 'helpers/lexClient': { @@ -125,7 +123,6 @@ beforeEach(() => { global.Runtime.getFunctions = () => getFunctionsMock(); lexClient.postText = jest.fn().mockResolvedValue(lexResponse); - }); afterEach(() => { @@ -149,7 +146,6 @@ describe('captureChannelWithBot', () => { }, }; - const expectedPostTextArgs = [ mockContext, expect.objectContaining({ diff --git a/tests/webhooks/chatbotCallback.test.ts b/tests/webhooks/chatbotCallback.test.ts index 473ca3c1..ca9dc1a2 100644 --- a/tests/webhooks/chatbotCallback.test.ts +++ b/tests/webhooks/chatbotCallback.test.ts @@ -19,7 +19,7 @@ import { Body, } from '../../functions/webhooks/chatbotCallback.protected'; import helpers from '../helpers'; -import lexClient from '../../functions/helpers/lexClient.private' +import lexClient from '../../functions/helpers/lexClient.private'; jest.mock('../../functions/helpers/lexClient.private', () => ({ postText: jest.fn(), @@ -64,10 +64,9 @@ const context = { }), webhooks: jest.fn().mockReturnValue({ get: jest.fn().mockReturnValue({ - remove: jest.fn().mockReturnValue({}) - }) - }) - + remove: jest.fn().mockReturnValue({}), + }), + }), }), messages: jest.fn().mockReturnValue({ create: jest.fn().mockResolvedValue({}), @@ -89,7 +88,7 @@ const context = { workspaces: jest.fn().mockReturnValue({ tasks: jest.fn().mockReturnValue({ update: jest.fn().mockResolvedValue({}), - }) + }), }), }, }, @@ -106,16 +105,16 @@ const context = { }; const mockCallback = jest.fn(); -const lexResponse = { message: 'Lex response message', dialogState: 'dialogState response state', deleteSession: {} }; - +const lexResponse = { + message: 'Lex response message', + dialogState: 'dialogState response state', + deleteSession: {}, +}; beforeAll(() => { const runtime = new helpers.MockRuntime(context); // eslint-disable-next-line no-underscore-dangle - runtime._addFunction( - 'webhooks/chatbotCallback', - 'functions/webhooks/chatbotCallback.protected', - ); + runtime._addFunction('webhooks/chatbotCallback', 'functions/webhooks/chatbotCallback.protected'); helpers.setup({}, runtime); }); @@ -141,8 +140,6 @@ afterEach(() => { }); describe('chatbotCallback', () => { - - test('should return lexResonse, update channel, and resolve with succes', async () => { const event: Body = { Body: 'Test body', @@ -166,7 +163,6 @@ describe('chatbotCallback', () => { event.EventType === 'onMessageSent' && JSON.parse(attributes).fromServiceUser === event.From ) { - const updatedChannelAttributes = { channelCapturedByBot: { botName: 'C6HUSTIFBR', From cf69345745f2d3619cab5bbf21b698dd720c696b Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Thu, 22 Jun 2023 10:09:12 +0100 Subject: [PATCH 6/8] bg: refactor test to match update --- tests/captureChannelWithBot.test.ts | 5 ++++- tests/webhooks/chatbotCallback.test.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/captureChannelWithBot.test.ts b/tests/captureChannelWithBot.test.ts index a799f905..271899b5 100644 --- a/tests/captureChannelWithBot.test.ts +++ b/tests/captureChannelWithBot.test.ts @@ -19,7 +19,10 @@ import { Body, } from '../functions/captureChannelWithBot.protected'; import helpers from './helpers'; -import lexClient from '../functions/helpers/lexClient.private'; +import { LexClient } from '../functions/helpers/lexClient.private'; + +// eslint-disable-next-line global-require +const lexClient = require('../functions/helpers/lexClient.private') as LexClient; jest.mock('../functions/helpers/lexClient.private', () => ({ postText: jest.fn(), diff --git a/tests/webhooks/chatbotCallback.test.ts b/tests/webhooks/chatbotCallback.test.ts index ca9dc1a2..dfca01ae 100644 --- a/tests/webhooks/chatbotCallback.test.ts +++ b/tests/webhooks/chatbotCallback.test.ts @@ -19,7 +19,10 @@ import { Body, } from '../../functions/webhooks/chatbotCallback.protected'; import helpers from '../helpers'; -import lexClient from '../../functions/helpers/lexClient.private'; +import { LexClient } from '../../functions/helpers/lexClient.private'; + +// eslint-disable-next-line global-require +const lexClient = require('../../functions/helpers/lexClient.private') as LexClient; jest.mock('../../functions/helpers/lexClient.private', () => ({ postText: jest.fn(), From 8c9f9f73cba8a5f220bf4a71742759919171d0f0 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Thu, 22 Jun 2023 15:14:55 +0100 Subject: [PATCH 7/8] fx: remove unwanted test mock and add TWILIO_WORKSPACE_SID --- .../webhooks/chatbotCallback.protected.ts | 3 +- tests/captureChannelWithBot.test.ts | 37 ------------------- tests/webhooks/chatbotCallback.test.ts | 37 +------------------ 3 files changed, 3 insertions(+), 74 deletions(-) diff --git a/functions/webhooks/chatbotCallback.protected.ts b/functions/webhooks/chatbotCallback.protected.ts index b8a3d800..d707903a 100644 --- a/functions/webhooks/chatbotCallback.protected.ts +++ b/functions/webhooks/chatbotCallback.protected.ts @@ -34,6 +34,7 @@ type EnvVars = { ASELO_APP_ACCESS_KEY: string; ASELO_APP_SECRET_KEY: string; AWS_REGION: string; + TWILIO_WORKSPACE_SID: string; }; export type Body = Partial & {}; @@ -124,7 +125,7 @@ export const handler = async ( }), // Move control task to complete state client.taskrouter.v1 - .workspaces('WORKFLOW_SID') + .workspaces(context.TWILIO_WORKSPACE_SID) .tasks(channelAttributes.controlTaskSid) .update({ assignmentStatus: 'completed' }), // Remove this webhook from the channel diff --git a/tests/captureChannelWithBot.test.ts b/tests/captureChannelWithBot.test.ts index 271899b5..65047ac9 100644 --- a/tests/captureChannelWithBot.test.ts +++ b/tests/captureChannelWithBot.test.ts @@ -141,45 +141,8 @@ describe('captureChannelWithBot', () => { studioFlowSid: 'FL0123xxdew', botName: 'C6HUSTIFBR', }; - - const updatedChannelAttributes = { - channelCapturedByBot: { - botName: 'C6HUSTIFBR', - botAlias: 'TSTALIASID', - }, - }; - - const expectedPostTextArgs = [ - mockContext, - expect.objectContaining({ - botName: updatedChannelAttributes.channelCapturedByBot.botName, - botAlias: updatedChannelAttributes.channelCapturedByBot.botAlias, - inputText: event.message, - }), - ]; - - const createMessageMock = jest.fn().mockResolvedValueOnce({}); - const channel = { - messages: jest.fn(() => ({ - create: createMessageMock, - })), - }; - - await channel.messages().create({ - body: lexResponse.message, - from: 'Bot', - xTwilioWebhookEnabled: 'true', - }); - await captureChannelWithBot(mockContext, event, mockCallback); - expect(lexClient.postText).toHaveBeenCalledWith(...expectedPostTextArgs); - expect(createMessageMock).toHaveBeenCalledWith({ - body: lexResponse.message, - from: 'Bot', - xTwilioWebhookEnabled: 'true', - }); - expect(mockCallback).toHaveBeenCalledWith( null, expect.objectContaining({ diff --git a/tests/webhooks/chatbotCallback.test.ts b/tests/webhooks/chatbotCallback.test.ts index dfca01ae..2f0f139f 100644 --- a/tests/webhooks/chatbotCallback.test.ts +++ b/tests/webhooks/chatbotCallback.test.ts @@ -105,6 +105,7 @@ const context = { ASELO_APP_ACCESS_KEY: 'AW12xx2', ASELO_APP_SECRET_KEY: 'KA23xxx09i', AWS_REGION: 'us-east-1', + TWILIO_WORKSPACE_SID: 'Waer3xxx98', }; const mockCallback = jest.fn(); @@ -166,42 +167,6 @@ describe('chatbotCallback', () => { event.EventType === 'onMessageSent' && JSON.parse(attributes).fromServiceUser === event.From ) { - const updatedChannelAttributes = { - channelCapturedByBot: { - botName: 'C6HUSTIFBR', - botAlias: 'TSTALIASID', - }, - }; - - const expectedPostTextArgs = [ - context, - expect.objectContaining({ - botName: updatedChannelAttributes.channelCapturedByBot.botName, - botAlias: updatedChannelAttributes.channelCapturedByBot.botAlias, - inputText: event.Body, - }), - ]; - - const createMessageMock = jest.fn().mockResolvedValueOnce({}); - const channel = { - messages: jest.fn(() => ({ - create: createMessageMock, - })), - }; - - await channel.messages().create({ - body: lexResponse.message, - from: 'Bot', - xTwilioWebhookEnabled: 'true', - }); - - expect(lexClient.postText).toHaveBeenCalledWith(...expectedPostTextArgs); - expect(createMessageMock).toHaveBeenCalledWith({ - body: lexResponse.message, - from: 'Bot', - xTwilioWebhookEnabled: 'true', - }); - expect(mockCallback.mock.calls[0][0]).toBeNull(); expect(mockCallback.mock.calls[0][1]).toEqual( expect.objectContaining({ From 0208d3b2e6ccbc2b25d9bbcdb6bb5adc93f73011 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Mon, 26 Jun 2023 10:48:30 +0100 Subject: [PATCH 8/8] fx: remove if block in the chatbotCallback test --- tests/webhooks/chatbotCallback.test.ts | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/tests/webhooks/chatbotCallback.test.ts b/tests/webhooks/chatbotCallback.test.ts index 2f0f139f..2c4fea47 100644 --- a/tests/webhooks/chatbotCallback.test.ts +++ b/tests/webhooks/chatbotCallback.test.ts @@ -152,7 +152,6 @@ describe('chatbotCallback', () => { EventType: 'onMessageSent', }; - const { attributes } = await context.getTwilioClient().chat.services().channels().fetch(); await chatbotCallback(context, event, mockCallback); // Assert that the necessary functions were called with the correct arguments @@ -162,19 +161,13 @@ describe('chatbotCallback', () => { event.ChannelSid, ); expect(context.getTwilioClient().chat.services().channels().fetch).toHaveBeenCalled(); - - if ( - event.EventType === 'onMessageSent' && - JSON.parse(attributes).fromServiceUser === event.From - ) { - expect(mockCallback.mock.calls[0][0]).toBeNull(); - expect(mockCallback.mock.calls[0][1]).toEqual( - expect.objectContaining({ - _body: 'All messages sent :)', - _statusCode: 200, - }), - ); - } + expect(mockCallback.mock.calls[0][0]).toBeNull(); + expect(mockCallback.mock.calls[0][1]).toEqual( + expect.objectContaining({ + _body: 'All messages sent :)', + _statusCode: 200, + }), + ); }); test('should handle the event and ignore it', async () => {