Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Cli core upgrade #112

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ node-defaults: &node-defaults
working_directory: *workspace
executor:
name: node/default
tag: 13.10.1
tag: 14.0.0

release-filter: &release-filter
filters:
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ module.exports = {
setupFiles: ['./test/setupTests.js'],

// The test environment that will be used for testing
testEnvironment: 'node'
testEnvironment: 'node',
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"@oclif/command": "^1.5.19",
"@oclif/config": "^1.14.0",
"@oclif/plugin-help": "^5.1.11",
"@twilio-labs/serverless-api": "^4.0.3",
"@twilio/cli-core": "^6.7.0",
"@twilio-labs/serverless-api": "^5.4.0",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shrutiburman did you ensure this doesn't break any of the ahoy apps that rely on this plugin?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't aware of the ahoy apps. Have only tested the default cmds that the plugin has. Create, deploy and delete a video app. Can you help me with the ahoy apps implementation?

Copy link

@seancoleman2 seancoleman2 Oct 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This plugin was created to to enable customers to build/deploy our Video reference apps. Here are instructions to deploy the React app: https://github.com/twilio/twilio-video-app-react#clone-the-repository

I would assume upgrading will be fine but just wanted to call it out.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the readme file of the twilio-video-app-react, I did some basic acceptance testing. Gist here. It'd be great if you or someone from the video-app-react team could take a look at it.

"@twilio/cli-core": "^7.0.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nanoid": "^3.3.4"
Expand Down
303 changes: 1 addition & 302 deletions test/e2e/e2e.test.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,16 @@
const { APP_NAME } = require('../../src/constants');

const DeleteCommand = require('../../src/commands/rtc/apps/video/delete');
const DeployCommand = require('../../src/commands/rtc/apps/video/deploy');
const ViewCommand = require('../../src/commands/rtc/apps/video/view');

const jwt = require('jsonwebtoken');
const { nanoid } = require('nanoid');
const path = require('path');
const { stdout } = require('stdout-stderr');
const superagent = require('superagent');

const twilioClient = require('twilio')(process.env.TWILIO_API_KEY, process.env.TWILIO_API_SECRET, {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shrutiburman are these account credentials configured by the customer when they deploy the React / iOS / Android app? Or are you saying these are associated with a Twilio account?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These account creds are configured on circle CI env and are referenced from there. These are associated with a Twilio account.

accountSid: process.env.TWILIO_ACCOUNT_SID,
});

// Uncomment to see output from CLI
// stdout.print = true;

jest.setTimeout(80000);

jest.mock('../../src/constants', () => ({
APP_NAME: 'video-app-e2e-tests',
}));

function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

function getPasscode(output) {
const match = output.match(/Passcode: ([\d\s]+)\n/);
return match ? match[1].replace(/\s+/g, '') : null;
}

function getWebAppURL(output) {
const match = output.match(/Web App URL: (.+)\n/);
return match ? match[1] : null;
}

function getURL(output) {
const passcode = getPasscode(output);
return `https://${APP_NAME}-${passcode.slice(6, 10)}-${passcode.slice(10)}-dev.twil.io`;
}

describe('the RTC Twilio-CLI Plugin', () => {
beforeAll(async () => {
Expand Down Expand Up @@ -91,276 +62,4 @@ describe('the RTC Twilio-CLI Plugin', () => {
});
});

describe('after deploying a video app', () => {
let URL;
let webAppURL;
let passcode;

beforeAll(async done => {
stdout.start();
await DeployCommand.run([
'--authentication',
'passcode',
'--app-directory',
path.join(__dirname, '../test-assets'),
]);
stdout.stop();
passcode = getPasscode(stdout.output);
URL = getURL(stdout.output);
webAppURL = getWebAppURL(stdout.output);
done();
});

afterAll(async () => {
await delay(60000);
return await DeleteCommand.run([]);
});

describe('the view command', () => {
it('should correctly display the deployment details', async () => {
stdout.start();
await ViewCommand.run([]);
stdout.stop();
expect(stdout.output).toMatch(
/Web App URL: .+\nPasscode: \d{3} \d{3} \d{4} \d{4}\nExpires: .+\nRoom Type: group\nEdit your token server at: https:\/\/www.twilio.com\/console\/functions\/editor\/ZS\w{32}\/environment\/ZE\w{32}\/function\/ZH\w{32}/
);
});
});

describe('the serverless deployment', () => {
it('should create a group room and return a video token when the correct passcode is provided', async () => {
const ROOM_NAME = nanoid();
const { body } = await superagent
.post(`${URL}/token`)
.send({ passcode, room_name: ROOM_NAME, user_identity: 'test user' });
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(
expect.objectContaining({ identity: 'test user', video: { room: ROOM_NAME } })
);
expect(body.room_type).toEqual('group');

try {
await twilioClient.video.rooms(ROOM_NAME).fetch();
} catch (e) {
expect(e).toMatchObject({ status: 404 });
}
});

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`)
.send({ passcode: '0000' })
.catch(e => expect(e.status).toBe(401));
});

it('should display a URL which returns the web app', async () => {
const { text } = await superagent.get(webAppURL);
expect(text).toEqual('<html>test</html>');
});

it('should return the web app from "/login"', async () => {
const webAppURL = URL;
const { text } = await superagent.get(webAppURL + '/login');
expect(text).toEqual('<html>test</html>');
});
});

describe('the deploy command', () => {
it('should not redeploy the app when no --override flag is passed', async () => {
stdout.start();
await DeployCommand.run([
'--authentication',
'passcode',
'--app-directory',
path.join(__dirname, '../test-assets'),
]);
stdout.stop();
expect(stdout.output).toContain(
'A Video app is already deployed. Use the --override flag to override the existing deployment.'
);
});

it('should redeploy the app when --override flag is passed', async () => {
stdout.start();
await DeployCommand.run([
'--authentication',
'passcode',
'--override',
'--app-directory',
path.join(__dirname, '../test-assets'),
]);
stdout.stop();
const updatedPasscode = getPasscode(stdout.output);
const testURL = getURL(stdout.output);
const testWebAppURL = getWebAppURL(stdout.output);
expect(updatedPasscode).not.toEqual(passcode);
expect(testURL).toEqual(URL);
const { text } = await superagent.get(testWebAppURL + '/login');
expect(text).toEqual('<html>test</html>');
});

it('should redeploy the token server when no app-directory is set and when --override flag is true', async () => {
stdout.start();
await DeployCommand.run(['--authentication', 'passcode', '--override']);
stdout.stop();
const updatedPasscode = getPasscode(stdout.output);
const testURL = getURL(stdout.output);
const testWebAppURL = getWebAppURL(stdout.output);
expect(updatedPasscode).not.toEqual(passcode);
expect(testURL).toEqual(URL);
superagent.get(`${testWebAppURL}`).catch(e => expect(e.status).toBe(404));
});
});
});

describe('after deploying a token server (with go rooms)', () => {
let URL;
let passcode;
let webAppURL;

beforeAll(async done => {
stdout.start();
await DeployCommand.run(['--authentication', 'passcode', '--room-type', 'go']);
stdout.stop();
passcode = getPasscode(stdout.output);
URL = getURL(stdout.output);
webAppURL = getWebAppURL(stdout.output);
done();
});

afterAll(async () => {
await delay(60000);
return await DeleteCommand.run([]);
});

describe('the view command', () => {
it('should correctly display the deployment details', async () => {
stdout.start();
await ViewCommand.run([]);
stdout.stop();
expect(stdout.output).toMatch(
/Passcode: \d{3} \d{3} \d{4} \d{4}\nExpires: .+\nRoom Type: go\nEdit your token server at: https:\/\/www.twilio.com\/console\/functions\/editor\/ZS\w{32}\/environment\/ZE\w{32}\/function\/ZH\w{32}/
);
expect(stdout.output).not.toMatch(/Web App URL:/);
});
});

describe('the serverless deployment', () => {
it('should create a go room and return a video token when the correct passcode is provided', async () => {
const ROOM_NAME = nanoid();
const { body } = await superagent
.post(`${URL}/token`)
.send({ passcode, room_name: ROOM_NAME, user_identity: 'test user' });
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();
expect(room.type).toEqual('go');
});

it('should return a 401 error when an incorrect passcode is provided', () => {
superagent
.post(`${URL}/token`)
.send({ passcode: '0000' })
.catch(e => expect(e.status).toBe(401));
});

it('should not display an app URL', () => {
expect(webAppURL).toBeNull();
});

it('should return a 404 from "/"', () => {
superagent.get(`${URL}`).catch(e => expect(e.status).toBe(404));
});
});

describe('the deploy command', () => {
it('should redeploy the token server when --override flag is passed', async () => {
stdout.start();
await DeployCommand.run(['--authentication', 'passcode', '--override']);
stdout.stop();

const updatedPasscode = getPasscode(stdout.output);
const testURL = getURL(stdout.output);
expect(updatedPasscode).not.toEqual(passcode);
expect(testURL).toEqual(URL);

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(
expect.objectContaining({ identity: 'test user', video: { room: 'test-room' } })
);
});
});
});
});