Skip to content
4 changes: 2 additions & 2 deletions functions/captureChannelWithBot.protected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -141,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,
Expand Down
5 changes: 3 additions & 2 deletions functions/webhooks/chatbotCallback.protected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<WebhookEvent> & {};
Expand Down Expand Up @@ -78,7 +79,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,
Expand Down Expand Up @@ -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
Expand Down
232 changes: 232 additions & 0 deletions tests/captureChannelWithBot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/**
* 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 '@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';

// eslint-disable-next-line global-require
const lexClient = require('../functions/helpers/lexClient.private') as LexClient;

jest.mock('../functions/helpers/lexClient.private', () => ({
postText: jest.fn(),
}));

const fetch = jest.fn().mockReturnValue({
attributes: JSON.stringify({
channelCapturedByBot: {
botId: 'C6HUSTIFBR',
botAliasId: 'TSTALIASID',
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 = {
getTwilioClient: jest.fn().mockImplementation(() => ({
chat: {
v2: {
services: jest.fn().mockReturnValue({
channels: jest.fn().mockReturnValue({
fetch,
}),
}),
},
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',
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: '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', () => {
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',
};
await captureChannelWithBot(mockContext, event, mockCallback);

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

test('should resolve with error message when channelSid is missing', async () => {
const event = { ...mockEvent, channelSid: undefined };

// @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({
_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(lexClient.postText).not.toHaveBeenCalled();
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(lexClient.postText).not.toHaveBeenCalled();
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(lexClient.postText).not.toHaveBeenCalled();
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,
}),
);
});
});
Loading