diff --git a/tests/peerconnection.js b/tests/peerconnection.js index 982a48c2..a8531354 100644 --- a/tests/peerconnection.js +++ b/tests/peerconnection.js @@ -783,7 +783,6 @@ describe('PeerConnection', () => { const eIceServers = 'iceServers'; const eIss = 'this is iss'; const eSDP = 'sdp'; - const messagesToRegisterFor = ['messagesToRegisterFor-foo', 'messagesToRegisterFor-bar']; let context = null; let version = null; @@ -825,7 +824,6 @@ describe('PeerConnection', () => { eCallSid, eConstraints, eIceServers, - messagesToRegisterFor, callback, ); }); @@ -867,7 +865,7 @@ describe('PeerConnection', () => { assert(version.createOffer.calledWithExactly(undefined, undefined, {audio: true}, sinon.match.func, sinon.match.func)); assert.equal(callback.called, false); assert(context.pstream.invite.calledOnce); - assert(context.pstream.invite.calledWithExactly(eSDP, eCallSid, true, eParams, messagesToRegisterFor)); + assert(context.pstream.invite.calledWithExactly(eSDP, eCallSid, true, eParams)); assert(version.getSDP.calledOnce); assert(version.getSDP.calledWithExactly()); assert(context.pstream.on.calledWithExactly('answer', sinon.match.func)); @@ -884,7 +882,7 @@ describe('PeerConnection', () => { assert(version.createOffer.calledWithExactly(undefined, undefined, {audio: true}, sinon.match.func, sinon.match.func)); assert.equal(callback.called, false); assert(context.pstream.invite.calledOnce); - assert(context.pstream.invite.calledWithExactly(eSDP, eCallSid, true, eParams, messagesToRegisterFor)); + assert(context.pstream.invite.calledWithExactly(eSDP, eCallSid, true, eParams)); assert(version.getSDP.calledOnce); assert(version.getSDP.calledWithExactly()); assert(context.pstream.on.calledWithExactly('answer', sinon.match.func)); diff --git a/tests/pstream.js b/tests/pstream.js index 40103f04..41d883a2 100644 --- a/tests/pstream.js +++ b/tests/pstream.js @@ -249,6 +249,47 @@ describe('PStream', () => { }); }); + describe('sendMessage', () => { + const callsid = 'testcallsid'; + const content = { foo: 'content' }; + const messagetype = 'user-defined-message'; + const voiceeventsid = 'testvoiceeventsid'; + + it('should send a message with the provided info', () => { + pstream.sendMessage(callsid, content, undefined, messagetype, voiceeventsid); + assert.equal(pstream.transport.send.callCount, 1); + console.log(pstream.transport.send.args[0][0]) + assert.deepEqual(JSON.parse(pstream.transport.send.args[0][0]), { + type: 'message', + version:EXPECTED_PSTREAM_VERSION, + payload: { + callsid, + content, + contenttype: 'application/json', + messagetype, + voiceeventsid, + } + }); + }); + + it('should override contenttype', () => { + pstream.sendMessage(callsid, content, 'text/plain', messagetype, voiceeventsid); + assert.equal(pstream.transport.send.callCount, 1); + console.log(pstream.transport.send.args[0][0]) + assert.deepEqual(JSON.parse(pstream.transport.send.args[0][0]), { + type: 'message', + version:EXPECTED_PSTREAM_VERSION, + payload: { + callsid, + content, + contenttype: 'text/plain', + messagetype, + voiceeventsid, + } + }); + }); + }); + describe('destroy', () => { it('should return this', () => { assert.equal(pstream.destroy(), pstream); @@ -315,24 +356,19 @@ describe('PStream', () => { ]], ['invite', [ { - args: ['bar', 'foo', true, '', []], - payload: { callsid: 'foo', sdp: 'bar', preflight: true, twilio: {}, registerFor: [] }, + args: ['bar', 'foo', true, ''], + payload: { callsid: 'foo', sdp: 'bar', preflight: true, twilio: {} }, scenario: 'called with empty params' }, { - args: ['bar', 'foo', true, 'baz=zee&foo=2', []], - payload: { callsid: 'foo', sdp: 'bar', preflight: true, twilio: { params: 'baz=zee&foo=2' }, registerFor: [] }, + args: ['bar', 'foo', true, 'baz=zee&foo=2'], + payload: { callsid: 'foo', sdp: 'bar', preflight: true, twilio: { params: 'baz=zee&foo=2' } }, scenario: 'called with non-empty params' }, { - args: ['bar', 'foo', false, '', []], - payload: { callsid: 'foo', sdp: 'bar', preflight: false, twilio: {}, registerFor: [] }, + args: ['bar', 'foo', false, ''], + payload: { callsid: 'foo', sdp: 'bar', preflight: false, twilio: {} }, scenario: 'called with preflight = false' - }, - { - args: ['bar', 'foo', false, '', ['registerFor-foo', 'registerFor-bar']], - payload: { callsid: 'foo', sdp: 'bar', preflight: false, twilio: {}, registerFor: ['registerFor-foo', 'registerFor-bar'] }, - scenario: 'passing events to "registerFor"' } ]], ['answer', [ diff --git a/tests/unit/call.ts b/tests/unit/call.ts index a5cf9ef6..3e5b3793 100644 --- a/tests/unit/call.ts +++ b/tests/unit/call.ts @@ -26,6 +26,8 @@ describe('Call', function() { let soundcache: Map; let voiceEventSidGenerator: () => string; + const wait = (timeout?: number) => new Promise(r => setTimeout(r, timeout || 0)); + const MediaHandler = () => { mediaHandler = createEmitterStub(require('../../lib/twilio/rtc/peerconnection')); mediaHandler.setInputTracksFromStream = sinon.spy((rejectCode?: number) => { @@ -520,7 +522,7 @@ describe('Call', function() { return p; }); - mediaHandler.makeOutgoingCall = sinon.spy((a: any, b: any, c: any, d: any, e: any, f: any, _callback: Function) => { + mediaHandler.makeOutgoingCall = sinon.spy((a: any, b: any, c: any, d: any, e: any, _callback: Function) => { callback = _callback; }); }); @@ -544,14 +546,6 @@ describe('Call', function() { }); }); - it('should call mediaHandler.makeOutgoingCall with an array of messages to `messagesToRegisterFor`', () => { - const messagesToRegisterFor = ['messagesToRegisterFor-foo', 'messagesToRegisterFor-bar'] as any; - conn.accept({ messagesToRegisterFor }); - return wait.then(() => { - assert.deepEqual(mediaHandler.makeOutgoingCall.args[0][5], messagesToRegisterFor); - }); - }); - context('when the success callback is called', () => { it('should publish an accepted-by-remote event', () => { conn.accept(); @@ -988,6 +982,15 @@ describe('Call', function() { }); describe('.sendMessage()', () => { + let message: Call.Message; + + beforeEach(() => { + message = { + content: { foo: 'foo' }, + messageType: Call.MessageType.UserDefinedMessage, + }; + }); + context('when messageType is invalid', () => { [ undefined, @@ -998,7 +1001,7 @@ describe('Call', function() { ].forEach((messageType: any) => { it(`should throw on ${JSON.stringify(messageType)}`, () => { assert.throws( - () => conn.sendMessage(messageType, {}), + () => conn.sendMessage({ ...message, messageType }), new InvalidArgumentError( '`messageType` must be an enumeration value of ' + '`Call.MessageType` or a string.', @@ -1006,62 +1009,67 @@ describe('Call', function() { ); }); }); + }); - context('when messageType is valid', () => { - ['foo', 'bar'].forEach((messageType: string) => { - it(`should not throw on '${messageType}'`, () => { - conn['_status'] = Call.State.Open; - conn.parameters.CallSid = 'foobar'; - assert.doesNotThrow(() => { - conn.sendMessage(messageType as Call.MessageType, {}); - }); - }); + context('when content is invalid', () => { + [ + undefined, + null, + ].forEach((content: any) => { + it(`should throw on ${JSON.stringify(content)}`, () => { + assert.throws( + () => conn.sendMessage({ ...message, content }), + new InvalidArgumentError('`content` is empty'), + ); }); }); + }); - it('should throw if pstream is unavailable', () => { - // @ts-ignore - conn._pstream = null; - assert.throws( - () => conn.sendMessage('foobar' as Call.MessageType, {}), - new InvalidStateError( - 'Could not send CallMessage; Signaling channel is disconnected', - ), - ); - }); + it('should throw if pstream is unavailable', () => { + // @ts-ignore + conn._pstream = null; + assert.throws( + () => conn.sendMessage(message), + new InvalidStateError( + 'Could not send CallMessage; Signaling channel is disconnected', + ), + ); + }); - it('should invoke pstream.sendMessage', () => { - conn['_status'] = Call.State.Open; - const mockCallSid = conn.parameters.CallSid = 'foobar-callsid'; - const mockVoiceEventSid = 'foobar-voice-event-sid'; - const mockContent = {}; - conn.sendMessage(Call.MessageType.UserDefinedMessage, mockContent); - sinon.assert.calledOnceWithExactly( - pstream.sendMessage, - mockCallSid, - mockVoiceEventSid, - Call.MessageType.UserDefinedMessage, - mockContent, - ); - }); + it('should throw if the call sid is not set', () => { + assert.throws( + () => conn.sendMessage(message), + new InvalidStateError( + 'Could not send CallMessage; Call has no CallSid', + ), + ); + }); - it('should generate a voiceEventSid', () => { - conn['_status'] = Call.State.Open; - conn.parameters.CallSid = 'foobar-callsid'; - conn.sendMessage(Call.MessageType.UserDefinedMessage, {}); - sinon.assert.calledOnceWithExactly( - voiceEventSidGenerator as sinon.SinonStub, - ); - }); + it('should invoke pstream.sendMessage', () => { + const mockCallSid = conn.parameters.CallSid = 'foobar-callsid'; + conn.sendMessage(message); + sinon.assert.calledOnceWithExactly( + pstream.sendMessage, + mockCallSid, + message.content, + undefined, + Call.MessageType.UserDefinedMessage, + 'foobar-voice-event-sid', + ); + }); - it('should throw if the call sid is not set', () => { - assert.throws( - () => conn.sendMessage(Call.MessageType.UserDefinedMessage, {}), - new InvalidStateError( - 'Could not send CallMessage; Call has no CallSid', - ), - ); - }); + it('should return voiceEventSid', () => { + conn.parameters.CallSid = 'foobar-callsid'; + const sid = conn.sendMessage(message); + assert.strictEqual(sid, 'foobar-voice-event-sid'); + }); + + it('should generate a voiceEventSid', () => { + conn.parameters.CallSid = 'foobar-callsid'; + conn.sendMessage(message); + sinon.assert.calledOnceWithExactly( + voiceEventSidGenerator as sinon.SinonStub, + ); }); }); @@ -1692,44 +1700,144 @@ describe('Call', function() { }); }); - describe('pstream.message event', () => { - const wait = (timeout?: number) => new Promise(r => { - setTimeout(r, timeout || 0); - clock.tick(0); - }); - + describe('pstream.ack event', () => { const mockcallsid = 'mock-callsid'; + let mockMessagePayload: any; + let mockAckPayload: any; beforeEach(() => { + mockMessagePayload = { + content: { + foo: 'bar', + }, + contentType: 'application/json', + messageType: 'foo-bar', + }; + + mockAckPayload = { + acktype: 'message', + callsid: mockcallsid, + voiceeventsid: 'mockvoiceeventsid', + }; + conn.parameters = { ...conn.parameters, CallSid: mockcallsid, }; + clock.restore(); }); - it('emits the payload', async () => { - const mockPayload = { - callsid: mockcallsid, - content: { - foo: 'bar', - }, - contenttype: 'application/json', - messagetype: 'foo-bar', - voiceeventsid: 'mock-voiceeventsid-foobar', - }; + it('should emit messageSent', async () => { + const sid = conn.sendMessage(mockMessagePayload); + const payloadPromise = new Promise((resolve) => { + conn.on('messageSent', resolve); + }); + pstream.emit('ack', { ...mockAckPayload, voiceeventsid: sid }); + assert.deepEqual(await payloadPromise, { ...mockMessagePayload, voiceEventSid: sid }); + }); + + it('should ignore ack when callSids do not match', async () => { + const sid = conn.sendMessage(mockMessagePayload); + const payloadPromise = new Promise((resolve, reject) => { + conn.on('messageSent', reject); + }); + pstream.emit('ack', { ...mockAckPayload, voiceeventsid: sid, callsid: 'foo' }); + await Promise.race([ + wait(1), + payloadPromise, + ]); + }); + + it('should not emit messageSent when acktype is not `message`', async () => { + const sid = conn.sendMessage(mockMessagePayload); + const payloadPromise = new Promise((resolve, reject) => { + conn.on('messageSent', reject); + }); + pstream.emit('ack', { ...mockAckPayload, voiceeventsid: sid, acktype: 'foo' }); + await Promise.race([ + wait(1), + payloadPromise, + ]); + }); + + it('should not emit messageSent if voiceEventSid was not previously sent', async () => { + const sid = conn.sendMessage(mockMessagePayload); + const payloadPromise = new Promise((resolve, reject) => { + conn.on('messageSent', reject); + }); + pstream.emit('ack', { ...mockAckPayload, voiceeventsid: 'foo' }); + await Promise.race([ + wait(1), + payloadPromise, + ]); + }); + it('should emit messageSent only once', async () => { + const sid = conn.sendMessage(mockMessagePayload); + const handler = sinon.stub(); const payloadPromise = new Promise((resolve) => { - conn.on('message', resolve); + conn.on('messageSent', () => { + handler(); + resolve(null); + }); }); + pstream.emit('ack', { ...mockAckPayload, voiceeventsid: sid }); + pstream.emit('ack', { ...mockAckPayload, voiceeventsid: sid }); + await payloadPromise; + sinon.assert.calledOnce(handler); + }); - pstream.emit('message', mockPayload); + it('should not emit messageSent after an error', async () => { + const sid = conn.sendMessage(mockMessagePayload); + const payloadPromise = new Promise((resolve, reject) => { + conn.on('messageSent', reject); + }); + pstream.emit('error', { callsid: mockcallsid, voiceeventsid: sid }); + pstream.emit('ack', { ...mockAckPayload, voiceeventsid: sid }); + await Promise.race([ + wait(1), + payloadPromise, + ]); + }); + + it('should ignore errors with different callsid', async () => { + const sid = conn.sendMessage(mockMessagePayload); + const payloadPromise = new Promise((resolve) => { + conn.on('messageSent', resolve); + }); + pstream.emit('error', { callsid: 'foo', voiceeventsid: sid }); + pstream.emit('ack', { ...mockAckPayload, voiceeventsid: sid }); + await payloadPromise; + }); - assert.deepEqual(mockPayload, await payloadPromise); + it('should ignore errors with different voiceeventsid', async () => { + const sid = conn.sendMessage(mockMessagePayload); + const payloadPromise = new Promise((resolve) => { + conn.on('messageSent', resolve); + }); + pstream.emit('error', { callsid: mockcallsid, voiceeventsid: 'foo' }); + pstream.emit('ack', { ...mockAckPayload, voiceeventsid: sid }); + await payloadPromise; }); - it('ignores messages when callSids do not match', async () => { - const mockPayload = { - callsid: 'mock-callsid-foobar', + it('should ignore errors with missing voiceeventsid', async () => { + const sid = conn.sendMessage(mockMessagePayload); + const payloadPromise = new Promise((resolve) => { + conn.on('messageSent', resolve); + }); + pstream.emit('error', { callsid: mockcallsid }); + pstream.emit('ack', { ...mockAckPayload, voiceeventsid: sid }); + await payloadPromise; + }); + }); + + describe('pstream.message event', () => { + const mockcallsid = 'mock-callsid'; + let mockPayload: any; + + beforeEach(() => { + mockPayload = { + callsid: mockcallsid, content: { foo: 'bar', }, @@ -1737,15 +1845,33 @@ describe('Call', function() { messagetype: 'foo-bar', voiceeventsid: 'mock-voiceeventsid-foobar', }; + conn.parameters = { + ...conn.parameters, + CallSid: mockcallsid, + }; + clock.restore(); + }); - const messagePromise = new Promise((resolve, reject) => { - conn.on('message', reject); + it('should emit messageReceived', async () => { + const payloadPromise = new Promise((resolve) => { + conn.on('messageReceived', resolve); }); - pstream.emit('message', mockPayload); + assert.deepEqual(await payloadPromise, { + content: mockPayload.content, + contentType: mockPayload.contenttype, + messageType: mockPayload.messagetype, + voiceEventSid: mockPayload.voiceeventsid, + }); + }); + it('should ignore messages when callSids do not match', async () => { + const messagePromise = new Promise((resolve, reject) => { + conn.on('messageReceived', reject); + }); + pstream.emit('message', { ...mockPayload, callsid: 'foo' }); await Promise.race([ - wait(), + wait(1), messagePromise, ]); });