diff --git a/CHANGELOG.md b/CHANGELOG.md
index 41fb14a..688e289 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## 0.7.0
+
+### Enhancements
+
+- This version adds a new endpoint which allows users to update the recording rules for a given room. See the [README](README.md#recording-rules) for more details.
+- Upgraded @twilio/cli-core from 5.9.1 to 5.9.3
+- Upgraded moment from 2.28.0 to 2.29.0
+- Upgraded nanoid from 3.1.13 to 3.1.16
+- Upgraded @twilio-labs/serverless-api from 4.0.2 to 4.0.3
+
## 0.6.0
### Enhancements
diff --git a/README.md b/README.md
index ced5a92..50710df 100644
--- a/README.md
+++ b/README.md
@@ -56,7 +56,7 @@ A mobile and web collaboration application built with Twilio Programmable Video.
- [iOS App](https://github.com/twilio/twilio-video-app-ios)
- [Android App](https://github.com/twilio/twilio-video-app-android)
-#### Token Server API Documentation
+## Token Server API Documentation
The following section documents the application [token server](/src/video-token-server.js) used to provide [Programable Video access tokens](https://www.twilio.com/docs/video/tutorials/user-identity-access-tokens) to supported Twilio Video applications. The token server is deployed as a [Twilio Function](https://www.twilio.com/docs/runtime/functions).
@@ -64,11 +64,11 @@ The following section documents the application [token server](/src/video-token-
| ------ | ------------------ |
| POST | [`/token`](#token) |
-##### Authentication
+### Authentication
-The application token server requires an [authentication mechanism](#twilio-rtcappsvideodeploy---authentication-auth) to be specified when deploying. The following section documents each support authentication mechanism.
+The application token server requires an [authentication mechanism](#twilio-rtcappsvideodeploy---authentication-auth) to be specified when deploying. The following section documents each supported authentication mechanism.
-###### Passcode
+#### Passcode
Each request is verified using a passcode generated at deploy time. Passcodes remain valid for one week. After the passcode expires, users can redeploy an application and a new passcode will be generated. The snippet below provides an example request body used by a supported application.
@@ -80,7 +80,7 @@ Each request is verified using a passcode generated at deploy time. Passcodes re
}
```
-##### Token
+### Token
Returns a Programmable Video Access token.
@@ -88,7 +88,7 @@ Returns a Programmable Video Access token.
POST /token
```
-###### Parameters
+#### Parameters
| Name | Type | Description |
| --------------- | --------- | -------------------------------------------------------------------------------------- |
@@ -97,7 +97,7 @@ POST /token
| `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. |
-###### Success Responses
+#### Success Responses
@@ -119,7 +119,7 @@ POST /token
-###### Error Responses
+#### Error Responses
@@ -189,6 +189,108 @@ POST /token
+### Recording Rules
+
+Changes the Recording Rules for a given room SID.
+
+```shell
+POST /recordingrules
+```
+
+#### Parameters
+
+| Name | Type | Description |
+| ---------- | -------- | ------------------------------------------------------------------- |
+| `passcode` | `string` | **Required**. The application passcode. |
+| `room_sid` | `string` | **Required**. The SID of the room to change the recording rules of. |
+| `rules` | `array` | **Required**. An array of recording rules to apply to the room. |
+
+#### Success Responses
+
+
+
+ Status | Response |
+
+
+ 200 |
+
+
+```json
+{
+ "roomSid": "RM00000000000000000000000000000000",
+ "rules": [
+ {
+ "all": true,
+ "type": "exclude"
+ }
+ ],
+ "dateCreated": "2020-11-18T02:58:20.000Z",
+ "dateUpdated": "2020-11-18T03:21:18.000Z"
+}
+```
+
+ |
+
+
+
+
+#### Error Responses
+
+
+
+ Status | Response |
+
+
+
+ 400 |
+
+
+```json
+{
+ "error": {
+ "message": "missing room_sid",
+ "explanation": "The room_sid parameter is missing."
+ }
+}
+```
+
+ |
+
+
+
+ 400 |
+
+
+```json
+{
+ "error": {
+ "message": "missing rules",
+ "explanation": "The rules parameter is missing."
+ }
+}
+```
+
+ |
+
+
+
+ 401 |
+
+
+```json
+{
+ "error": {
+ "message": "passcode incorrect",
+ "explanation": "The passcode used to validate application users is incorrect."
+ }
+}
+```
+
+ |
+
+
+
+
## Commands
diff --git a/package-lock.json b/package-lock.json
index c2194e9..258fbac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "@twilio-labs/plugin-rtc",
- "version": "0.6.1",
+ "version": "0.7.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index b562a60..abbb883 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@twilio-labs/plugin-rtc",
- "version": "0.6.1",
+ "version": "0.7.0",
"description": "A Twilio-CLI plugin for real-time communication apps",
"main": "index.js",
"publishConfig": {
diff --git a/src/helpers.js b/src/helpers.js
index 1d8258e..69e3df8 100644
--- a/src/helpers.js
+++ b/src/helpers.js
@@ -47,18 +47,33 @@ async function getAssets(folder) {
const indexHTML = assets.find(asset => asset.name.includes('index.html'));
- assets.push({
- ...indexHTML,
- path: '/',
- name: '/',
- });
- assets.push({
- ...indexHTML,
- path: '/login',
- name: '/login',
- });
+ const allAssets = assets.concat([
+ {
+ ...indexHTML,
+ path: '/',
+ name: '/',
+ },
+ {
+ ...indexHTML,
+ path: '/login',
+ name: '/login',
+ },
+ ]);
+
+ return allAssets;
+}
+
+function getMiddleware() {
+ const authHandlerFn = fs.readFileSync(path.join(__dirname, './serverless/middleware/auth.js'));
- return assets;
+ return [
+ {
+ name: 'auth-handler',
+ path: '/auth-handler.js',
+ content: authHandlerFn,
+ access: 'private',
+ },
+ ];
}
async function findApp() {
@@ -80,7 +95,7 @@ async function getAppInfo() {
const assets = await appInstance.assets.list();
const functions = await appInstance.functions.list();
- const tokenServerFunction = functions.find(fn => fn.friendlyName === 'token');
+ const tokenServerFunction = functions.find(fn => fn.friendlyName.includes('token'));
const passcodeVar = variables.find(v => v.key === 'API_PASSCODE');
const expiryVar = variables.find(v => v.key === 'API_PASSCODE_EXPIRY');
@@ -97,7 +112,7 @@ async function getAppInfo() {
expiry: moment(Number(expiry)).toString(),
sid: app.sid,
passcode: fullPasscode,
- hasAssets: Boolean(assets.length),
+ hasWebAssets: Boolean(assets.find(asset => asset.friendlyName.includes('index.html'))),
roomType,
environmentSid: environment.sid,
functionSid: tokenServerFunction.sid,
@@ -112,7 +127,7 @@ async function displayAppInfo() {
return;
}
- if (appInfo.hasAssets) {
+ if (appInfo.hasWebAssets) {
console.log(`Web App URL: ${appInfo.url}`);
}
@@ -130,6 +145,12 @@ async function displayAppInfo() {
async function deploy() {
const assets = this.flags['app-directory'] ? await getAssets(this.flags['app-directory']) : [];
+ const { functions } = await getListOfFunctionsAndAssets(__dirname, {
+ functionsFolderNames: ['serverless/functions'],
+ assetsFolderNames: [],
+ });
+
+ assets.push(...getMiddleware());
if (this.twilioClient.username === this.twilioClient.accountSid) {
// When twilioClient.username equals twilioClient.accountSid, it means that the user
@@ -158,8 +179,6 @@ TWILIO_API_SECRET = the secret for the API Key`);
const pin = getRandomInt(6);
const expiryTime = Date.now() + EXPIRY_PERIOD;
- const fn = fs.readFileSync(path.join(__dirname, './video-token-server.js'));
-
cli.action.start('deploying app');
const deployOptions = {
@@ -170,17 +189,14 @@ TWILIO_API_SECRET = the secret for the API Key`);
API_PASSCODE_EXPIRY: expiryTime,
ROOM_TYPE: this.flags['room-type'],
},
- pkgJson: {},
- functionsEnv: 'dev',
- functions: [
- {
- name: 'token',
- path: '/token',
- content: fn,
- access: 'public',
+ pkgJson: {
+ dependencies: {
+ twilio: '^3.51.0',
},
- ],
- assets: assets,
+ },
+ functionsEnv: 'dev',
+ functions,
+ assets,
};
if (this.appInfo && this.appInfo.sid) {
@@ -205,6 +221,7 @@ module.exports = {
displayAppInfo,
findApp,
getAssets,
+ getMiddleware,
getAppInfo,
getPasscode,
getRandomInt,
diff --git a/src/serverless/functions/recordingrules.js b/src/serverless/functions/recordingrules.js
new file mode 100644
index 0000000..8bf80f2
--- /dev/null
+++ b/src/serverless/functions/recordingrules.js
@@ -0,0 +1,52 @@
+/* global Twilio Runtime */
+'use strict';
+
+// We need to use a newer twilio client than the one provided by context.getTwilioClient(),
+// so we require it here. The version is specified in helpers.js in the 'deployOptions' object.
+// TODO: replace with context.getTwilioClient() when https://issues.corp.twilio.com/browse/RUN-3731 is complete
+const twilio = require('twilio');
+
+module.exports.handler = async (context, event, callback) => {
+ const authHandler = require(Runtime.getAssets()['/auth-handler.js'].path);
+ authHandler(context, event, callback);
+
+ let response = new Twilio.Response();
+ response.appendHeader('Content-Type', 'application/json');
+
+ const { room_sid, rules } = event;
+
+ if (typeof room_sid === 'undefined') {
+ response.setStatusCode(400);
+ response.setBody({
+ error: {
+ message: 'missing room_sid',
+ explanation: 'The room_sid parameter is missing.',
+ },
+ });
+ return callback(null, response);
+ }
+
+ if (typeof rules === 'undefined') {
+ response.setStatusCode(400);
+ response.setBody({
+ error: {
+ message: 'missing rules',
+ explanation: 'The rules parameter is missing.',
+ },
+ });
+ return callback(null, response);
+ }
+
+ const client = twilio(context.ACCOUNT_SID, context.AUTH_TOKEN);
+
+ try {
+ const recordingRulesResponse = await client.video.rooms(room_sid).recordingRules.update({ rules });
+ response.setStatusCode(200);
+ response.setBody(recordingRulesResponse);
+ } catch (err) {
+ response.setStatusCode(500);
+ response.setBody({ error: { message: err.message, code: err.code } });
+ }
+
+ callback(null, response);
+};
diff --git a/src/video-token-server.js b/src/serverless/functions/token.js
similarity index 64%
rename from src/video-token-server.js
rename to src/serverless/functions/token.js
index adace0d..4952811 100644
--- a/src/video-token-server.js
+++ b/src/serverless/functions/token.js
@@ -1,4 +1,4 @@
-/* global Twilio */
+/* global Twilio Runtime */
'use strict';
const AccessToken = Twilio.jwt.AccessToken;
@@ -6,18 +6,12 @@ const VideoGrant = AccessToken.VideoGrant;
const MAX_ALLOWED_SESSION_DURATION = 14400;
module.exports.handler = async (context, event, callback) => {
- const {
- ACCOUNT_SID,
- TWILIO_API_KEY_SID,
- TWILIO_API_KEY_SECRET,
- API_PASSCODE,
- API_PASSCODE_EXPIRY,
- DOMAIN_NAME,
- ROOM_TYPE,
- } = context;
+ const { ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, ROOM_TYPE } = context;
- const { user_identity, room_name, passcode, create_room = true } = event;
- const [, appID, serverlessID] = DOMAIN_NAME.match(/-?(\d*)-(\d+)(?:-\w+)?.twil.io$/);
+ const authHandler = require(Runtime.getAssets()['/auth-handler.js'].path);
+ authHandler(context, event, callback);
+
+ const { user_identity, room_name, create_room = true } = event;
let response = new Twilio.Response();
response.appendHeader('Content-Type', 'application/json');
@@ -33,29 +27,6 @@ module.exports.handler = async (context, event, callback) => {
return callback(null, response);
}
- if (Date.now() > API_PASSCODE_EXPIRY) {
- response.setStatusCode(401);
- response.setBody({
- error: {
- message: 'passcode expired',
- explanation:
- 'The passcode used to validate application users has expired. Re-deploy the application to refresh the passcode.',
- },
- });
- return callback(null, response);
- }
-
- if (API_PASSCODE + appID + serverlessID !== passcode) {
- response.setStatusCode(401);
- response.setBody({
- error: {
- message: 'passcode incorrect',
- explanation: 'The passcode used to validate application users is incorrect.',
- },
- });
- return callback(null, response);
- }
-
if (!user_identity) {
response.setStatusCode(400);
response.setBody({
diff --git a/src/serverless/middleware/auth.js b/src/serverless/middleware/auth.js
new file mode 100644
index 0000000..8f58ceb
--- /dev/null
+++ b/src/serverless/middleware/auth.js
@@ -0,0 +1,35 @@
+/* global Twilio */
+'use strict';
+
+module.exports = async (context, event, callback) => {
+ const { API_PASSCODE, API_PASSCODE_EXPIRY, DOMAIN_NAME } = context;
+
+ const { passcode } = event;
+ const [, appID, serverlessID] = DOMAIN_NAME.match(/-?(\d*)-(\d+)(?:-\w+)?.twil.io$/);
+
+ let response = new Twilio.Response();
+ response.appendHeader('Content-Type', 'application/json');
+
+ if (Date.now() > API_PASSCODE_EXPIRY) {
+ response.setStatusCode(401);
+ response.setBody({
+ error: {
+ message: 'passcode expired',
+ explanation:
+ 'The passcode used to validate application users has expired. Re-deploy the application to refresh the passcode.',
+ },
+ });
+ return callback(null, response);
+ }
+
+ if (API_PASSCODE + appID + serverlessID !== passcode) {
+ response.setStatusCode(401);
+ response.setBody({
+ error: {
+ message: 'passcode incorrect',
+ explanation: 'The passcode used to validate application users is incorrect.',
+ },
+ });
+ return callback(null, response);
+ }
+};
diff --git a/test/helpers/helpers.test.js b/test/helpers/helpers.test.js
index ae8723c..7f239d4 100644
--- a/test/helpers/helpers.test.js
+++ b/test/helpers/helpers.test.js
@@ -8,6 +8,7 @@ const {
getPasscode,
getRandomInt,
verifyAppDirectory,
+ getMiddleware,
} = require('../../src/helpers');
const { getListOfFunctionsAndAssets } = require('@twilio-labs/serverless-api/dist/utils/fs');
const path = require('path');
@@ -43,7 +44,7 @@ function getMockTwilioInstance(options) {
};
const mockAppInstance = {
- assets: { list: () => Promise.resolve(options.hasAssets ? [{}] : []) },
+ assets: { list: () => Promise.resolve(options.hasWebAssets ? [{ friendlyName: 'index.html' }] : []) },
functions: {},
update: jest.fn(() => Promise.resolve()),
};
@@ -114,23 +115,40 @@ describe('the verifyAppDirectory function', () => {
describe('the getAssets function', () => {
it('should add index.html at "/" and "/login" paths', async () => {
- expect(await getAssets('mockFolder')).toEqual([
- {
- name: 'index.html',
- path: 'index.html',
- content: 'mockHTMLcontent',
- },
- {
- name: '/',
- path: '/',
- content: 'mockHTMLcontent',
- },
- {
- name: '/login',
- path: '/login',
- content: 'mockHTMLcontent',
- },
- ]);
+ expect(await getAssets('mockFolder')).toEqual(
+ expect.arrayContaining([
+ {
+ name: 'index.html',
+ path: 'index.html',
+ content: 'mockHTMLcontent',
+ },
+ {
+ name: '/',
+ path: '/',
+ content: 'mockHTMLcontent',
+ },
+ {
+ name: '/login',
+ path: '/login',
+ content: 'mockHTMLcontent',
+ },
+ ])
+ );
+ });
+});
+
+describe('the getMiddleware function', () => {
+ it('should add the auth-handler.js as a private asset', async () => {
+ expect(await getMiddleware('mockFolder')).toEqual(
+ expect.arrayContaining([
+ {
+ name: 'auth-handler',
+ path: '/auth-handler.js',
+ content: expect.any(Buffer),
+ access: 'private',
+ },
+ ])
+ );
});
it('should use the CWD when provided with a relative path', async () => {
@@ -185,7 +203,7 @@ describe('the getAppInfo function', () => {
expiry: 'Wed May 20 2020 18:40:00 GMT+0000',
environmentSid: 'environmentSid',
functionSid: 'tokenFunctionSid',
- hasAssets: false,
+ hasWebAssets: false,
passcode: '12345612345678',
sid: 'appSid',
url: `https://${APP_NAME}-1234-5678-dev.twil.io?passcode=12345612345678`,
@@ -193,15 +211,15 @@ describe('the getAppInfo function', () => {
});
});
- it('should return the correct information when there are assets', async () => {
+ it('should return the correct information when there are web assets', async () => {
const result = await getAppInfo.call({
- twilioClient: getMockTwilioInstance({ exists: true, hasAssets: true }),
+ twilioClient: getMockTwilioInstance({ exists: true, hasWebAssets: true }),
});
expect(result).toEqual({
expiry: 'Wed May 20 2020 18:40:00 GMT+0000',
environmentSid: 'environmentSid',
functionSid: 'tokenFunctionSid',
- hasAssets: true,
+ hasWebAssets: true,
passcode: '12345612345678',
sid: 'appSid',
url: `https://${APP_NAME}-1234-5678-dev.twil.io?passcode=12345612345678`,
@@ -236,7 +254,7 @@ describe('the displayAppInfo function', () => {
it('should display the correct information when there are assets', async () => {
await displayAppInfo.call({
- twilioClient: getMockTwilioInstance({ exists: true, hasAssets: true }),
+ twilioClient: getMockTwilioInstance({ exists: true, hasWebAssets: true }),
});
expect(stdout.output).toMatchInlineSnapshot(`
"Web App URL: https://${APP_NAME}-1234-5678-dev.twil.io?passcode=12345612345678
diff --git a/test/serverless/functions/recordingrules.test.js b/test/serverless/functions/recordingrules.test.js
new file mode 100644
index 0000000..1d80c4b
--- /dev/null
+++ b/test/serverless/functions/recordingrules.test.js
@@ -0,0 +1,88 @@
+const { handler } = require('../../../src/serverless/functions/recordingrules');
+const twilio = require('twilio');
+
+const mockUpdateFn = jest.fn(() => Promise.resolve('mockSuccessResponse'));
+
+const mockClient = jest.fn(() => ({
+ video: {
+ rooms: jest.fn(() => ({
+ recordingRules: {
+ update: mockUpdateFn,
+ },
+ })),
+ },
+}));
+
+jest.mock('twilio');
+twilio.mockImplementation(mockClient);
+
+describe('the recordingrules function', () => {
+ it('should correctly respond when a room update is successful', async () => {
+ const mockCallback = jest.fn();
+
+ await handler(
+ { ACCOUNT_SID: '1234', AUTH_TOKEN: '2345' },
+ { room_sid: 'mockRoomSid', rules: 'mockRules' },
+ mockCallback
+ );
+
+ expect(mockClient).toHaveBeenCalledWith('1234', '2345');
+ expect(mockCallback).toHaveBeenCalledWith(null, {
+ body: 'mockSuccessResponse',
+ headers: { 'Content-Type': 'application/json' },
+ statusCode: 200,
+ });
+ });
+
+ it('should correctly respond when a room update is not successful', async () => {
+ const mockCallback = jest.fn();
+ const mockError = { message: 'mockErrorMesage', code: 123 };
+ mockUpdateFn.mockImplementationOnce(() => Promise.reject(mockError));
+
+ await handler(
+ { ACCOUNT_SID: '1234', AUTH_TOKEN: '2345' },
+ { room_sid: 'mockRoomSid', rules: 'mockRules' },
+ mockCallback
+ );
+
+ expect(mockCallback).toHaveBeenCalledWith(null, {
+ body: { error: mockError },
+ headers: { 'Content-Type': 'application/json' },
+ statusCode: 500,
+ });
+ });
+
+ it('should return a "missing room_sid" error when the room_sid is absent', async () => {
+ const mockCallback = jest.fn();
+
+ await handler({ ACCOUNT_SID: '1234', AUTH_TOKEN: '2345' }, { rules: 'mockRules' }, mockCallback);
+
+ expect(mockCallback).toHaveBeenCalledWith(null, {
+ body: {
+ error: {
+ message: 'missing room_sid',
+ explanation: 'The room_sid parameter is missing.',
+ },
+ },
+ headers: { 'Content-Type': 'application/json' },
+ statusCode: 400,
+ });
+ });
+
+ it('should return a "missing rules" error when the rules array is absent', async () => {
+ const mockCallback = jest.fn();
+
+ await handler({ ACCOUNT_SID: '1234', AUTH_TOKEN: '2345' }, { room_sid: 'mockSid' }, mockCallback);
+
+ expect(mockCallback).toHaveBeenCalledWith(null, {
+ body: {
+ error: {
+ message: 'missing rules',
+ explanation: 'The rules parameter is missing.',
+ },
+ },
+ headers: { 'Content-Type': 'application/json' },
+ statusCode: 400,
+ });
+ });
+});
diff --git a/test/video-token-server.test.js b/test/serverless/functions/token.test.js
similarity index 75%
rename from test/video-token-server.test.js
rename to test/serverless/functions/token.test.js
index 90b2520..0524de1 100644
--- a/test/video-token-server.test.js
+++ b/test/serverless/functions/token.test.js
@@ -1,4 +1,4 @@
-const { handler } = require('../src/video-token-server');
+const { handler } = require('../../../src/serverless/functions/token');
const jwt = require('jsonwebtoken');
const { set } = require('lodash');
@@ -8,64 +8,23 @@ const mockCreateFunction = jest.fn();
const mockTwilioClient = set({}, 'video.rooms.create', mockCreateFunction);
+Date.now = () => 5;
+
const mockContext = {
ACCOUNT_SID: 'AC1234',
TWILIO_API_KEY_SID: 'SK1234',
TWILIO_API_KEY_SECRET: 'api_secret',
- API_PASSCODE: '123456',
- API_PASSCODE_EXPIRY: '10',
- DOMAIN_NAME: 'video-app-1234-5678-dev.twil.io',
ROOM_TYPE: 'group',
getTwilioClient: () => mockTwilioClient,
};
describe('the video-token-server', () => {
beforeEach(() => {
- Date.now = () => 5;
mockCreateFunction.mockImplementation(() => Promise.resolve());
});
- it('should return an "unauthorized" error when the passcode is incorrect', () => {
- handler(mockContext, { passcode: '9876543210', user_identity: 'test identity' }, callback);
-
- expect(callback).toHaveBeenCalledWith(null, {
- body: {
- error: {
- message: 'passcode incorrect',
- explanation: 'The passcode used to validate application users is incorrect.',
- },
- },
- headers: { 'Content-Type': 'application/json' },
- statusCode: 401,
- });
- });
-
- it('should return an "expired" error when the current time is past the API_PASSCODE_EXPIRY time', () => {
- Date.now = () => 15;
-
- handler(mockContext, { passcode: '12345612345678', user_identity: 'test identity' }, callback);
-
- expect(callback).toHaveBeenCalledWith(null, {
- body: {
- error: {
- message: 'passcode expired',
- explanation:
- 'The passcode used to validate application users has expired. Re-deploy the application to refresh the passcode.',
- },
- },
- headers: { 'Content-Type': 'application/json' },
- statusCode: 401,
- });
- });
-
it('should return an "invalid parameter" error when the create_room parameter is not a boolean', async () => {
- Date.now = () => 5;
-
- await handler(
- mockContext,
- { passcode: '12345612345678', user_identity: 'test identity', create_room: 'no thanks' },
- callback
- );
+ await handler(mockContext, { user_identity: 'test identity', create_room: 'no thanks' }, callback);
expect(callback).toHaveBeenCalledWith(null, {
body: {
@@ -80,7 +39,7 @@ describe('the video-token-server', () => {
});
it('should return a "missing user_identity" error when the "user_identity" parameter is not supplied', () => {
- handler(mockContext, { passcode: '12345612345678' }, callback);
+ handler(mockContext, {}, callback);
expect(callback).toHaveBeenCalledWith(null, {
body: {
@@ -95,7 +54,7 @@ describe('the video-token-server', () => {
});
it('should return a token when no room_name is supplied', async () => {
- await handler(mockContext, { passcode: '12345612345678', user_identity: 'test identity' }, callback);
+ await handler(mockContext, { user_identity: 'test identity' }, callback);
expect(callback).toHaveBeenCalledWith(null, {
body: { token: expect.any(String), room_type: 'group' },
@@ -118,11 +77,7 @@ describe('the video-token-server', () => {
describe('when passcode, room_name, and user_identity parameters are supplied', () => {
it('should return a valid token', async () => {
- await handler(
- mockContext,
- { passcode: '12345612345678', room_name: 'test-room', user_identity: 'test-user' },
- callback
- );
+ await handler(mockContext, { room_name: 'test-room', user_identity: 'test-user' }, callback);
expect(callback).toHaveBeenCalledWith(null, {
body: { token: expect.any(String), room_type: 'group' },
diff --git a/test/serverless/middleware/auth.test.js b/test/serverless/middleware/auth.test.js
new file mode 100644
index 0000000..fa73511
--- /dev/null
+++ b/test/serverless/middleware/auth.test.js
@@ -0,0 +1,59 @@
+const verifyPasscode = jest.requireActual('../../../src/serverless/middleware/auth');
+
+describe('the auth middleware', () => {
+ it('should return an "unauthorized" error when the passcode is incorrect', () => {
+ Date.now = () => 5;
+ const mockCallback = jest.fn();
+ verifyPasscode(
+ { API_PASSCODE: '123456', API_PASSCODE_EXPIRY: '10', DOMAIN_NAME: 'video-app-1234-5678-dev.twil.io' },
+ { passcode: '9876543210' },
+ mockCallback
+ );
+
+ expect(mockCallback).toHaveBeenCalledWith(null, {
+ body: {
+ error: {
+ message: 'passcode incorrect',
+ explanation: 'The passcode used to validate application users is incorrect.',
+ },
+ },
+ headers: { 'Content-Type': 'application/json' },
+ statusCode: 401,
+ });
+ });
+
+ it('should return an "expired" error when the current time is past the API_PASSCODE_EXPIRY time', () => {
+ Date.now = () => 15;
+ const mockCallback = jest.fn();
+
+ verifyPasscode(
+ { API_PASSCODE: '123456', API_PASSCODE_EXPIRY: '10', DOMAIN_NAME: 'video-app-1234-5678-dev.twil.io' },
+ { passcode: '12345612345678', user_identity: 'test identity' },
+ mockCallback
+ );
+
+ expect(mockCallback).toHaveBeenCalledWith(null, {
+ body: {
+ error: {
+ message: 'passcode expired',
+ explanation:
+ 'The passcode used to validate application users has expired. Re-deploy the application to refresh the passcode.',
+ },
+ },
+ headers: { 'Content-Type': 'application/json' },
+ statusCode: 401,
+ });
+ });
+
+ it('should not call the callback function when the passcode is correct and not expired', () => {
+ Date.now = () => 5;
+ const mockCallback = jest.fn();
+ verifyPasscode(
+ { API_PASSCODE: '123456', API_PASSCODE_EXPIRY: '10', DOMAIN_NAME: 'video-app-1234-5678-dev.twil.io' },
+ { passcode: '12345612345678' },
+ mockCallback
+ );
+
+ expect(mockCallback).not.toHaveBeenCalled();
+ });
+});
diff --git a/test/setupTests.js b/test/setupTests.js
index 8ae2918..14c10fe 100644
--- a/test/setupTests.js
+++ b/test/setupTests.js
@@ -20,3 +20,16 @@ class Response {
global.Twilio = require('twilio');
global.Twilio.Response = Response;
+
+const verifyPasscodePath = `${__dirname}/../src/serverless/middleware/auth.js`;
+
+global.Runtime = {
+ getAssets: () => ({
+ '/auth-handler.js': {
+ path: verifyPasscodePath,
+ },
+ }),
+};
+
+// Mocking this as a no-op since this function is tested in 'tests/serverless/middleware/auth.ts'.
+jest.doMock(verifyPasscodePath, () => () => {});