@babblevoice/babble-drachtio-callmanager
Version:
Call processing to create a PBX
1,404 lines (1,041 loc) • 44.5 kB
JavaScript
const expect = require( "chai" ).expect
const callmanager = require( "../../index.js" )
const call = require( "../../lib/call.js" )
const srf = require( "../mock/srf.js" )
const projectrtp = require( "@babblevoice/projectrtp" ).projectrtp
const projectrtpmessage = require( "@babblevoice/projectrtp/lib/message.js" )
const net = require( "net" )
/* These DO NOT form part of our interface */
const clearcallmanager = require( "../../lib/callmanager.js" )._clear
const callstore = require( "../../lib/store.js" )
after( async () => {
await projectrtp.shutdown()
} )
before( async () => {
await projectrtp.run()
} )
/* All call objects should be created by the framework - but we create them to test */
describe( "call object", function() {
afterEach( function() {
clearcallmanager()
} )
beforeEach( function() {
clearcallmanager()
} )
it( "uas.newuac - create uas", async function() {
const srfscenario = new srf.srfscenario()
let newcallcalled = false
srfscenario.options.em.on( "call.new", ( /*newcall*/ ) => {
newcallcalled = true
} )
const call = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 1,
"storebyuuid": 1,
"storebyentity": 0
} )
await call.hangup()
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 0,
"storebyuuid": 0,
"storebyentity": 0
} )
expect( newcallcalled ).to.be.true
expect( call ).to.have.property( "uuid" ).that.is.a( "string" )
expect( call ).to.have.property( "type" ).that.is.a( "string" ).to.equal( "uas" )
expect( call ).to.have.property( "state" ).that.is.a( "object" )
expect( call.state ).to.have.property( "trying" ).that.is.a( "boolean" ).to.be.false
expect( call.state ).to.have.property( "ringing" ).that.is.a( "boolean" ).to.be.false
expect( call.state ).to.have.property( "established" ).that.is.a( "boolean" ).to.be.false
expect( call.state ).to.have.property( "canceled" ).that.is.a( "boolean" ).to.be.false
expect( call.state ).to.have.property( "destroyed" ).that.is.a( "boolean" ).to.be.true
expect( call ).to.have.property( "children" ) // Set - how do you test?
expect( call ).to.have.property( "parent" ).that.is.undefined
expect( call ).to.have.property( "vars" ).that.is.a( "object" )
expect( call ).to.have.property( "epochs" ).that.is.a( "object" )
expect( call.epochs ).to.have.property( "startat" ).that.is.a( "number" )
expect( call.epochs ).to.have.property( "answerat" ).that.is.a( "number" )
expect( call.epochs ).to.have.property( "endat" ).that.is.a( "number" )
expect( call ).to.have.property( "channels" ).that.is.a( "object" )
expect( call.channels ).to.have.property( "audio" ).to.be.undefined
/* if uas */
expect( call ).to.have.property( "network" ).that.is.a( "object" )
expect( call.network ).to.have.property( "remote" ).that.is.a( "object" )
expect( call.network.remote ).to.have.property( "address" ).that.is.a( "string" )
expect( call.network.remote ).to.have.property( "port" ).that.is.a( "number" )
expect( call.network.remote ).to.have.property( "protocol" ).that.is.a( "string" )
expect( call ).to.have.property( "sip" ).that.is.a( "object" )
expect( call.sip ).to.have.property( "callid" ).that.is.a( "string" )
expect( call.sip ).to.have.property( "tags" ).that.is.a( "object" )
expect( call.sip.tags ).to.have.property( "remote" ).that.is.a( "string" )
expect( call.sip.tags ).to.have.property( "local" ).that.is.a( "string" )
expect( call.hangup_cause.reason ).to.equal( "NORMAL_CLEARING" )
expect( call.hangup_cause.sip ).to.equal( 487 )
expect( call.state.cleaned ).to.be.true
/* defaults are loaded from call manager */
expect( call.options.preferedcodecs ).to.equal( "g722 ilbc pcmu pcma" )
} )
it( "uas.newuac - create uac", async function() {
const srfscenario = new srf.srfscenario()
const call = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 1,
"storebyuuid": 1,
"storebyentity": 0
} )
const child = await call.newuac( { "contact": "1000@dummy" } )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 2,
"storebyuuid": 2,
"storebyentity": 0
} )
expect( child ).to.have.property( "uuid" ).that.is.a( "string" )
expect( child ).to.have.property( "type" ).that.is.a( "string" ).to.equal( "uac" )
expect( child ).to.have.property( "state" ).that.is.a( "object" )
expect( child.state ).to.have.property( "trying" ).that.is.a( "boolean" ).to.be.true
expect( child.state ).to.have.property( "ringing" ).that.is.a( "boolean" ).to.be.false
expect( child.state ).to.have.property( "established" ).that.is.a( "boolean" ).to.be.true /* becuase we awaited the newuax */
expect( child.state ).to.have.property( "canceled" ).that.is.a( "boolean" ).to.be.false
expect( child.state ).to.have.property( "destroyed" ).that.is.a( "boolean" ).to.be.false
expect( child ).to.have.property( "children" ) // Set - how do you test?
expect( child ).to.have.property( "parent" ).that.is.a( "object" )
expect( child ).to.have.property( "vars" ).that.is.a( "object" )
expect( child ).to.have.property( "epochs" ).that.is.a( "object" )
expect( child.epochs ).to.have.property( "startat" ).that.is.a( "number" )
expect( child.epochs ).to.have.property( "answerat" ).that.is.a( "number" )
expect( child.epochs ).to.have.property( "endat" ).that.is.a( "number" )
expect( child ).to.have.property( "channels" ).that.is.a( "object" )
expect( child.channels ).to.have.property( "audio" ).that.is.a( "object" )
expect( child ).to.have.property( "sip" ).that.is.a( "object" )
/* if uac */
expect( child ).to.not.have.property( "source" ).that.is.a( "object" )
/* Hanging up child does not hangup parent */
await child.hangup()
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 1,
"storebyuuid": 1,
"storebyentity": 0
} )
/* Hangup parent */
await call.hangup()
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 0,
"storebyuuid": 0,
"storebyentity": 0
} )
expect( child.state ).to.have.property( "destroyed" ).that.is.a( "boolean" ).to.be.true
expect( call.state.cleaned ).to.be.true
expect( child.state.cleaned ).to.be.true
} )
it( "uas.newuac - onconnect callback is called after bridge with valid audio", async function() {
const srfscenario = new srf.srfscenario()
const call = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
let onconnectcount = 0
let connectedcall
let audioatconnect
let mixepochatconnect
const child = await call.newuac( { "contact": "1000@dummy" }, {
"onconnect": ( c ) => {
onconnectcount++
connectedcall = c
audioatconnect = c.channels.audio
mixepochatconnect = c.epochs.mix
}
} )
/* the handler ran exactly once, during newuac */
expect( onconnectcount ).to.equal( 1 )
/* it received the connected (child) call */
expect( connectedcall ).to.equal( child )
/* the audio channel was a valid object inside the handler - it must not
go undefined/false (the regression this callback originally exposed) */
expect( audioatconnect ).to.be.an( "object" )
expect( child.channels.audio ).to.equal( audioatconnect )
/* it fired AFTER the mix, i.e. once genuinely connected/bridged */
expect( mixepochatconnect ).to.be.a( "number" ).that.is.above( 0 )
await child.hangup()
await call.hangup()
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 0,
"storebyuuid": 0,
"storebyentity": 0
} )
} )
it( "call.preconnect/postconnect - parent-leg hooks fire around the mix when a child bridges (ringall/calldevice path)", async function() {
const srfscenario = new srf.srfscenario()
const call = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
let precount = 0, postcount = 0
let preleg, postleg
let premixepoch, postmixepoch
let preaudio
/* hooks are registered on the parent (caller) leg, before the child exists */
call.preconnect( ( c ) => {
precount++
preleg = c
premixepoch = c.epochs.mix /* not mixed yet -> 0 */
preaudio = c.channels.audio /* channel must be open before the mix */
} )
call.postconnect( ( c ) => {
postcount++
postleg = c
postmixepoch = c.epochs.mix /* mixed now -> > 0 */
} )
/* a normal (non-orphan) newuac - the child bridges via #answerparent */
const child = await call.newuac( { "contact": "1000@dummy" } )
/* each fired exactly once, received the connecting child */
expect( precount ).to.equal( 1 )
expect( postcount ).to.equal( 1 )
expect( preleg ).to.equal( child )
expect( postleg ).to.equal( child )
/* preconnect ran BEFORE the mix, with a valid (open) audio channel;
postconnect ran AFTER the mix */
expect( premixepoch ).to.equal( 0 )
expect( preaudio ).to.be.an( "object" )
expect( postmixepoch ).to.be.a( "number" ).that.is.above( 0 )
/* fire-once: both hooks have been cleared off the parent leg */
expect( call.callbacks.preconnect ).to.be.undefined
expect( call.callbacks.postconnect ).to.be.undefined
await child.hangup()
await call.hangup()
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 0,
"storebyuuid": 0,
"storebyentity": 0
} )
} )
it( "call.preconnect/postconnect - parent-leg hooks fire around adopt-and-mix (enterprise/ring-group path)", async function() {
const srfscenario = new srf.srfscenario()
const call = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
await call.answer()
/* enterprise queues create the agent leg orphaned (no parent), so it does
NOT bridge via #answerparent - it is later adopted by the winning caller */
const agent = await call.newuac( { "contact": "1000@dummy", "orphan": true } )
expect( agent.parent ).to.be.undefined
let precount = 0, postcount = 0
let preleg, postleg
let premixepoch, postmixepoch
let resolveconnected
const connected = new Promise( ( resolve ) => { resolveconnected = resolve } )
call.preconnect( ( c ) => {
precount++
preleg = c
premixepoch = c.epochs.mix
} )
call.postconnect( ( c ) => {
postcount++
postleg = c
postmixepoch = c.epochs.mix
resolveconnected()
} )
/* the orphaned agent existing did not fire either hook */
expect( precount ).to.equal( 0 )
expect( postcount ).to.equal( 0 )
/* the caller adopts-and-mixes the agent - this is the connect moment */
call.adopt( agent, true )
await connected
expect( precount ).to.equal( 1 )
expect( postcount ).to.equal( 1 )
expect( preleg ).to.equal( agent )
expect( postleg ).to.equal( agent )
/* preconnect ran before the mix, postconnect after */
expect( premixepoch ).to.equal( 0 )
expect( postmixepoch ).to.be.a( "number" ).that.is.above( 0 )
/* fire-once: a second adopt-and-mix does not refire */
expect( call.callbacks.preconnect ).to.be.undefined
expect( call.callbacks.postconnect ).to.be.undefined
const agent2 = await call.newuac( { "contact": "1000@dummy", "orphan": true } )
call.adopt( agent2, true )
expect( precount ).to.equal( 1 )
expect( postcount ).to.equal( 1 )
await agent2.hangup()
await agent.hangup()
await call.hangup()
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 0,
"storebyuuid": 0,
"storebyentity": 0
} )
} )
it( "uas.newuac - create uac by entity no registrar", async function() {
const srfscenario = new srf.srfscenario()
const call = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
const child = await call.newuac( { "entity": { "uri": "1000@dummy" } } )
expect( child ).to.be.false
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 1,
"storebyuuid": 1,
"storebyentity": 0
} )
await call.hangup()
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 0,
"storebyuuid": 0,
"storebyentity": 0
} )
expect( call.state.cleaned ).to.be.true
} )
it( "uas.newuac - create uac by entity with registrar", async function() {
const options = {
"registrar": {
"contacts": async ( /* entity */ ) => {
return {
"username": "1000",
"realm": "dummy.com",
"display": "Bob",
"uri": "1000@dummy.com",
"contacts": [ { "contact": "sip:1000@dummy.com:5060" }, { "contact": "sip:1000@dummy.com:5060;transport=blah" } ]
}
}
}
}
const srfscenario = new srf.srfscenario( options )
const call = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
call._req.setparsedheader( "remote-party-id", {
"name": "",
"uri": "",
"user": "0123456789",
"host": "someotherrealm.com",
"params": { "privacy": false },
"type": "callerid"
} )
/* when newuacv returns, only one of the contacts should hacve succeeded */
const child = await call.newuac( { "entity": { "uri": "1000@dummy" } } )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 2,
"storebyuuid": 2,
"storebyentity": 1
} )
const e = await child.entity
expect( e.ccc ).to.equal( 1 )
/* mock */
let requestoptions = false
child.parseallow( { get:() => { return "INVITE, UPDATE, OPTIONS" } } )
child._dialog.on( "request", ( options ) => requestoptions = options )
child.update()
expect( requestoptions.method ).to.equal( "UPDATE" )
expect( requestoptions.headers[ "remote-party-id" ] ).to.equal( "\"0123456789\" <sip:0123456789@someotherrealm.com>;party=called;screen=yes" )
/* simulate wire to propogate hangup */
await call.hangup( call.hangupcodes.NORMAL_CLEARING, false, "wire" )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 0,
"storebyuuid": 0,
"storebyentity": 0
} )
expect( srfscenario.options.srf._createuaccount ).to.equal( 2 )
expect( call.state.cleaned ).to.be.true
expect( child.state.cleaned ).to.be.true
} )
it( "uas.newuac - create uac by entity with max limit and registrar", async function() {
const options = {
"registrar": {
"contacts": async ( /* entity */ ) => {
return {
"username": "1000",
"realm": "dummy.com",
"display": "Bob",
"uri": "1000@dummy.com",
"contacts": [ { "contact": "sip:1000@dummy.com:5060" }, { "contact": "sip:1000@dummy.com:5060;transport=blah" } ]
}
}
}
}
const srfscenario = new srf.srfscenario( options )
const inboundcall = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
const child = await inboundcall.newuac( { "entity": { "uri": "1000@dummy", "max": 1 } } )
const child2 = await call.newuac( { "entity": { "uri": "1000@dummy", "max": 1 } } )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 2,
"storebyuuid": 2,
"storebyentity": 1
} )
const child3newcalls = []
await call.newuac( { "entity": { "uri": "1000@dummy" } }, { early:( c ) => child3newcalls.push( c ) } )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 4,
"storebyuuid": 4,
"storebyentity": 1
} )
await Promise.all( [
child3newcalls[ 0 ].hangup(),
child3newcalls[ 1 ].hangup()
] )
await inboundcall.hangup( call.hangupcodes.NORMAL_CLEARING, false, "wire" )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 0,
"storebyuuid": 0,
"storebyentity": 0
} )
expect( srfscenario.options.srf._createuaccount ).to.equal( 4 )
expect( child2 ).to.be.false
expect( child.state.cleaned ).to.be.true
expect( child3newcalls[ 0 ].state.cleaned ).to.be.true
expect( child3newcalls[ 1 ].state.cleaned ).to.be.true
} )
it( "uas.newuac detatch from parent", async function() {
const srfscenario = new srf.srfscenario()
const call = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 1,
"storebyuuid": 1,
"storebyentity": 0
} )
const child = await call.newuac( { "contact": "1000@dummy" } )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 2,
"storebyuuid": 2,
"storebyentity": 0
} )
child.detach()
await child.hangup()
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 1,
"storebyuuid": 1,
"storebyentity": 0
} )
await call.hangup()
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 0,
"storebyuuid": 0,
"storebyentity": 0
} )
expect( call.state.cleaned ).to.be.true
expect( child.state.cleaned ).to.be.true
} )
it( "uas.newuac - 486", async function() {
const srfscenario = new srf.srfscenario()
srfscenario.oncreateUAC( () => {
throw { "status": 486 }
} )
const call = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
const children = []
const child = await call.newuac( { "contact": "1000@dummy" }, { "early": ( c ) => {
children.push( c )
}
} )
expect( children.length ).to.equal( 1 )
expect( children[ 0 ].hangup_cause.sip ).equal( 486 )
expect( children[ 0 ].hangup_cause.reason ).equal( "USER_BUSY" )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 1,
"storebyuuid": 1,
"storebyentity": 0
} )
await call.hangup()
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 0,
"storebyuuid": 0,
"storebyentity": 0
} )
expect( call.state.cleaned ).to.be.true
expect( child.state.cleaned ).to.be.true
} )
it( "uas.newuac - timeout", async function() {
const srfscenario = new srf.srfscenario()
srfscenario.options.srf.newuactimeout = 20 /* longer than our uactimeout */
const call = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
const children = []
const child = await call.newuac( { "contact": "1000@dummy", "uactimeout": 10 }, { "early": ( c ) => {
children.push( c )
}
} ) /* overide default - very short */
expect( child.destroyed ).to.be.true
expect( children.length ).to.equal( 1 )
expect( children[ 0 ].destroyed ).to.be.true
expect( children[ 0 ].hangup_cause.sip ).equal( 408 )
expect( children[ 0 ].hangup_cause.reason ).equal( "REQUEST_TIMEOUT" )
expect( children[ 0 ].hangup_cause.src ).equal( "us" )
/* when we get here the clean up is on teh event loop */
await new Promise( resolve => setTimeout( resolve, 100 ) )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 1,
"storebyuuid": 1,
"storebyentity": 0
} )
await call.hangup()
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 0,
"storebyuuid": 0,
"storebyentity": 0
} )
expect( call.state.cleaned ).to.be.true
expect( child.state.cleaned ).to.be.true
} )
it( "uas.newuac - createUAC never settles - watchdog forces return", async function() {
const srfscenario = new srf.srfscenario()
/* simulate a far end that never sends a final response and never honours
CANCEL: createUAC never resolves or rejects. Without the settle watchdog
newuac would hang here forever (and this test would hit the mocha timeout). */
srfscenario.oncreateUAC( () => new Promise( () => {} ) )
const call = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
const start = Date.now()
const child = await call.newuac( { "contact": "1000@dummy", "uactimeout": 50 } )
const elapsed = Date.now() - start
/* the watchdog must make newuac return promptly (~uactimeout) rather than
block on the never-settling createUAC */
expect( elapsed ).to.be.below( 1000 )
expect( child.destroyed ).to.be.true
expect( child.hangup_cause.sip ).to.equal( 408 )
expect( child.hangup_cause.reason ).to.equal( "REQUEST_TIMEOUT" )
expect( child.hangup_cause.src ).to.equal( "us" )
/* clean up runs on the event loop */
await new Promise( resolve => setTimeout( resolve, 100 ) )
await call.hangup()
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 0,
"storebyuuid": 0,
"storebyentity": 0
} )
expect( call.state.cleaned ).to.be.true
expect( child.state.cleaned ).to.be.true
} )
it( "uas.newuac - child remote hangup", async function() {
const srfscenario = new srf.srfscenario()
const call = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 1,
"storebyuuid": 1,
"storebyentity": 0
} )
const child = await call.newuac( { "contact": "1000@dummy" } )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 2,
"storebyuuid": 2,
"storebyentity": 0
} )
expect( child.established ).to.be.true
/* mock destroy from network */
child._dialog.destroy()
await child.waitforhangup()
expect( child.hangup_cause.sip ).equal( 487 )
expect( child.hangup_cause.reason ).equal( "NORMAL_CLEARING" )
expect( child.hangup_cause.src ).equal( "wire" )
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 1,
"storebyuuid": 1,
"storebyentity": 0
} )
await call.hangup()
expect( await callstore.stats() ).to.deep.include( {
"storebycallid": 0,
"storebyuuid": 0,
"storebyentity": 0
} )
expect( child.state ).to.have.property( "destroyed" ).that.is.a( "boolean" ).to.be.true
expect( call.state.cleaned ).to.be.true
expect( child.state.cleaned ).to.be.true
} )
it( "uas.newuac - new call event", async function() {
const srfscenario = new srf.srfscenario()
let eventhappened = false
srfscenario.options.em.on( "call.new", ( c ) => {
expect( c ).to.be.an.instanceof( call )
expect( c.type ).to.be.an( "string" ).to.be.equal( "uas" )
eventhappened = true
} )
const inbound = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
expect( eventhappened ).to.be.true
await inbound.hangup()
} )
it( "uas.newuac - ringing event", async function() {
const srfscenario = new srf.srfscenario()
let eventhappened = false
srfscenario.options.em.on( "call.ringing", ( c ) => {
expect( c ).to.be.an.instanceof( call )
expect( c.type ).to.be.an( "string" ).to.be.equal( "uas" )
eventhappened = true
} )
const c = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
/* signal ringing (this would normally come from the other side) */
c.ring()
expect( eventhappened ).to.be.true
await c.hangup()
} )
it( "uas.newuac - destroyed event", async function() {
const srfscenario = new srf.srfscenario()
let eventhappened = false
const c = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
c.on( "call.destroyed", ( /*c*/ ) => {
eventhappened = true
} )
expect( eventhappened ).to.be.false
await c.hangup()
expect( eventhappened ).to.be.true
} )
it( "uas.newuac - answered and destroyed event", async function() {
const srfscenario = new srf.srfscenario()
let eventhappened = false
srfscenario.options.em.on( "call.answered", ( c ) => {
expect( c ).to.be.an.instanceof( call )
expect( c.type ).to.be.an( "string" ).to.be.equal( "uas" )
eventhappened = true
} )
srfscenario.options.em.on( "call.destroyed", ( /*c*/ ) => {
eventhappened = false
} )
const c = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => { resolve( call ) } )
srfscenario.inbound()
} )
/* signal answered (this could be called for a ivr or a bridged call) */
await c.answer()
expect( eventhappened ).to.be.true
await c.hangup()
expect( eventhappened ).to.be.false
} )
it( "uas.newuac - authed event", async function() {
const options = {
"userlookup": async ( username, realm ) => {
return {
"secret": "zanzibar",
"username": username,
"realm": realm
}
}
}
const srfscenario = new srf.srfscenario( options )
let eventhappened = false
srfscenario.options.em.on( "call.authed", ( c ) => {
expect( c ).to.be.an.instanceof( call )
expect( c.type ).to.be.an( "string" ).to.be.equal( "uas" )
eventhappened = true
} )
const c = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => {
resolve( call )
} )
srfscenario.inbound()
} )
let onsendcount = 0
/* mock - auth example from https://datatracker.ietf.org/doc/html/draft-smith-sipping-auth-examples-01 3.3*/
c._req.msg.uri = "sip:bob@biloxi.com"
c._req.setparsedheader( "from", { "params": { "tag": "767sf76wew" }, "uri": "sip:bob@biloxi.com", "host": "biloxi.com" } )
c._res.onsend( ( code, /*msg*/ ) => {
if( 407 == code ) {
c._auth._nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093"
c._auth._opaque = "5ccc069c403ebaf9f0171e9517f40e41"
/* We would normally look here to get nonce and opaque - howevere we are frigging it */
//let request = msg.headers[ "Proxy-Authenticate" ]
let request = "Digest realm=\"biloxi.com\", algorithm=MD5, qop=\"auth\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", opaque=\"5ccc069c403ebaf9f0171e9517f40e41\", stale=false"
/* The items a uac will add */
request += ", username=\"bob\", nc=00000001,cnonce=\"0a4f113b\","
request += " uri=\"sip:bob@biloxi.com\","
request += " response=\"89eb0059246c02b2f6ee02c7961d5ea3\""
srfscenario.req.set( "Proxy-Authorization", request )
c._onauth( srfscenario.req, srfscenario.res )
}
onsendcount++
} )
/* signal answered (this could be called for a ivr or a bridged call) */
let shouldnothappen = false
try{
await c.auth()
} catch( e ) {
shouldnothappen = true
}
await c.hangup()
expect( eventhappened ).to.be.true
expect( shouldnothappen ).to.be.false
expect( c.hangup_cause.reason ).to.equal( "NORMAL_CLEARING" )
expect( c.hangup_cause.src ).to.equal( "us" )
expect( c.state.destroyed ).to.equal( true )
expect( c.state.authed ).to.equal( true )
const e = await c.entity
expect( e.username ).to.equal( "bob" )
expect( e.realm ).to.equal( "biloxi.com" )
expect( e.uri ).to.equal( "bob@biloxi.com" )
expect( onsendcount ).to.equal( 2 )
} )
it( "uas.newuac - auth failed", async function() {
const options = {
"userlookup": async ( username, realm ) => {
return {
"secret": "zanzibar",
"username": username,
"realm": realm
}
}
}
const srfscenario = new srf.srfscenario( options )
let eventhappened = false
srfscenario.options.em.on( "call.authed.failed", ( c ) => {
expect( c ).to.be.an.instanceof( call )
expect( c.type ).to.be.an( "string" ).to.be.equal( "uas" )
eventhappened = true
} )
const c = await new Promise( ( resolve ) => {
srfscenario.oncall( async ( call ) => {
let onsendcount = 0
/* mock -
auth example from https://datatracker.ietf.org/doc/html/draft-smith-sipping-auth-examples-01 3.3*/
call._req.msg.uri = "sip:bob@biloxi.com"
call._req.setparsedheader( "from", { "params": { "tag": "767sf76wew" }, "uri": "sip:bob@biloxi.com", "host": "biloxi.com" } )
call._auth._nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093"
call._auth._opaque = "5ccc069c403ebaf9f0171e9517f40e41"
call._res.onsend( ( code, msg ) => {
if( 0 == onsendcount ) {
let request = msg.headers[ "Proxy-Authenticate" ]
/* The items a uac will add */
request += ", username=\"bob\", nc=00000001,cnonce=\"0a4f113b\","
request += " uri=\"sip:bob@biloxi.com\","
request += " response=\"89eb0059246c02b2f6ee02c7961d5ea\""
srfscenario.req.set( "Proxy-Authorization", request )
call._onauth( srfscenario.req, srfscenario.res )
} else if( 1 == onsendcount ) {
resolve( call )
}
onsendcount++
} )
/* signal answered (this could be called for a ivr or a bridged call) */
await call.auth()
} )
srfscenario.inbound()
} )
await c.waitforhangup()
expect( eventhappened ).to.be.true
expect( c.hangup_cause.reason ).to.equal( "FORBIDDEN" )
expect( c.hangup_cause.src ).to.equal( "us" )
expect( c.state.destroyed ).to.equal( true )
expect( c.state.authed ).to.equal( false )
} )
it( "uas.newuac - caller id set correctly", async function() {
const srfscenario = new srf.srfscenario( {} )
let createuacoptions
srfscenario.oncreateUAC( ( contact, options, /*callbacks*/ ) => {
createuacoptions = options
} )
const options = {
"contact": "ourcontactstring",
"late": true
}
await call.newuac( options, { "early": ( c ) => c.hangup() } )
expect( createuacoptions.headers[ "remote-party-id" ] ).to.equal( "\"0000000000\" <sip:0000000000@localhost.localdomain>;party=called;screen=yes" )
expect( createuacoptions.late ).to.be.true
} )
it( "uas.newuac - early callback is called", async function() {
new srf.srfscenario( {} )
const options = {
"contact": "ourcontactstring",
"late": true
}
let earlycallbackcalled = false
const c = await call.newuac( options, { "early": ( /*c*/ ) => earlycallbackcalled = true } )
c.hangup()
expect( earlycallbackcalled ).to.be.true
} )
it( "uas.newuac - confirmed callback is called", async function() {
new srf.srfscenario( {} )
const options = {
"contact": "ourcontactstring",
"late": true
}
let earlycallbackcalled = false
const c = await call.newuac( options, { "confirm": ( /*c*/ ) => earlycallbackcalled = true } )
c.hangup()
expect( earlycallbackcalled ).to.be.true
} )
it( "uas.newuac - simple update", async function() {
new srf.srfscenario( {} )
const options = {
"contact": "ourcontactstring",
"late": true
}
const c = await call.newuac( options )
/* mock */
c.parseallow( { get:() => { return "INVITE, UPDATE, OPTIONS" } } )
let requestoptions
c._dialog.on( "request", ( options ) => requestoptions = options )
await c.update( { "callerid": {
"name": "Kermit",
"number": "kermy"
} } )
c.hangup()
expect( requestoptions.method ).to.equal( "UPDATE" )
expect( requestoptions.body ).to.be.a( "string" )
expect( requestoptions.headers[ "remote-party-id" ] ).to.equal( "\"Kermit\" <sip:kermy@localhost.localdomain>;party=called;screen=yes" )
} )
it( "uas.newuac - simple update - but don't allow as not in allow", async function() {
new srf.srfscenario( {} )
const options = {
"contact": "ourcontactstring",
"late": true
}
const c = await call.newuac( options )
/* mock */
let requestoptions = false
c._dialog.on( "request", ( options ) => requestoptions = options )
const returnedval = await c.update( { "remote": {
"display": "Kermit",
"realm": "muppetshow.com",
"username": "kermy"
} } )
c.hangup()
expect( returnedval ).to.be.false
expect( requestoptions ).to.be.false
} )
it( "Test listen and emit event on call object", async function() {
new srf.srfscenario( {} )
const options = {
"contact": "ourcontactstring",
"late": true
}
const c = await call.newuac( options )
let eventfired = false
c.on( "somerandomevent", ( ob ) => {
if( "hello" === ob.vars.xinfo ) eventfired = true
} )
c.vars.xinfo = "hello"
c.emit( "somerandomevent" )
c.hangup()
expect( eventfired ).to.be.true
} )
it( "Test listen and emit event on and removealllisteners call object", async function() {
new srf.srfscenario( {} )
const options = {
"contact": "ourcontactstring",
"late": true
}
const c = await call.newuac( options )
let eventfired = 0
c.on( "somerandomevent", ( ob ) => {
if( "hello" === ob.vars.xinfo ) eventfired++
} )
c.vars.xinfo = "hello"
c.emit( "somerandomevent" )
c.removealllisteners( "somerandomevent" )
c.emit( "somerandomevent" )
c.hangup()
expect( eventfired ).to.be.equal( 1 )
} )
it( "Test listen and emit event on and removealllisteners (none specified) call object", async function() {
new srf.srfscenario( {} )
const options = {
"contact": "ourcontactstring",
"late": true
}
const c = await call.newuac( options )
let eventfired = 0
c.on( "somerandomevent", ( ob ) => {
if( "hello" === ob.vars.xinfo ) eventfired++
} )
c.vars.xinfo = "hello"
c.emit( "somerandomevent" )
c.removealllisteners()
c.emit( "somerandomevent" )
c.hangup()
expect( eventfired ).to.be.equal( 1 )
} )
it( "Test listen and emit event on and off call object", async function() {
new srf.srfscenario( {} )
const options = {
"contact": "ourcontactstring",
"late": true
}
const c = await call.newuac( options )
let eventfired = 0
const ourcb = ( ob ) => {
if( "hello" === ob.vars.xinfo ) eventfired++
}
c.on( "somerandomevent", ourcb )
c.vars.xinfo = "hello"
c.emit( "somerandomevent" )
c.off( "somerandomevent", ourcb )
c.emit( "somerandomevent" )
c.hangup()
expect( eventfired ).to.be.equal( 1 )
} )
it( "Test listen and emit event once call object", async function() {
new srf.srfscenario( {} )
const options = {
"contact": "ourcontactstring",
"late": true
}
const c = await call.newuac( options )
let eventfired = 0
c.once( "somerandomevent", ( ob ) => {
if( "hello" === ob.vars.xinfo ) eventfired++
} )
c.vars.xinfo = "hello"
c.emit( "somerandomevent" )
c.emit( "somerandomevent" )
c.hangup()
expect( eventfired ).to.be.equal( 1 )
} )
it( "Test listen and emit call.pick on call object", async function() {
new srf.srfscenario( {} )
const options = {
"contact": "ourcontactstring",
"late": true
}
const c = await call.newuac( options )
let eventfired = false
c.on( "call.pick", ( /*callobject*/ ) => {
eventfired = true
} )
c.pick()
c.hangup()
expect( eventfired ).to.be.true
} )
it( "Create a call and mock rtpengine and ensure we receive events", async function() {
new srf.srfscenario( {} )
const rtpserver = await callmanager.projectrtp.proxy.listen()
const connection = net.createConnection( 9002, "127.0.0.1" )
.on( "error", ( e ) => {
console.error( e )
} )
connection.on( "connect", () => {
/* announce our node */
connection.write( projectrtpmessage.createmessage( {"status":{"channel":{"available":5000,"current":0},"workercount":12,"instance":"ca0ef6a9-9174-444d-bdeb-4c9eb54d5c94"}} ) )
} )
const messagestate = projectrtpmessage.newstate()
let msgid
connection.on( "data", ( data ) => {
projectrtpmessage.parsemessage( messagestate, data, ( msg ) => {
if( "open" === msg.channel ) {
msgid = msg.id
setTimeout( () => connection.write( projectrtpmessage.createmessage( {"local":{"port":10008,"dtls":{"fingerprint":"Some fingerprint","enabled":false},"address":"192.168.0.141"},"id": msg.id, "uuid":"6d8ba7bb-44b9-4989-9aaf-5d938b496c49","action":"open","status":{"channel":{"available":4995,"current":5},"workercount":12,"instance":"ca0ef6a9-9174-444d-bdeb-4c9eb54d5c94"}} ) ), 2 )
} else if( "close" == msg.channel ) {
connection.write( projectrtpmessage.createmessage( {"id": msgid,"uuid":"6d8ba7bb-44b9-4989-9aaf-5d938b496c49","action":"record","file":"/tmp/voicemail/recording/03039cdb-1949-407d-91d6-15ba6894955c.wav","event":"finished.channelclosed","filesize":160684,"emailed":true,"transcription":"test recording for voicemail","status":{"channel":{"available":4995,"current":5},"workercount":12,"instance":"ca0ef6a9-9174-444d-bdeb-4c9eb54d5c94"}} ) )
connection.write( projectrtpmessage.createmessage( {"id": msgid,"uuid":"6d8ba7bb-44b9-4989-9aaf-5d938b496c49","action":"close","reason":"requested","stats":{"in":{"mos":4.5,"count":586,"dropped":0,"skip":0},"out":{"count":303,"skip":0},"tick":{"meanus":124,"maxus":508,"count":597}},"status":{"channel":{"available":4995,"current":5},"workercount":12,"instance":"ca0ef6a9-9174-444d-bdeb-4c9eb54d5c94"}} ) )
}
} )
} )
/* ensure we are connected */
await new Promise( ( resolve ) => setTimeout( () => resolve(), 100 ) )
/* this flow mimicks the flow associated with a voicemail being left */
const c = await call.newuac( {
"contact": "ourcontactstring"
} )
setTimeout( () => connection.write( projectrtpmessage.createmessage( {"id": msgid,"uuid":"6d8ba7bb-44b9-4989-9aaf-5d938b496c49","action":"play","event":"start","reason":"new","status":{"channel":{"available":4995,"current":5},"workercount":12,"instance":"ca0ef6a9-9174-444d-bdeb-4c9eb54d5c94"}} ) ), 1 )
setTimeout( () => connection.write( projectrtpmessage.createmessage( {"id": msgid,"uuid":"6d8ba7bb-44b9-4989-9aaf-5d938b496c49","action":"play","event":"end","reason":"completed","status":{"channel":{"available":4995,"current":5},"workercount":12,"instance":"ca0ef6a9-9174-444d-bdeb-4c9eb54d5c94"}} ) ), 10 )
c.channels.audio.play( { "files": [ { "wav": "/voicemail/greeting.wav", "alt": "greeting" } ] } )
const playgreetendev = await c.waitforanyevent( { "action": "play", "event": "end" } )
setTimeout( () => connection.write( projectrtpmessage.createmessage( {"id": msgid,"uuid":"6d8ba7bb-44b9-4989-9aaf-5d938b496c49","action":"play","event":"start","reason":"new","status":{"channel":{"available":4995,"current":5},"workercount":12,"instance":"ca0ef6a9-9174-444d-bdeb-4c9eb54d5c94"}} ) ) )
setTimeout( () => connection.write( projectrtpmessage.createmessage( {"id": msgid,"uuid":"6d8ba7bb-44b9-4989-9aaf-5d938b496c49","action":"play","event":"end","reason":"completed","status":{"channel":{"available":4995,"current":5},"workercount":12,"instance":"ca0ef6a9-9174-444d-bdeb-4c9eb54d5c94"}} ) ) )
c.channels.audio.play( { "files": [ { "wav": "/voicemail/boing.wav", "alt": "" } ] } )
const playendev = await c.waitforanyevent( { "action": "play", "event": "end" } )
setTimeout( () => connection.write( projectrtpmessage.createmessage( {"id": msgid,"uuid":"6d8ba7bb-44b9-4989-9aaf-5d938b496c49","action":"record","file":"/tmp/voicemail/recording/03039cdb-1949-407d-91d6-15ba6894955c.wav","event":"recording","status":{"channel":{"available":4995,"current":5},"workercount":12,"instance":"ca0ef6a9-9174-444d-bdeb-4c9eb54d5c94"}} ) ), 5 )
setTimeout( () => connection.write( projectrtpmessage.createmessage( {"id": msgid,"uuid":"6d8ba7bb-44b9-4989-9aaf-5d938b496c49","action":"record","file":"/tmp/voicemail/recording/03039cdb-1949-407d-91d6-15ba6894955c.wav","event":"finished","event":"finished.channelclosed", "transcription": "test recording for voicemail", "emailed": true, "filesize": 160684, "status":{"channel":{"available":4995,"current":5},"workercount":12,"instance":"ca0ef6a9-9174-444d-bdeb-4c9eb54d5c94"}} ) ), 10 )
c.channels.audio.record( {
"file": "/voicemail/jhjhjgjhgjg.wav",
"maxduration": 120 * 1000,
"numchannels": 1,
"email": {
"to": "test@example.com",
"foruser": "1000",
"from": "012345789"
},
"transcribe": true
} )
setTimeout( () => {
/* hangup from client */
c._onhangup( "wire" )
}, 50 )
const recordfinishev = await c.waitforanyevent( { "action": "record", "event": /finished.*|\*/ }, 10000 )
connection.destroy()
await rtpserver.destroy()
expect( playgreetendev.action ).to.equal( "play" )
expect( playgreetendev.event ).to.equal( "end" )
expect( playgreetendev.reason ).to.equal( "completed" )
expect( playendev.action ).to.equal( "play" )
expect( playendev.event ).to.equal( "end" )
expect( playendev.reason ).to.equal( "completed" )
expect( recordfinishev.action ).to.equal( "record" )
expect( recordfinishev.event ).to.equal( "finished.channelclosed" )
expect( recordfinishev.transcription ).to.equal( "test recording for voicemail" )
expect( recordfinishev.emailed ).to.be.true
expect( recordfinishev.filesize ).to.equal( 160684 )
} )
it( "set get moh", async function() {
new srf.srfscenario( {} )
const options = {
"contact": "ourcontactstring",
"late": true
}
const c = await call.newuac( options )
const s = {
"loop": true,
"files": [
{ "wav": "some.wav" }
]
}
/* no default configured */
expect( c.moh ).to.be.undefined
c.moh = s
expect( c._moh.loop ).to.equal( true )
expect( c.moh.loop ).to.equal( true )
c._onhangup( "wire" )
} )
it( "overide caller id name", async function() {
new srf.srfscenario( {} )
const options = {
"contact": "ourcontactstring",
"callerid": {
"name": "Hello"
}
}
const c = await call.newuac( options )
/* no default configured */
expect( c.options.headers[ "remote-party-id" ] ).to.equal( "\"Hello\" <sip:0000000000@localhost.localdomain>;party=called;screen=yes" )
c._onhangup( "wire" )
} )
it( "overide caller id number", async function() {
new srf.srfscenario( {} )
const options = {
"contact": "ourcontactstring",
"callerid": {
"number": "012345789"
}
}
const c = await call.newuac( options )
/* no default configured */
expect( c.options.headers[ "remote-party-id" ] ).to.equal( "\"012345789\" <sip:012345789@localhost.localdomain>;party=called;screen=yes" )
c._onhangup( "wire" )
} )
} )