diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f82deb..2b23201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.8.0 + +### New Features + +- Added support for Twilio Conversations. When an application requests a token from the `/token` endpoint, a `create_conversation=true` parameter can now be specified. When this parameter is present, the `/token` endpoint will create a conversation that is associated with the room and add the participant to it. This allows video application to use [Twilio Conversations](https://www.twilio.com/conversations-api) for in-room chat. + +### Maintenence + +- Upgraded @twilio/cli-core from 5.15.1 to 5.17.0 +- Upgraded lodash from 4.17.20 to 4.17.21 + ## 0.7.1 ### Maintenence diff --git a/LICENSE b/LICENSE index 460cdf8..45782f7 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2020 Twilio Inc. + Copyright 2021 Twilio Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 50710df..6e7a6e1 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Each request is verified using a passcode generated at deploy time. Passcodes re ### Token -Returns a Programmable Video Access token. +This endpoint returns a Programmable Video Access token. When `create_room` is true, it will create a room, and when `create_conversation` is true, it will create a [Twilio Conversation](https://www.twilio.com/docs/conversations/api/conversation-resource) associated with the room. This token is used by the above mentioned Video Apps to connect to a video room and a conversation. ```shell POST /token @@ -90,12 +90,13 @@ POST /token #### Parameters -| Name | Type | Description | -| --------------- | --------- | -------------------------------------------------------------------------------------- | -| `passcode` | `string` | **Required**. The application passcode. | -| `user_identity` | `string` | **Required**. The user's identity. | -| `room_name` | `string` | A room name that will be used to create a token scoped to connecting to only one room. | -| `create_room` | `boolean` | (default: `true`) When false, a room will not be created when a token is requested. | +| Name | Type | Description | +| --------------------- | --------- | -------------------------------------------------------------------------------------- | +| `passcode` | `string` | **Required**. The application passcode. | +| `user_identity` | `string` | **Required**. The user's identity. | +| `room_name` | `string` | **Required when `create_room` is `true`** A room name that will be used to create a token scoped to connecting to only one room. | +| `create_room` | `boolean` | (default: `true`) When false, a room will not be created when a token is requested. | +| `create_conversation` | `boolean` | (default: `false`) When true, a [Twilio Conversation](https://www.twilio.com/docs/conversations/api/conversation-resource) will be created (if it doesn't already exist) and a participant will be added to it when a token is requested. `create_room` must also be `true`. | #### Success Responses diff --git a/package-lock.json b/package-lock.json index 2849c77..686abf7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@twilio-labs/plugin-rtc", - "version": "0.7.2", + "version": "0.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index ce92d54..2e40244 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@twilio-labs/plugin-rtc", - "version": "0.7.2", + "version": "0.8.0", "description": "A Twilio-CLI plugin for real-time communication apps", "main": "index.js", "publishConfig": { diff --git a/src/commands/rtc/apps/video/delete.js b/src/commands/rtc/apps/video/delete.js index c2b8eb0..75d9c35 100644 --- a/src/commands/rtc/apps/video/delete.js +++ b/src/commands/rtc/apps/video/delete.js @@ -1,13 +1,17 @@ -const { findApp } = require('../../../../helpers'); +const { findApp, findConversationsService } = require('../../../../helpers'); const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands; class DeleteCommand extends TwilioClientCommand { async run() { await super.run(); const appInfo = await findApp.call(this); + const conversatsionsServiceInfo = await findConversationsService.call(this); if (appInfo) { await this.twilioClient.serverless.services(appInfo.sid).remove(); + if (conversatsionsServiceInfo) { + await this.twilioClient.conversations.services(conversatsionsServiceInfo.sid).remove(); + } console.log('Successfully deleted app.'); } else { console.log('There is no app to delete.'); diff --git a/src/helpers.js b/src/helpers.js index d5fee66..0d01df2 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -143,6 +143,24 @@ async function displayAppInfo() { ); } +async function findConversationsService() { + const services = await this.twilioClient.conversations.services.list(); + return services.find(service => service.friendlyName.includes(APP_NAME)); +} + +async function getConversationsServiceSID() { + const exisitingConversationsService = await findConversationsService.call(this); + + if (exisitingConversationsService) { + return exisitingConversationsService.sid; + } + + const service = await this.twilioClient.conversations.services.create({ + friendlyName: `${APP_NAME}-conversations-service`, + }); + return service.sid; +} + async function deploy() { const assets = this.flags['app-directory'] ? await getAssets(this.flags['app-directory']) : []; const { functions } = await getListOfFunctionsAndAssets(__dirname, { @@ -181,6 +199,8 @@ TWILIO_API_SECRET = the secret for the API Key`); cli.action.start('deploying app'); + const conversationServiceSid = await getConversationsServiceSID.call(this); + const deployOptions = { env: { TWILIO_API_KEY_SID: this.twilioClient.username, @@ -188,6 +208,7 @@ TWILIO_API_SECRET = the secret for the API Key`); API_PASSCODE: pin, API_PASSCODE_EXPIRY: expiryTime, ROOM_TYPE: this.flags['room-type'], + CONVERSATIONS_SERVICE_SID: conversationServiceSid, }, pkgJson: { dependencies: { @@ -220,6 +241,7 @@ module.exports = { deploy, displayAppInfo, findApp, + findConversationsService, getAssets, getMiddleware, getAppInfo, diff --git a/src/serverless/functions/token.js b/src/serverless/functions/token.js index 4952811..badf27c 100644 --- a/src/serverless/functions/token.js +++ b/src/serverless/functions/token.js @@ -3,15 +3,16 @@ const AccessToken = Twilio.jwt.AccessToken; const VideoGrant = AccessToken.VideoGrant; +const ChatGrant = AccessToken.ChatGrant; const MAX_ALLOWED_SESSION_DURATION = 14400; module.exports.handler = async (context, event, callback) => { - const { ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, ROOM_TYPE } = context; + const { ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, ROOM_TYPE, CONVERSATIONS_SERVICE_SID } = context; const authHandler = require(Runtime.getAssets()['/auth-handler.js'].path); authHandler(context, event, callback); - const { user_identity, room_name, create_room = true } = event; + const { user_identity, room_name, create_room = true, create_conversation = false } = event; let response = new Twilio.Response(); response.appendHeader('Content-Type', 'application/json'); @@ -27,6 +28,17 @@ module.exports.handler = async (context, event, callback) => { return callback(null, response); } + if (typeof create_conversation !== 'boolean') { + response.setStatusCode(400); + response.setBody({ + error: { + message: 'invalid parameter', + explanation: 'A boolean value must be provided for the create_conversation parameter', + }, + }); + return callback(null, response); + } + if (!user_identity) { response.setStatusCode(400); response.setBody({ @@ -38,14 +50,29 @@ module.exports.handler = async (context, event, callback) => { return callback(null, response); } + if (!room_name && create_room) { + response.setStatusCode(400); + response.setBody({ + error: { + message: 'missing room_name', + explanation: 'The room_name parameter is missing. room_name is required when create_room is true.', + }, + }); + return callback(null, response); + } + if (create_room) { const client = context.getTwilioClient(); + let room; try { - await client.video.rooms.create({ uniqueName: room_name, type: ROOM_TYPE }); + // See if a room already exists + room = await client.video.rooms(room_name).fetch(); } catch (e) { - // Ignore 53113 error (room already exists). See: https://www.twilio.com/docs/api/errors/53113 - if (e.code !== 53113) { + try { + // If room doesn't exist, create it + room = await client.video.rooms.create({ uniqueName: room_name, type: ROOM_TYPE }); + } catch (e) { response.setStatusCode(500); response.setBody({ error: { @@ -56,14 +83,65 @@ module.exports.handler = async (context, event, callback) => { return callback(null, response); } } + + if (create_conversation) { + const conversationsClient = client.conversations.services(CONVERSATIONS_SERVICE_SID); + + try { + // See if conversation already exists + await conversationsClient.conversations(room.sid).fetch(); + } catch (e) { + try { + // If conversation doesn't exist, create it. + await conversationsClient.conversations.create({ uniqueName: room.sid }); + } catch (e) { + response.setStatusCode(500); + response.setBody({ + error: { + message: 'error creating conversation', + explanation: 'Something went wrong when creating a conversation.', + }, + }); + return callback(null, response); + } + } + + try { + // Add participant to conversation + await conversationsClient.conversations(room.sid).participants.create({ identity: user_identity }); + } catch (e) { + // Ignore "Participant already exists" error (50433) + if (e.code !== 50433) { + response.setStatusCode(500); + response.setBody({ + error: { + message: 'error creating conversation participant', + explanation: 'Something went wrong when creating a conversation participant.', + }, + }); + return callback(null, response); + } + } + } } + // Create token const token = new AccessToken(ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, { ttl: MAX_ALLOWED_SESSION_DURATION, }); + + // Add participant's identity to token token.identity = user_identity; + + // Add video grant to token const videoGrant = new VideoGrant({ room: room_name }); token.addGrant(videoGrant); + + // Add chat grant to token + const chatGrant = new ChatGrant({ serviceSid: CONVERSATIONS_SERVICE_SID }); + token.addGrant(chatGrant); + + // Return token response.setStatusCode(200); response.setBody({ token: token.toJwt(), room_type: ROOM_TYPE }); return callback(null, response); diff --git a/test/e2e/e2e.test.js b/test/e2e/e2e.test.js index 19bdf37..659adb9 100644 --- a/test/e2e/e2e.test.js +++ b/test/e2e/e2e.test.js @@ -133,20 +133,53 @@ describe('the RTC Twilio-CLI Plugin', () => { const { body } = await superagent .post(`${URL}/token`) .send({ passcode, room_name: ROOM_NAME, user_identity: 'test user' }); - expect(jwt.decode(body.token).grants).toEqual({ identity: 'test user', video: { room: ROOM_NAME } }); + expect(jwt.decode(body.token).grants).toEqual( + expect.objectContaining({ identity: 'test user', video: { room: ROOM_NAME } }) + ); expect(body.room_type).toEqual('group'); const room = await twilioClient.video.rooms(ROOM_NAME).fetch(); expect(room.type).toEqual('group'); }); + it('should return a video token with a valid Chat Grant and add the participant to the conversation', async () => { + const ROOM_NAME = nanoid(); + const { body } = await superagent + .post(`${URL}/token`) + .send({ passcode, room_name: ROOM_NAME, user_identity: 'test user', create_conversation: true }); + + const conversationServiceSid = jwt.decode(body.token).grants.chat.service_sid; + + const room = await twilioClient.video.rooms(ROOM_NAME).fetch(); + + // Find the deployed conversations service + const deployedConversationsServices = await twilioClient.conversations.services.list(); + const deployedConversationsService = deployedConversationsServices.find( + service => (service.sid = conversationServiceSid) + ); + + // Find the conversation participant + const conversationParticipants = await twilioClient.conversations + .services(deployedConversationsService.sid) + .conversations(room.sid) + .participants.list(); + const conversationParticipant = conversationParticipants.find( + participant => participant.identity === 'test user' + ); + + expect(deployedConversationsService).toBeDefined(); + expect(conversationParticipant).toBeDefined(); + }); + it('should return a video token without creating a room when the "create_room" flag is false', async () => { expect.assertions(3); const ROOM_NAME = nanoid(); const { body } = await superagent .post(`${URL}/token`) .send({ passcode, room_name: ROOM_NAME, user_identity: 'test user', create_room: false }); - expect(jwt.decode(body.token).grants).toEqual({ identity: 'test user', video: { room: ROOM_NAME } }); + expect(jwt.decode(body.token).grants).toEqual( + expect.objectContaining({ identity: 'test user', video: { room: ROOM_NAME } }) + ); expect(body.room_type).toEqual('group'); try { @@ -156,6 +189,30 @@ describe('the RTC Twilio-CLI Plugin', () => { } }); + it('should return a video token without creating a conversation when the "create_conversation" flag is false', async () => { + const ROOM_NAME = nanoid(); + const { body } = await superagent + .post(`${URL}/token`) + .send({ passcode, room_name: ROOM_NAME, user_identity: 'test user', create_conversation: false }); + + const conversationServiceSid = jwt.decode(body.token).grants.chat.service_sid; + + const room = await twilioClient.video.rooms(ROOM_NAME).fetch(); + + // Find the deployed conversations service + const deployedConversationsServices = await twilioClient.conversations.services.list(); + const deployedConversationsService = deployedConversationsServices.find( + service => (service.sid = conversationServiceSid) + ); + + const conversationPromise = twilioClient.conversations + .services(deployedConversationsService.sid) + .conversations(room.sid) + .fetch(); + + expect(conversationPromise).rejects.toEqual(expect.objectContaining({ code: 20404 })); + }); + it('should return a 401 error when an incorrect passcode is provided', () => { superagent .post(`${URL}/token`) @@ -261,7 +318,9 @@ describe('the RTC Twilio-CLI Plugin', () => { const { body } = await superagent .post(`${URL}/token`) .send({ passcode, room_name: ROOM_NAME, user_identity: 'test user' }); - expect(jwt.decode(body.token).grants).toEqual({ identity: 'test user', video: { room: ROOM_NAME } }); + expect(jwt.decode(body.token).grants).toEqual( + expect.objectContaining({ identity: 'test user', video: { room: ROOM_NAME } }) + ); expect(body.room_type).toEqual('go'); const room = await twilioClient.video.rooms(ROOM_NAME).fetch(); @@ -298,7 +357,9 @@ describe('the RTC Twilio-CLI Plugin', () => { const { body } = await superagent .post(`${testURL}/token`) .send({ passcode: updatedPasscode, room_name: 'test-room', user_identity: 'test user' }); - expect(jwt.decode(body.token).grants).toEqual({ identity: 'test user', video: { room: 'test-room' } }); + expect(jwt.decode(body.token).grants).toEqual( + expect.objectContaining({ identity: 'test user', video: { room: 'test-room' } }) + ); }); }); }); diff --git a/test/helpers/helpers.test.js b/test/helpers/helpers.test.js index 7f239d4..e6f4a6c 100644 --- a/test/helpers/helpers.test.js +++ b/test/helpers/helpers.test.js @@ -3,6 +3,7 @@ const { deploy, displayAppInfo, findApp, + findConversationsService, getAppInfo, getAssets, getPasscode, @@ -39,6 +40,9 @@ jest.mock('@twilio-labs/serverless-api/dist/utils/fs', () => ({ function getMockTwilioInstance(options) { const mockTwilioClient = { serverless: {}, + conversations: { + services: {}, + }, username: options.username, password: options.password, }; @@ -71,6 +75,20 @@ function getMockTwilioInstance(options) { }, ]); + mockTwilioClient.conversations.services.list = () => + Promise.resolve([ + { + friendlyName: options.conversationsServiceExists + ? APP_NAME + ' conversations-service' + : 'other-conversations-service', + sid: 'mockConversationsServiceSID', + }, + ]); + + mockTwilioClient.conversations.services.create = jest.fn(() => + Promise.resolve({ sid: 'newMockConversationsServiceSid' }) + ); + return mockTwilioClient; } @@ -300,6 +318,24 @@ describe('the deploy function', () => { expect(mockDeployProject.mock.calls[0][0].serviceName).toMatch(new RegExp(`${APP_NAME}-(\\d{4})`)); }); + it('should create a new conversations service when one does not already exist', async () => { + await deploy.call({ + twilioClient: getMockTwilioInstance({ username: '', password: '' }), + flags: {}, + }); + + expect(mockDeployProject.mock.calls[0][0].env.CONVERSATIONS_SERVICE_SID).toBe('newMockConversationsServiceSid'); + }); + + it('should use an existing conversations service when one already exists', async () => { + await deploy.call({ + twilioClient: getMockTwilioInstance({ username: '', password: '', conversationsServiceExists: true }), + flags: {}, + }); + + expect(mockDeployProject.mock.calls[0][0].env.CONVERSATIONS_SERVICE_SID).toBe('mockConversationsServiceSID'); + }); + it('should set ui-editable to false when the flag is false', async () => { const mockTwilioClient = getMockTwilioInstance({ username: '', password: '' }); await deploy.call({ @@ -352,4 +388,22 @@ describe('the deploy function', () => { TWILIO_API_SECRET = the secret for the API Key] `); }); + + describe('the findConversationsService function', () => { + it('should return the correct conversations service when it exists', async () => { + const service = await findConversationsService.call({ + twilioClient: getMockTwilioInstance({ conversationsServiceExists: true }), + }); + + expect(service.sid).toEqual('mockConversationsServiceSID'); + }); + + it("should return undefined when the conversations service doesn't exist", async () => { + const service = await findConversationsService.call({ + twilioClient: getMockTwilioInstance({ conversationsServiceExists: false }), + }); + + expect(service).toEqual(undefined); + }); + }); }); diff --git a/test/serverless/functions/token.test.js b/test/serverless/functions/token.test.js index 0524de1..c512294 100644 --- a/test/serverless/functions/token.test.js +++ b/test/serverless/functions/token.test.js @@ -1,12 +1,35 @@ const { handler } = require('../../../src/serverless/functions/token'); const jwt = require('jsonwebtoken'); -const { set } = require('lodash'); const callback = jest.fn(); -const mockCreateFunction = jest.fn(); +const mockFns = { + fetchConversation: jest.fn(() => Promise.resolve({ sid: 'mockConversationSid' })), + createConversation: jest.fn(() => Promise.resolve({ sid: 'newMockConversationSid' })), + createParticipant: jest.fn(() => Promise.resolve({ sid: 'mockParticipantSid' })), + createRoom: jest.fn(() => Promise.resolve({ sid: 'mockNewRoomSid' })), + fetchRoom: jest.fn(() => Promise.resolve({ sid: 'mockRoomSid' })), +}; + +const mockConversationsClient = { + conversations: jest.fn(() => ({ + fetch: mockFns.fetchConversation, + participants: { + create: mockFns.createParticipant, + }, + })), +}; -const mockTwilioClient = set({}, 'video.rooms.create', mockCreateFunction); +mockConversationsClient.conversations.create = mockFns.createConversation; + +const mockTwilioClient = { + video: { + rooms: jest.fn(() => ({ fetch: mockFns.fetchRoom })), + }, + conversations: jest.fn(), +}; +mockTwilioClient.video.rooms.create = mockFns.createRoom; +mockTwilioClient.conversations.services = jest.fn(() => mockConversationsClient); Date.now = () => 5; @@ -14,13 +37,144 @@ const mockContext = { ACCOUNT_SID: 'AC1234', TWILIO_API_KEY_SID: 'SK1234', TWILIO_API_KEY_SECRET: 'api_secret', + CONVERSATIONS_SERVICE_SID: 'MockServiceSid', ROOM_TYPE: 'group', getTwilioClient: () => mockTwilioClient, }; describe('the video-token-server', () => { - beforeEach(() => { - mockCreateFunction.mockImplementation(() => Promise.resolve()); + beforeEach(jest.clearAllMocks); + + describe("when a room and conversation doesn't already exist", () => { + beforeEach(() => { + mockFns.fetchRoom.mockImplementationOnce(() => Promise.reject()); + mockFns.fetchConversation.mockImplementationOnce(() => Promise.reject()); + }); + + it('should create a new room and conversation, then return a valid token', async () => { + await handler( + mockContext, + { passcode: '12345612345678', room_name: 'test-room', user_identity: 'test-user', create_conversation: true }, + callback + ); + + expect(mockFns.createRoom).toHaveBeenCalledWith({ type: 'group', uniqueName: 'test-room' }); + expect(mockFns.createConversation).toHaveBeenCalledWith({ uniqueName: 'mockNewRoomSid' }); + expect(callback).toHaveBeenCalledWith(null, { + body: { token: expect.any(String), room_type: 'group' }, + headers: { 'Content-Type': 'application/json' }, + statusCode: 200, + }); + }); + + it('should return an error when there is a problem creating the room', async () => { + mockFns.createRoom.mockImplementationOnce(() => Promise.reject({ code: 12345 })); + + await handler( + mockContext, + { passcode: '12345612345678', room_name: 'test-room', user_identity: 'test-user' }, + callback + ); + + expect(callback).toHaveBeenCalledWith(null, { + body: { + error: { + explanation: 'Something went wrong when creating a room.', + message: 'error creating room', + }, + }, + headers: { 'Content-Type': 'application/json' }, + statusCode: 500, + }); + }); + + it('should return an error when there is a problem creating the conversation', async () => { + mockFns.createConversation.mockImplementationOnce(() => Promise.reject({ code: 12345 })); + + await handler( + mockContext, + { passcode: '12345612345678', room_name: 'test-room', user_identity: 'test-user', create_conversation: true }, + callback + ); + + expect(callback).toHaveBeenCalledWith(null, { + body: { + error: { + explanation: 'Something went wrong when creating a conversation.', + message: 'error creating conversation', + }, + }, + headers: { 'Content-Type': 'application/json' }, + statusCode: 500, + }); + }); + }); + + describe('when a room and conversation already exist', () => { + it('should fetch the existing room and conversation, then return a valid token', async () => { + await handler( + mockContext, + { passcode: '12345612345678', room_name: 'test-room', user_identity: 'test-user', create_conversation: true }, + callback + ); + + expect(mockTwilioClient.video.rooms).toHaveBeenCalledWith('test-room'); + expect(mockFns.fetchRoom).toHaveBeenCalled(); + expect(mockConversationsClient.conversations).toHaveBeenCalledWith('mockRoomSid'); + expect(mockFns.fetchConversation).toHaveBeenCalled(); + expect(callback).toHaveBeenCalledWith(null, { + body: { token: expect.any(String), room_type: 'group' }, + headers: { 'Content-Type': 'application/json' }, + statusCode: 200, + }); + }); + }); + + it('should return an error when there is a problem adding a participant to the conversation', async () => { + mockFns.createParticipant.mockImplementationOnce(() => Promise.reject({ code: 12345 })); + + await handler( + mockContext, + { passcode: '12345612345678', room_name: 'test-room', user_identity: 'test-user', create_conversation: true }, + callback + ); + + expect(callback).toHaveBeenCalledWith(null, { + body: { + error: { + explanation: 'Something went wrong when creating a conversation participant.', + message: 'error creating conversation participant', + }, + }, + headers: { 'Content-Type': 'application/json' }, + statusCode: 500, + }); + }); + + it('should ignore 50433 errors when adding a participant to the conversation', async () => { + mockFns.createParticipant.mockImplementationOnce(() => Promise.reject({ code: 50433 })); + + await handler( + mockContext, + { passcode: '12345612345678', room_name: 'test-room', user_identity: 'test-user', create_conversation: true }, + callback + ); + + expect(callback).toHaveBeenCalledWith(null, { + body: { token: expect.any(String), room_type: 'group' }, + headers: { 'Content-Type': 'application/json' }, + statusCode: 200, + }); + }); + + it('should create a conversations client with the correct conversations service', async () => { + await handler( + mockContext, + { passcode: '12345612345678', room_name: 'test-room', user_identity: 'test-user', create_conversation: true }, + callback + ); + + expect(mockTwilioClient.conversations.services).toHaveBeenCalledWith('MockServiceSid'); }); it('should return an "invalid parameter" error when the create_room parameter is not a boolean', async () => { @@ -38,6 +192,21 @@ describe('the video-token-server', () => { }); }); + it('should return an "invalid parameter" error when the create_conversation parameter is not a boolean', async () => { + await handler(mockContext, { user_identity: 'test identity', create_conversation: 'no thanks' }, callback); + + expect(callback).toHaveBeenCalledWith(null, { + body: { + error: { + message: 'invalid parameter', + explanation: 'A boolean value must be provided for the create_conversation parameter', + }, + }, + headers: { 'Content-Type': 'application/json' }, + statusCode: 400, + }); + }); + it('should return a "missing user_identity" error when the "user_identity" parameter is not supplied', () => { handler(mockContext, {}, callback); @@ -53,8 +222,23 @@ describe('the video-token-server', () => { }); }); - it('should return a token when no room_name is supplied', async () => { - await handler(mockContext, { user_identity: 'test identity' }, callback); + it('should return a "missing room_name" error when the "room_name" parameter is not supplied when "create_room" is true', () => { + handler(mockContext, { user_identity: 'mockIdentity' }, callback); + + expect(callback).toHaveBeenCalledWith(null, { + body: { + error: { + message: 'missing room_name', + explanation: 'The room_name parameter is missing. room_name is required when create_room is true.', + }, + }, + headers: { 'Content-Type': 'application/json' }, + statusCode: 400, + }); + }); + + it('should return a token when no room_name is supplied and "create_room" is false', async () => { + await handler(mockContext, { user_identity: 'test identity', create_room: false }, callback); expect(callback).toHaveBeenCalledWith(null, { body: { token: expect.any(String), room_type: 'group' }, @@ -67,6 +251,9 @@ describe('the video-token-server', () => { grants: { identity: 'test identity', video: {}, + chat: { + service_sid: 'MockServiceSid', + }, }, iat: 0, iss: 'SK1234', @@ -92,6 +279,9 @@ describe('the video-token-server', () => { video: { room: 'test-room', }, + chat: { + service_sid: 'MockServiceSid', + }, }, iat: 0, iss: 'SK1234', @@ -116,14 +306,14 @@ describe('the video-token-server', () => { expect(jwt.verify(callback.mock.calls[0][1].body.token, 'api_secret')).toBeTruthy(); }); - it('should create a new room and return a valid token', async () => { + it('should return a valid token without creating a room when "create_room" is false', async () => { await handler( mockContext, - { passcode: '12345612345678', room_name: 'test-room', user_identity: 'test-user' }, + { passcode: '12345612345678', room_name: 'test-room', user_identity: 'test-user', create_room: false }, callback ); - expect(mockCreateFunction).toHaveBeenCalledWith({ type: 'group', uniqueName: 'test-room' }); + expect(mockFns.createRoom).not.toHaveBeenCalled(); expect(callback).toHaveBeenCalledWith(null, { body: { token: expect.any(String), room_type: 'group' }, headers: { 'Content-Type': 'application/json' }, @@ -131,14 +321,16 @@ describe('the video-token-server', () => { }); }); - it('should return a valid token without creating a room when "create_room" is false', async () => { + it('should return a valid token without creating a conversation when "create_conversation" is false', async () => { await handler( mockContext, - { passcode: '12345612345678', room_name: 'test-room', user_identity: 'test-user', create_room: false }, + { passcode: '12345612345678', room_name: 'test-room', user_identity: 'test-user', create_conversation: false }, callback ); - expect(mockCreateFunction).not.toHaveBeenCalled(); + expect(mockFns.fetchConversation).not.toHaveBeenCalled(); + expect(mockFns.createConversation).not.toHaveBeenCalled(); + expect(mockFns.createParticipant).not.toHaveBeenCalled(); expect(callback).toHaveBeenCalledWith(null, { body: { token: expect.any(String), room_type: 'group' }, headers: { 'Content-Type': 'application/json' }, @@ -147,7 +339,7 @@ describe('the video-token-server', () => { }); it('should return a valid token when passcode when the room already exists', async () => { - mockCreateFunction.mockImplementation(() => Promise.reject({ code: 53113 })); + mockFns.createRoom.mockImplementation(() => Promise.reject({ code: 53113 })); await handler( mockContext, @@ -161,26 +353,5 @@ describe('the video-token-server', () => { statusCode: 200, }); }); - - it('should return an error when there is a problem creating the room', async () => { - mockCreateFunction.mockImplementationOnce(() => Promise.reject({ code: 12345 })); - - await handler( - mockContext, - { passcode: '12345612345678', room_name: 'test-room', user_identity: 'test-user' }, - callback - ); - - expect(callback).toHaveBeenCalledWith(null, { - body: { - error: { - explanation: 'Something went wrong when creating a room.', - message: 'error creating room', - }, - }, - headers: { 'Content-Type': 'application/json' }, - statusCode: 500, - }); - }); }); });