UNPKG

opentok

Version:
1,618 lines (1,526 loc) 62.2 kB
/* */ var expect = require('chai').expect; var nock = require('nock'); var _ = require('lodash'); var jwt = require('jsonwebtoken'); // Subject var OpenTok = require('../lib/opentok.js'); var Session = require('../lib/session.js'); var SipInterconnect = require('../lib/sipInterconnect.js'); var pkg = require('../package.json'); // Fixtures var apiKey = '123456'; var apiSecret = '1234567890abcdef1234567890abcdef1234567890'; var apiUrl = 'http://mymock.example.com'; // This is specifically concocted for these tests (uses fake apiKey/apiSecret above) var sessionId = '1_MX4xMjM0NTZ-flNhdCBNYXIgMTUgMTQ6NDI6MjMgUERUIDIwMTR-MC40OTAxMzAyNX4'; var badApiKey = 'badkey'; var badApiSecret = 'badsecret'; var goodSipUri = 'sip:siptesturl@tokbox.com'; var badSipUri = 'siptesturl@tokbox.com'; var defaultApiUrl = 'https://api.opentok.com'; var defaultTimeoutLength = 20000; // 20 seconds var recording = false; // Helpers var helpers = require('./helpers.js'); var validReply = JSON.stringify([ { session_id: 'SESSIONID', project_id: apiKey, partner_id: apiKey, create_dt: 'Fri Nov 18 15:50:36 PST 2016', media_server_url: '' } ]); var mockBroadcastObject = { id: 'fooId', sessionId: 'fooSessionId', projectId: 1234, createdAt: 1537477584724, updatedAt: 1537477584725, broadcastUrls: { hls: 'hlsUrl' }, maxDuration: 7200, resolution: '1280x720', event: 'broadcast', status: 'stopped' }; var mockListBroadcastsObject = { count: 1, items: [ { id: 'fooId', sessionId: 'fooSessionId', projectId: 1234, createdAt: 1537477584724, updatedAt: 1537477584725, broadcastUrls: { hls: 'hlsUrl' }, maxDuration: 7200, resolution: '1280x720', event: 'broadcast', status: 'stopped' } ] }; function validateBroadcastObject(broadcast, status) { expect(broadcast.id).to.equal('fooId'); expect(broadcast.sessionId).to.equal('fooSessionId'); expect(broadcast.projectId).to.equal(1234); expect(broadcast.createdAt).to.equal(1537477584724); expect(broadcast.updatedAt).to.equal(1537477584725); expect(broadcast.broadcastUrls.hls).to.equal('hlsUrl'); expect(broadcast.maxDuration).to.equal(7200); expect(broadcast.resolution).to.equal('1280x720'); expect(broadcast.status).to.equal(status || 'stopped'); expect(typeof broadcast.stop).to.equal('function'); } function validateListBroadcastsObject(broadcastListObject) { var broadcast; expect(broadcastListObject).to.be.an('array'); broadcast = broadcastListObject[0]; validateBroadcastObject(broadcast); } function validateTotalCount(totalCount) { expect(totalCount).to.be.a('number'); } function mockStreamRequest(sessId, streamId, status) { var body; if (!status) { body = JSON.stringify({ id: 'fooId', name: 'fooName', layoutClassList: ['fooClass'], videoType: 'screen' }); } nock('https://api.opentok.com') .get('/v2/project/APIKEY/session/' + sessId + '/stream/' + streamId) .reply(status || 200, body); } function mockListStreamsRequest(sessId, status) { var body; if (!status) { body = JSON.stringify({ count: 1, items: [ { id: 'fooId', name: 'fooName', layoutClassList: ['fooClass'], videoType: 'screen' } ] }); } nock('https://api.opentok.com') .get('/v2/project/123456/session/' + sessId + '/stream') .reply(status || 200, body); } function mockMuteStreamRequest(sessId, streamId, status) { nock('https://api.opentok.com') .post('/v2/project/123456/session/' + sessId + '/stream/' + streamId + '/mute') .reply(status || 200); } function mockMuteAllStreamRequest(sessId, status) { nock('https://api.opentok.com') .post('/v2/project/123456/session/' + sessId + '/mute') .reply(status || 200); } function mockDisableForceMuteRequest(sessId, status) { nock('https://api.opentok.com') .post('/v2/project/123456/session/' + sessId + '/mute') .reply(status || 200); } function mockListBroadcastsRequest(query, status) { var body; if (status) { body = JSON.stringify({ message: 'error message' }); } else { body = JSON.stringify(mockListBroadcastsObject); } nock('https://api.opentok.com') .get('/v2/project/APIKEY/broadcast') .query(query) .reply(status || 200, body); } nock.disableNetConnect(); if (recording) { // set these values before changing the above to true apiKey = ''; apiSecret = ''; // nock.enableNetConnect(); nock.recorder.rec(); } describe('OpenTok', function () { it('should initialize with a valid apiKey and apiSecret', function () { var opentok = new OpenTok(apiKey, apiSecret); expect(opentok).to.be.an.instanceof(OpenTok); expect(opentok.apiKey).to.be.equal(apiKey); expect(opentok.apiSecret).to.be.equal(apiSecret); expect(opentok.apiUrl).to.be.equal(defaultApiUrl); }); it('should initialize without `new`', function () { var opentok = OpenTok(apiKey, apiSecret); expect(opentok).to.be.an.instanceof(OpenTok); }); it('should not initialize with just an apiKey but no apiSecret', function () { expect(function () { var opentok = new OpenTok(apiKey); // eslint-disable-line }).to.throw(Error); }); it('should not initialize with incorrect type parameters', function () { expect(function () { var opentok = new OpenTok(new Date(), 'asdasdasdasdasd'); // eslint-disable-line }).to.throw(Error); expect(function () { opentok = new OpenTok(4, {}); // eslint-disable-line }).to.throw(Error); }); it('should cooerce a number for the apiKey', function () { var opentok = new OpenTok(parseInt(apiKey, 10), apiSecret); expect(opentok).to.be.an.instanceof(OpenTok); }); }); describe('JWT token', function describeJwtToken() { it('should not be expired', function (done) { var opentok = new OpenTok(apiKey, apiSecret); var expiration; var now; try { expiration = jwt.verify(opentok.generateJwt(), apiSecret, { issuer: apiKey }).exp; now = Math.floor(Date.now() / 1000); expect(expiration).to.be.above(now); done(); } catch (error) { done(error); } }); it('should have the apiKey set as the issuer', function (done) { var opentok = new OpenTok(apiKey, apiSecret); var issuer; try { issuer = jwt.verify(opentok.generateJwt(), apiSecret, { issuer: apiKey }).iss; expect(issuer).to.be.equal(apiKey); done(); } catch (error) { done(error); } }); // decoding with a valid secret is implicitly covered in the above tests it('should not decode with an invalid secret', function () { var opentok = new OpenTok(apiKey, apiSecret); expect(function () { jwt.verify(opentok.generateJwt(), 'invalid_secret', { issuer: apiKey }); }).to.throw(Error); }); }); describe('when initialized with an apiUrl', function () { beforeEach(function () { this.opentok = new OpenTok(apiKey, apiSecret, apiUrl); }); it('exposes the custom apiUrl', function () { expect(this.opentok.apiUrl).to.be.equal(apiUrl); }); it('sends its requests to the set apiUrl', function (done) { var scope = nock(apiUrl) .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/session/create', 'archiveMode=manual&p2p.preference=enabled') .reply(200, validReply, { server: 'nginx', date: 'Thu, 20 Mar 2014 06:35:24 GMT', 'content-type': 'application/json', connection: 'keep-alive', 'access-control-allow-origin': '*', 'strict-transport-security': 'max-age=31536000; includeSubdomains', 'content-length': '204' }); this.opentok.createSession(function (err, session) { if (err) { done(err); return; } expect(session).to.be.an.instanceof(Session); expect(session.sessionId).to.equal('SESSIONID'); scope.done(); done(err); }); }); }); describe('when initialized with a proxy', function () { beforeEach(function () { // TODO: remove temporary proxy value this.proxyUrl = 'http://localhost:8080'; this.opentok = new OpenTok(apiKey, apiSecret, { proxy: this.proxyUrl }); }); it('sends its requests through an http proxy', function (done) { var scope; this.timeout(10000); scope = nock('https://api.opentok.com:443') .post('/session/create', 'archiveMode=manual&p2p.preference=enabled') .reply(200, validReply, { server: 'nginx', date: 'Mon, 14 Jul 2014 04:26:35 GMT', 'content-type': 'application/json', connection: 'keep-alive', 'access-control-allow-origin': '*', 'x-tb-host': 'mantis402-oak.tokbox.com', 'content-length': '274' }); this.opentok.createSession(function (err) { scope.done(); done(err); }); }); }); describe('when initialized with a timeout', function () { beforeEach(function () { this.opentok = new OpenTok(apiKey, apiSecret, { timeout: 100 }); }); it('sends its requests with a timeout', function () { expect(this.opentok.client.c.request.timeout).to.equal(100); }); }); describe('when initialized without a timeout', function () { beforeEach(function () { this.opentok = new OpenTok(apiKey, apiSecret); }); it('sends its requests with 20000 timeout', function () { expect(this.opentok.client.c.request.timeout).to.equal(20000); }); }); describe('when a user agent addendum is needed', function () { beforeEach(function () { this.addendum = 'my-special-app'; this.opentok = new OpenTok(apiKey, apiSecret, { uaAddendum: this.addendum }); }); it('appends the addendum in a create session request', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/session/create', 'archiveMode=manual&p2p.preference=enabled') .reply(200, validReply, { server: 'nginx', date: 'Thu, 20 Mar 2014 06:35:24 GMT', 'content-type': 'application/json', connection: 'keep-alive', 'access-control-allow-origin': '*', 'strict-transport-security': 'max-age=31536000; includeSubdomains', 'content-length': '204' }); this.opentok.createSession(function (err) { if (err) { done(err); return; } scope.done(); done(err); }); }); it.skip('appends the addendum in an archiving request', function () { // TODO: }); }); describe.skip('when there is too much network latency', function () { beforeEach(function () { this.opentok = new OpenTok(apiKey, apiSecret); }); it('times out when the request takes longer than the default timeout', function (done) { // make sure the mocha test runner doesn't time out for at least as long as we are willing to // wait plus some reasonable amount of overhead time (100ms) var scope; this.timeout(defaultTimeoutLength + 100); scope = nock('https://api.opentok.com:443') .post('/session/create', 'archiveMode=manual&p2p.preference=enabled') .delayConnection(defaultTimeoutLength + 10) .reply(200, validReply, { server: 'nginx', date: 'Mon, 14 Jul 2014 04:26:35 GMT', 'content-type': 'application/json', connection: 'keep-alive', 'access-control-allow-origin': '*', 'x-tb-host': 'mantis402-oak.tokbox.com', 'content-length': '274' }); this.opentok.createSession(function (err) { expect(err).to.be.an.instanceof(Error); scope.done(); done(); }); }); }); describe('when initialized with bad credentials', function () { beforeEach(function () { this.opentok = new OpenTok(badApiKey, badApiSecret); }); describe('#createSession', function () { it('throws a client error', function (done) { var scope = nock('https://api.opentok.com:443') .post('/session/create', 'archiveMode=manual&p2p.preference=enabled') .reply( 403, JSON.stringify({ code: -1, message: 'No suitable authentication found' }), { server: 'nginx', date: 'Fri, 30 May 2014 19:37:12 GMT', 'content-type': 'application/json', connection: 'keep-alive', 'content-length': '56' } ); this.opentok.createSession(function (err) { expect(err).to.be.an.instanceof(Error); scope.done(); done(); }); }); }); }); describe('#createSession', function () { beforeEach(function () { this.opentok = new OpenTok(apiKey, apiSecret); }); it('creates a new session', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/session/create', 'archiveMode=manual&p2p.preference=enabled') .reply(200, validReply, { server: 'nginx', date: 'Thu, 20 Mar 2014 06:35:24 GMT', 'content-type': 'application/json', connection: 'keep-alive', 'access-control-allow-origin': '*', 'strict-transport-security': 'max-age=31536000; includeSubdomains', 'content-length': '204' }); // pass no options parameter this.opentok.createSession(function (err, session) { if (err) { done(err); return; } expect(session).to.be.an.instanceof(Session); expect(session.sessionId).to.equal('SESSIONID'); expect(session.mediaMode).to.equal('relayed'); scope.done(); done(); }); }); it('creates a media routed session', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/session/create', 'archiveMode=manual&p2p.preference=disabled') .reply(200, validReply, { server: 'nginx', date: 'Thu, 20 Mar 2014 06:35:24 GMT', 'content-type': 'application/json', connection: 'keep-alive', 'access-control-allow-origin': '*', 'strict-transport-security': 'max-age=31536000; includeSubdomains', 'content-length': '204' }); this.opentok.createSession( { mediaMode: 'routed' }, function (err, session) { if (err) { done(err); return; } expect(session).to.be.an.instanceof(Session); expect(session.sessionId).to.equal('SESSIONID'); expect(session.mediaMode).to.equal('routed'); scope.done(); done(err); } ); }); it('creates a media relayed session even if the media mode is invalid', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/session/create', 'archiveMode=manual&p2p.preference=enabled') .reply(200, validReply, { server: 'nginx', date: 'Thu, 20 Mar 2014 06:35:24 GMT', 'content-type': 'application/json', connection: 'keep-alive', 'access-control-allow-origin': '*', 'strict-transport-security': 'max-age=31536000; includeSubdomains', 'content-length': '204' }); this.opentok.createSession({ mediaMode: 'blah' }, function (err, session) { if (err) { done(err); return; } expect(session).to.be.an.instanceof(Session); expect(session.sessionId).to.equal('SESSIONID'); expect(session.mediaMode).to.equal('relayed'); scope.done(); done(err); }); }); it('creates a session with manual archive mode', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/session/create', 'archiveMode=manual&p2p.preference=disabled') .reply(200, validReply, { server: 'nginx', date: 'Thu, 20 Mar 2014 06:35:24 GMT', 'content-type': 'application/json', connection: 'keep-alive', 'access-control-allow-origin': '*', 'strict-transport-security': 'max-age=31536000; includeSubdomains', 'content-length': '204' }); this.opentok.createSession( { mediaMode: 'routed', archiveMode: 'manual' }, function (err, session) { if (err) { done(err); return; } expect(session).to.be.an.instanceof(Session); expect(session.sessionId).to.equal('SESSIONID'); expect(session.archiveMode).to.equal('manual'); scope.done(); done(err); } ); }); it('creates a session with always archive mode', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/session/create', 'archiveMode=always&p2p.preference=disabled') .reply(200, validReply, { server: 'nginx', date: 'Thu, 20 Mar 2014 06:35:24 GMT', 'content-type': 'application/json', connection: 'keep-alive', 'access-control-allow-origin': '*', 'strict-transport-security': 'max-age=31536000; includeSubdomains', 'content-length': '204' }); this.opentok.createSession( { mediaMode: 'routed', archiveMode: 'always' }, function (err, session) { if (err) { done(err); return; } expect(session).to.be.an.instanceof(Session); expect(session.sessionId).to.equal('SESSIONID'); expect(session.archiveMode).to.equal('always'); scope.done(); done(err); } ); }); it('creates a session with manual archive mode even if the archive mode is invalid', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/session/create', 'archiveMode=manual&p2p.preference=disabled') .reply(200, validReply, { server: 'nginx', date: 'Thu, 20 Mar 2014 06:35:24 GMT', 'content-type': 'application/json', connection: 'keep-alive', 'access-control-allow-origin': '*', 'strict-transport-security': 'max-age=31536000; includeSubdomains', 'content-length': '204' }); this.opentok.createSession( { mediaMode: 'routed', archiveMode: 'invalid' }, function (err, session) { if (err) { done(err); return; } expect(session).to.be.an.instanceof(Session); expect(session.sessionId).to.equal('SESSIONID'); expect(session.archiveMode).to.equal('manual'); scope.done(); done(err); } ); }); it('adds a location hint to the created session', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post( '/session/create', 'location=12.34.56.78&archiveMode=manual&p2p.preference=enabled' ) .reply(200, validReply, { server: 'nginx', date: 'Thu, 20 Mar 2014 06:35:24 GMT', 'content-type': 'application/json', connection: 'keep-alive', 'access-control-allow-origin': '*', 'strict-transport-security': 'max-age=31536000; includeSubdomains', 'content-length': '204' }); // passes location: '12.34.56.78' this.opentok.createSession( { location: '12.34.56.78' }, function (err, session) { if (err) { done(err); return; } expect(session).to.be.an.instanceof(Session); expect(session.sessionId).to.equal('SESSIONID'); expect(session.mediaMode).to.equal('relayed'); expect(session.location).to.equal('12.34.56.78'); scope.done(); done(err); } ); }); it('complains when the location value is not valid', function (done) { this.opentok.createSession( { location: 'not an ip address' }, function (err) { expect(err).to.be.an.instanceof(Error); done(); } ); }); it('complains when the archive mode is always and the media mode is routed', function (done) { this.opentok.createSession( { archiveMedia: 'always', mediaMode: 'routed' }, function (err) { expect(err).to.be.an.instanceof(Error); done(); } ); }); it('complains when there is no callback function', function () { expect(function () { this.opentok.createSession(); }).to.throw(Error); }); it('complains when a server error takes place', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/session/create', 'archiveMode=manual&p2p.preference=enabled') .reply(503, '', { server: 'nginx', date: 'Thu, 20 Mar 2014 06:35:24 GMT', 'content-type': 'text/xml', connection: 'keep-alive', 'access-control-allow-origin': '*', 'x-tb-host': 'mantis503-nyc.tokbox.com', 'content-length': '0' }); this.opentok.createSession(function (err) { expect(err).to.be.an.instanceof(Error); expect(err.message).to.contain('A server error occurred'); scope.done(); done(); }); }); it('returns a Session that can generate a token', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/session/create', 'archiveMode=manual&p2p.preference=enabled') .reply( 200, '[{"session_id":"' + sessionId + '","project_id":"' + apiKey + '","partner_id":"' + apiKey + '","create_dt":"Fri Nov 18 15:50:36 PST 2016","media_server_url":""}]', { server: 'nginx', date: 'Thu, 20 Mar 2014 06:35:24 GMT', 'content-type': 'application/json', connection: 'keep-alive', 'access-control-allow-origin': '*', 'strict-transport-security': 'max-age=31536000; includeSubdomains', 'content-length': '204' } ); // pass no options parameter this.opentok.createSession(function (err, session) { var token; if (err) { done(err); return; } scope.done(); token = session.generateToken(); expect(token).to.be.a('string'); done(err); }); }); it('should not modify the options object parameter', function (done) { var scope = nock('https://api.opentok.com:443') .filteringRequestBody(function () { return '*'; }) .post('/session/create', '*') .reply(200, validReply, { server: 'nginx', date: 'Thu, 20 Mar 2014 14:02:45 GMT', 'content-type': 'text/xml', connection: 'keep-alive', 'access-control-allow-origin': '*', 'x-tb-host': 'oms506-nyc.tokbox.com', 'content-length': '211' }); var options = { mediaMode: 'routed', archiveMode: 'manual' }; var optionsUntouched = _.clone(options); this.opentok.createSession(options, function (err) { if (err) { done(err); return; } scope.done(); expect(options).to.deep.equal(optionsUntouched); done(); }); }); }); describe('#generateToken', function () { beforeEach(function () { this.opentok = new OpenTok(apiKey, apiSecret); this.sessionId = sessionId; }); it('given a valid session, generates a token', function () { // call generateToken with no options var token = this.opentok.generateToken(this.sessionId); var decoded; expect(token).to.be.a('string'); expect(helpers.verifyTokenSignature(token, apiSecret)).to.be.true; decoded = helpers.decodeToken(token); expect(decoded.partner_id).to.equal(apiKey); expect(decoded.create_time).to.exist; expect(decoded.nonce).to.exist; }); it('assigns a role in the token', function () { // expects one with no role defined to assign "publisher" var defaultRoleToken = this.opentok.generateToken(this.sessionId); var decoded; var subscriberToken; expect(defaultRoleToken).to.be.a('string'); expect(helpers.verifyTokenSignature(defaultRoleToken, apiSecret)).to.be .true; decoded = helpers.decodeToken(defaultRoleToken); expect(decoded.role).to.equal('publisher'); // expects one with a valid role defined to set it subscriberToken = this.opentok.generateToken(this.sessionId, { role: 'subscriber' }); expect(subscriberToken).to.be.a('string'); expect(helpers.verifyTokenSignature(subscriberToken, apiSecret)).to.be.true; decoded = helpers.decodeToken(subscriberToken); expect(decoded.role).to.equal('subscriber'); // expects one with an invalid role to complain expect(function () { this.opentok.generateToken(this.sessionId, { role: 5 }); }).to.throw(Error); }); it('sets an expiration time for the token', function () { var now = Math.round(new Date().getTime() / 1000); var delta = 10; var decoded; var expireTime; // expects a token with no expiration time to assign 1 day var inOneDay = now + (60 * 60 * 24); var defaultExpireToken = this.opentok.generateToken(this.sessionId); var oneHourToken; var inOneHour; var oneHourAgo; var fractionalExpireTime; var roundedToken; expect(defaultExpireToken).to.be.a('string'); expect(helpers.verifyTokenSignature(defaultExpireToken, apiSecret)).to.be .true; decoded = helpers.decodeToken(defaultExpireToken); expireTime = parseInt(decoded.expire_time, 10); expect(expireTime).to.be.closeTo(inOneDay, delta); // expects a token with an expiration time to have it inOneHour = now + (60 * 60); oneHourToken = this.opentok.generateToken(this.sessionId, { expireTime: inOneHour }); expect(oneHourToken).to.be.a('string'); expect(helpers.verifyTokenSignature(oneHourToken, apiSecret)).to.be.true; decoded = helpers.decodeToken(oneHourToken); expireTime = parseInt(decoded.expire_time, 10); expect(expireTime).to.be.closeTo(inOneHour, delta); // expects a token with an invalid expiration time to complain expect(function () { this.opentok.generateToken(this.sessionId, { expireTime: 'not a time' }); }).to.throw(Error); oneHourAgo = now - (60 * 60); expect(function () { this.opentok.generateToken(this.sessionId, { expireTime: oneHourAgo }); }).to.throw(Error); // rounds down fractional expiration time fractionalExpireTime = now + 60.5; roundedToken = this.opentok.generateToken(this.sessionId, { expireTime: fractionalExpireTime }); expect(helpers.verifyTokenSignature(roundedToken, apiSecret)).to.be.true; decoded = helpers.decodeToken(roundedToken); expect(decoded.expire_time).to.equal(Math.round(fractionalExpireTime).toString()); }); it('sets initial layout class list in the token', function () { var layoutClassList = ['focus', 'inactive']; var singleLayoutClass = 'focus'; var layoutBearingToken = this.opentok.generateToken(this.sessionId, { initialLayoutClassList: layoutClassList }); var decoded = helpers.decodeToken(layoutBearingToken); var singleLayoutBearingToken = this.opentok.generateToken(this.sessionId, { initialLayoutClassList: singleLayoutClass }); expect(layoutBearingToken).to.be.a('string'); expect(helpers.verifyTokenSignature(layoutBearingToken, apiSecret)).to.be .true; expect(decoded.initial_layout_class_list).to.equal(layoutClassList.join(' ')); expect(singleLayoutBearingToken).to.be.a('string'); expect(helpers.verifyTokenSignature(singleLayoutBearingToken, apiSecret)).to .be.true; decoded = helpers.decodeToken(singleLayoutBearingToken); expect(decoded.initial_layout_class_list).to.equal(singleLayoutClass); // NOTE: ignores invalid options instead of throwing an error, except if its too long }); it('complains if the sessionId is not valid', function () { expect(function () { this.opentok.generateToken(); }).to.throw(Error); }); it('sets connection data in the token', function () { // expects a token with a connection data to have it var sampleData = 'name=Johnny'; var decoded; var dataBearingToken = this.opentok.generateToken(this.sessionId, { data: sampleData }); expect(dataBearingToken).to.be.a('string'); expect(helpers.verifyTokenSignature(dataBearingToken, apiSecret)).to.be .true; decoded = helpers.decodeToken(dataBearingToken); expect(decoded.connection_data).to.equal(sampleData); // expects a token with invalid connection data to complain expect(function () { this.opentok.generateToken(this.sessionId, { data: { dont: 'work' } }); }).to.throw(Error); expect(function () { this.opentok.generateToken(this.sessionId, { data: Array(2000).join('a') // 1999 char string of all 'a's }); }).to.throw(Error); }); it('complains if the sessionId is not valid', function () { expect(function () { this.opentok.generateToken(); }).to.throw(Error); expect(function () { this.opentok.generateToken('blahblahblah'); }).to.throw(Error); }); it('contains a unique nonce', function () { // generate a few and show the nonce exists each time and that they are different var tokens = [ this.opentok.generateToken(this.sessionId), this.opentok.generateToken(this.sessionId) ]; var nonces = _.map(tokens, function (token) { return helpers.decodeToken(token).nonce; }); expect(_.uniq(nonces)).to.have.length(nonces.length); }); it('does not modify passed in options', function () { var options = { data: 'test' }; var optionsUntouched = _.clone(options); this.opentok.generateToken(this.sessionId, options); expect(options).to.deep.equal(optionsUntouched); }); }); describe('#dial', function () { beforeEach(function () { this.opentok = new OpenTok(apiKey, apiSecret); this.sessionId = sessionId; this.token = this.opentok.generateToken(this.sessionId); }); it('dials a SIP gateway and adds a stream', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/v2/project/123456/dial', { sessionId: this.sessionId, token: this.token, sip: { uri: goodSipUri } }) .reply(200, { id: 'CONFERENCEID', connectionId: 'CONNECTIONID', streamId: 'STREAMID' }); this.opentok.dial( this.sessionId, this.token, goodSipUri, function (err, sipCall) { if (err) { done(err); return; } expect(sipCall).to.be.an.instanceof(SipInterconnect); expect(sipCall.id).to.equal('CONFERENCEID'); expect(sipCall.streamId).to.equal('STREAMID'); expect(sipCall.connectionId).to.equal('CONNECTIONID'); scope.done(); done(err); } ); }); it('dials a SIP gateway and adds a stream with custom headers', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/v2/project/123456/dial', { sessionId: this.sessionId, token: this.token, sip: { uri: goodSipUri, headers: { someKey: 'someValue' } } }) .reply(200, { id: 'CONFERENCEID', connectionId: 'CONNECTIONID', streamId: 'STREAMID' }); this.opentok.dial( this.sessionId, this.token, goodSipUri, { headers: { someKey: 'someValue' } }, function (err, sipCall) { if (err) { done(err); return; } expect(sipCall).to.be.an.instanceof(SipInterconnect); expect(sipCall.id).to.equal('CONFERENCEID'); expect(sipCall.streamId).to.equal('STREAMID'); expect(sipCall.connectionId).to.equal('CONNECTIONID'); scope.done(); done(err); } ); }); it('dials a SIP gateway and adds a stream with authentication', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/v2/project/123456/dial', { sessionId: this.sessionId, token: this.token, sip: { uri: goodSipUri, auth: { username: 'someUsername', password: 'somePassword' } } }) .reply(200, { id: 'CONFERENCEID', connectionId: 'CONNECTIONID', streamId: 'STREAMID' }); this.opentok.dial( this.sessionId, this.token, goodSipUri, { auth: { username: 'someUsername', password: 'somePassword' } }, function (err, sipCall) { if (err) { done(err); return; } expect(sipCall).to.be.an.instanceof(SipInterconnect); expect(sipCall.id).to.equal('CONFERENCEID'); expect(sipCall.streamId).to.equal('STREAMID'); expect(sipCall.connectionId).to.equal('CONNECTIONID'); scope.done(); done(err); } ); }); it('dials a SIP gateway and adds an encrypted media stream', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/v2/project/123456/dial', { sessionId: this.sessionId, token: this.token, sip: { uri: goodSipUri, secure: true } }) .reply(200, { id: 'CONFERENCEID', connectionId: 'CONNECTIONID', streamId: 'STREAMID' }); this.opentok.dial( this.sessionId, this.token, goodSipUri, { secure: true }, function (err, sipCall) { if (err) { done(err); return; } expect(sipCall).to.be.an.instanceof(SipInterconnect); expect(sipCall.id).to.equal('CONFERENCEID'); expect(sipCall.streamId).to.equal('STREAMID'); expect(sipCall.connectionId).to.equal('CONNECTIONID'); scope.done(); done(err); } ); }); it('dials a SIP gateway and adds a from field', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/v2/project/123456/dial', { sessionId: this.sessionId, token: this.token, sip: { uri: goodSipUri, from: '15551115555' } }) .reply(200, { id: 'CONFERENCEID', connectionId: 'CONNECTIONID', streamId: 'STREAMID' }); this.opentok.dial( this.sessionId, this.token, goodSipUri, { from: '15551115555' }, function (err, sipCall) { if (err) { done(err); return; } expect(sipCall).to.be.an.instanceof(SipInterconnect); expect(sipCall.id).to.equal('CONFERENCEID'); expect(sipCall.streamId).to.equal('STREAMID'); expect(sipCall.connectionId).to.equal('CONNECTIONID'); scope.done(); done(); } ); }); it('dials DTMF signal to every connection in the session', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/v2/project/123456/dial', { sessionId: this.sessionId, token: this.token, sip: { uri: goodSipUri, headers: { someKey: 'someValue' } } }) .reply(200, { id: 'CONFERENCEID', connectionId: 'CONNECTIONID', streamId: 'STREAMID' }); var dialDTMFScope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post( '/v2/project/<%apiKey%>/session/<%sessionId%>/play-dtmf' .replace(/<%apiKey%>/g, apiKey) .replace(/<%sessionId%>/g, this.sessionId), { digits: '6' } ) .reply(200, {}); var self = this; this.opentok.dial( this.sessionId, this.token, goodSipUri, { headers: { someKey: 'someValue' } }, function (err, sipCall) { if (err) { done(err); return; } expect(sipCall).to.be.an.instanceof(SipInterconnect); expect(sipCall.id).to.equal('CONFERENCEID'); expect(sipCall.streamId).to.equal('STREAMID'); expect(sipCall.connectionId).to.equal('CONNECTIONID'); scope.done(); self.opentok.playDTMF(self.sessionId, null, '6', function (error) { expect(error).to.equal(null); dialDTMFScope.done(); done(error); }); } ); }); it('dials DTMF signal to specific connection', function (done) { var scope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post('/v2/project/123456/dial', { sessionId: this.sessionId, token: this.token, sip: { uri: goodSipUri, headers: { someKey: 'someValue' } } }) .reply(200, { id: 'CONFERENCEID', connectionId: 'CONNECTIONID', streamId: 'STREAMID' }); var dialDTMFScope = nock('https://api.opentok.com:443') .matchHeader('x-opentok-auth', function (value) { try { jwt.verify(value, apiSecret, { issuer: apiKey }); return true; } catch (error) { done(error); return false; } }) .matchHeader('user-agent', new RegExp('OpenTok-Node-SDK/' + pkg.version)) .post( '/v2/project/<%apiKey%>/session/<%sessionId%>/connection/<%connectionId%>/play-dtmf' .replace(/<%apiKey%>/g, apiKey) .replace(/<%sessionId%>/g, this.sessionId) .replace(/<%connectionId%>/g, 'CONNECTIONID'), { digits: '6' } ) .reply(200, {}); var self = this; this.opentok.dial( this.sessionId, this.token, goodSipUri, { headers: { someKey: 'someValue' } }, function (err, sipCall) { if (err) { done(err); return; } expect(sipCall).to.be.an.instanceof(SipInterconnect); expect(sipCall.id).to.equal('CONFERENCEID'); expect(sipCall.streamId).to.equal('STREAMID'); expect(sipCall.connectionId).to.equal('CONNECTIONID'); scope.done(); self.opentok.playDTMF( self.sessionId, sipCall.connectionId, '6', function (error) { expect(error).to.equal(null); dialDTMFScope.done(); done(error); } ); } ); }); it('complains if sessionId, token, SIP URI, or callback are missing or invalid', function () { // Missing all params expect(function () { this.opentok.dial(); }).to.throw(Error); // Bad sessionId expect(function () { this.opentok.dial('blahblahblah'); }).to.throw(Error); // Missing token expect(function () { this.opentok.dial(this.sessionId); }).to.throw(Error); // Bad token expect(function () { this.opentok.dial(this.sessionId, 'blahblahblah'); }).to.throw(Error); // Missing SIP URI expect(function () { this.opentok.dial(this.sessionId, this.token); }).to.throw(Error); // Bad SIP URI expect(function () { this.opentok.dial(this.sessionId, this.token, badSipUri); }).to.throw(Error); // Bad sessionId, working token and SIP URI expect(function () { this.opentok.dial('someWrongSessionId', this.token, goodSipUri); }).to.throw(Error); // Good sessionId, bad token and good SIP URI expect(function () { this.opentok.dial(this.sessionId, 'blahblahblah', goodSipUri); }).to.throw(Error); // Good sessionId, good token, good SIP URI, null options, missing callback func expect(function () { this.opentok.dial(this.sessionId, this.token, goodSipUri, null); }).to.throw(Error); }); it('does not modify passed in options', function () { var options = { data: 'test' }; var optionsUntouched = _.clone(options); this.opentok.dial( this.sessionId, this.token, 'sip:testsipuri@tokbox.com', options, function () { expect(options).to.deep.equal(optionsUntouched); } ); }); }); describe('#startBroadcast', function () { var opentok = new OpenTok('APIKEY', 'APISECRET'); var SESSIONID = '1_MX4xMDB-MTI3LjAuMC4xflR1ZSBKYW4gMjggMTU6NDg6NDAgUFNUIDIwMTR-MC43NjAyOTYyfg'; var options = { outputs: { hls: {} } }; function mockStartBroadcastRequest(sessId, status, optionalBody) { var body; var broadcastObj; if (!status) { broadcastObj = _.clone(mockBroadcastObject); broadcastObj.status = 'started'; body = JSON.stringify(broadcastObj); } nock('https://api.opentok.com') .post('/v2/project/APIKEY/broadcast') .reply(status || 200, body || optionalBody); } afterEach(function () { nock.cleanAll(); }); it('succeeds given valid parameters', function (done) { mockStartBroadcastRequest(SESSIONID); opentok.startBroadcast(SESSIONID, options, function (err, broadcast) { validateBroadcastObject(broadcast, 'started'); done(); }); }); it('results in error if no callback method provided', function (done) { mockStartBroadcastRequest(SESSIONID); try { opentok.startBroadcast(SESSIONID, {}); } catch (err) { expect(err.message).to.equal('No callback given to startBroadcast'); done(); } }); it('results in error if no session ID provided', function (done) { mockStartBroadcastRequest(SESSIONID); opentok.startBroadcast(null, options, function (err) { expect(err.message).to.equal('No sessionId given to startBroadcast'); done(); }); }); it('results in error if no options provided', function (done) { mockStartBroadcastRequest(SESSIONID); opentok.startBroadcast(SESSIONID, null, function (err) { expect(err.message).to.equal('No options given to startBroadcast'); done(); }); }); it('results in error if both dvr and lowLatency is provided for options', function (done) { mockStartBroadcastRequest(SESSIONID); opentok.startBroadcast( SESSIONID, { outputs: { hls: { dvr: true, lowLatency: true } } }, function (err) { expect(err.message).to.equal('Cannot set both dvr and lowLatency on HLS'); done(); } ); }); it('results in error a response other than 200', function (done) { mockStartBroadcastRequest(SESSIONID, 400, { error: 'remote error message' }); opentok.startBroadcast(SESSIONID, options, function (err, broadcast) { expect(err.message).to.equal('Failed to start broadcast. Error: (400) {"error":"remote error message"}'); expect(broadcast).to.be.undefined; done(); }); }); }); describe('#patchBroadcast', function () { var opentok = new OpenTok('APIKEY', 'APISECRET'); var BROADCAST_ID = 'BROADCASTID'; var STREAM_ID = 'STREAMID'; var addStreamOptions = { addStream: 'abc123' }; var removeStreamOptions = { removeStream: 'abc123' }; var broadcastOptions = { hasAudio: true, hasVideo: true }; function mockPatchBroadcastRequest(broadcastId, optionalBody, status) { nock('https://api.opentok.com') .patch('/v2/project/APIKEY/broadcast/' + broadcastId + '/streams') .reply(200 || status, optionalBody); } afterEach(function () { nock.cleanAll(); }); it('patches broadcast given addStream', function (done) { mockPatchBroadcastRequest(BROADCAST_ID, addStreamOptions); opentok.addBroadcastStream( BROADCAST_ID, STREAM_ID, broadcastOptions, function (err) { expect(err).to.be.null; done(); } ); }); it('patches broadcast given removeStream', function (done) { mockPatchBroadcastRequest(BROADCAST_ID, removeStreamOptions); opentok.removeBroadcastStream(BROADCAST_ID, STREAM_ID, function (err) { expect(err).to.be.null; done(); }); }); it('fails to patch on emtpy function call', function () { opentok.addBroadcastStream(BR