From 0ce9300027306f3e95f0c68b8f713a3212752bd2 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Tue, 31 Oct 2023 11:47:46 -0400 Subject: [PATCH] SWI-3836 Update BXML Library and Add Integration Tests --- custom_templates/package.mustache | 2 +- models/bxml/index.ts | 3 + models/bxml/verbs/index.ts | 30 +++ models/index.ts | 1 + tests/integration/bxml.test.js | 294 ++++++++++++++++++++++++++++++ 5 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 models/bxml/index.ts create mode 100644 models/bxml/verbs/index.ts create mode 100644 tests/integration/bxml.test.js diff --git a/custom_templates/package.mustache b/custom_templates/package.mustache index 419e61f..e36b3da 100644 --- a/custom_templates/package.mustache +++ b/custom_templates/package.mustache @@ -24,7 +24,7 @@ "scripts": { "build": "tsc {{#supportsES6}}&& tsc -p tsconfig.esm.json{{/supportsES6}}", "prepare": "npm run build", - "test": "jest --coverage --detectOpenHandles" + "test": "jest --detectOpenHandles" }, "dependencies": { "axios": "^0.27.2", diff --git a/models/bxml/index.ts b/models/bxml/index.ts new file mode 100644 index 0000000..8aa6112 --- /dev/null +++ b/models/bxml/index.ts @@ -0,0 +1,3 @@ +import * as Bxml from './verbs'; + +export { Bxml }; diff --git a/models/bxml/verbs/index.ts b/models/bxml/verbs/index.ts new file mode 100644 index 0000000..9fbfca1 --- /dev/null +++ b/models/bxml/verbs/index.ts @@ -0,0 +1,30 @@ +export * from './Bridge'; +export * from './Conference'; +export * from './CustomParam'; +export * from './Forward'; +export * from './Gather'; +export * from './Hangup'; +export * from './Pause'; +export * from './PauseRecording'; +export * from './PhoneNumber'; +export * from './PlayAudio'; +export * from './Record'; +export * from './Redirect'; +export * from './ResumeRecording'; +export * from './Ring'; +export * from './SendDtmf'; +export * from './SipUri'; +export * from './SpeakSentence'; +export * from './StartGather'; +export * from './StartRecording'; +export * from './StartStream'; +export * from './StartTranscription'; +export * from './StopGather'; +export * from './StopRecording'; +export * from './StopStream'; +export * from './StopTranscription'; +export * from './StreamParam'; +export * from './Tag'; +export * from './Transfer'; +export * from '../Bxml'; +export * from '../Response'; diff --git a/models/index.ts b/models/index.ts index fc92b66..c43d81e 100644 --- a/models/index.ts +++ b/models/index.ts @@ -86,3 +86,4 @@ export * from './verify-code-request'; export * from './verify-code-response'; export * from './voice-api-error'; export * from './voice-code-response'; +export * from './bxml'; diff --git a/tests/integration/bxml.test.js b/tests/integration/bxml.test.js new file mode 100644 index 0000000..154415d --- /dev/null +++ b/tests/integration/bxml.test.js @@ -0,0 +1,294 @@ +const { CallsApi } = require('../../api/calls-api'); +const { Configuration } = require('../../configuration'); +const { Bxml, CallStateEnum } = require('../../models'); +const { createMantecaCall, sleep, cleanupCalls } = require('../callUtils'); + +describe('BXML Integration Tests', () => { + jest.setTimeout(20000); + + const config = new Configuration({username: BW_USERNAME, password: BW_PASSWORD}); + const callsApi = new CallsApi(config); + let activeCalls = []; + + afterAll(async () => { + await cleanupCalls(activeCalls, callsApi); + }); + + test('test one-off verbs', async () => { + const updateCallId = await createMantecaCall(callsApi); + const bridgeCallId = await createMantecaCall(callsApi); + activeCalls.push(updateCallId, bridgeCallId); + await sleep(SLEEP_TIME_S); + + const conferenceAttributes = { + mute: true, + hold: true, + callIdsToCoach: updateCallId, + conferenceEventUrl: 'https://initial.com', + conferenceEventMethod: 'POST', + conferenceEventFallbackUrl: 'https://initial.com', + conferenceEventFallbackMethod: 'POST', + username: 'initialUsername', + password: 'initialPassword', + fallbackUsername: 'initialFallbackUsername', + fallbackPassword: 'initialFallbackPassword', + tag: 'initialTag', + callbackTimeout: 1.1 + }; + + const gatherAttributes = { + gatherUrl: 'https://initial.com', + gatherMethod: 'POST', + gatherFallbackUrl: 'https://initial.com', + gatherFallbackMethod: 'POST', + username: 'initialUsername', + password: 'initialPassword', + fallbackUsername: 'initialFallbackUsername', + fallbackPassword: 'initialFallbackPassword', + tag: 'initialTag', + terminatingDigits: '5', + maxDigits: 1, + interDigitTimeout: 1.1, + firstDigitTimeout: 1.1, + repeatCount: 1 + }; + + const speakSentenceAttributes = { + voice: 'julie', + gender: 'female', + locale: 'en_US' + }; + + const recordAttributes = { + recordCompleteUrl: 'https://initial.com', + recordCompleteMethod: 'POST', + recordCompleteFallbackUrl: 'https://initial.com', + recordCompleteFallbackMethod: 'POST', + recordingAvailableUrl: 'https://initial.com', + recordingAvailableMethod: 'POST', + transcribe: true, + transcriptionAvailableUrl: 'https://initial.com', + transcriptionAvailableMethod: 'POST', + username: 'initialUsername', + password: 'initialPassword', + fallbackUsername: 'initialFallbackUsername', + fallbackPassword: 'initialFallbackPassword', + tag: 'initialTag', + terminatingDigits: '#', + maxDuration: 2, + silenceTimeout: 2, + fileFormat: 'wav' + }; + + const sendDtmfAttributes = { + toneDuration: 50, + toneInterval: 50 + }; + + const ringAttributes = { + duration: 5.1, + answerCall: true + }; + + const bridgeAttributes = { + bridgeCompleteUrl: 'https://initial.com', + bridgeCompleteMethod: 'POST', + bridgeCompleteFallbackUrl: 'https://initial.com', + bridgeCompleteFallbackMethod: 'POST', + bridgeTargetCompleteUrl: 'https://initial.com', + bridgeTargetCompleteMethod: 'POST', + bridgeTargetCompleteFallbackUrl: 'https://initial.com', + bridgeTargetCompleteFallbackMethod: 'POST', + username: 'initialUsername', + password: 'initialPassword', + fallbackUsername: 'initialFallbackUsername', + fallbackPassword: 'initialFallbackPassword', + tag: 'initialTag' + }; + + const transferAttributes = { + transferCallerId: '+19195551234', + callTimeout: 5, + transferCompleteUrl: 'https://initial.com', + transferCompleteMethod: 'POST', + transferCompleteFallbackUrl: 'https://initial.com', + transferCompleteFallbackMethod: 'POST', + username: 'initialUsername', + password: 'initialPassword', + fallbackUsername: 'initialFallbackUsername', + fallbackPassword: 'initialFallbackPassword', + tag: 'initialTag', + diversionTreatment: 'propagate', + diversionReason: 'user-busy' + }; + + const phoneNumber = new Bxml.PhoneNumber('+19195551234'); + const sipUri = new Bxml.SipUri('sip:1-999-123-4567@voip-provider.example.net'); + + const conference = new Bxml.Conference('my-conference', conferenceAttributes); + const gather = new Bxml.Gather(gatherAttributes); + const speakSentence = new Bxml.SpeakSentence('Holanodejs speak sentence SSML test', speakSentenceAttributes); + const nestedGather = new Bxml.Gather(gatherAttributes, [speakSentence]); + const pause = new Bxml.Pause({ duration: 2 }); + const playAudio = new Bxml.PlayAudio('https://www.example.com/file.wav', { username: 'initialUsername', password: 'initialPassword' }); + const record = new Bxml.Record(recordAttributes); + const sendDtmf = new Bxml.SendDtmf('1234', sendDtmfAttributes); + const tag = new Bxml.Tag('testTag'); + const ring = new Bxml.Ring(ringAttributes); + const hangup = new Bxml.Hangup(); + const bridge = new Bxml.Bridge(bridgeCallId, bridgeAttributes); + const transfer = new Bxml.Transfer(transferAttributes, [phoneNumber, sipUri]); + + const bxml = new Bxml.Response([tag, conference, speakSentence, gather, sendDtmf, nestedGather, pause, record, playAudio, sendDtmf, ring, bridge, transfer, hangup]); + + const { status: updateStatus } = + await callsApi.updateCallBxml(BW_ACCOUNT_ID, updateCallId, bxml.toBxml()); + expect(updateStatus).toEqual(204); + await sleep(SLEEP_TIME_S); + + const { status: completeStatus } = + await callsApi.updateCall(BW_ACCOUNT_ID, updateCallId, { state: CallStateEnum.Completed }); + expect(completeStatus).toEqual(200); + }); + + test('test start and stop verbs', async () => { + const updateCallId = await createMantecaCall(callsApi); + activeCalls.push(updateCallId); + await sleep(SLEEP_TIME_S); + + const startGatherAttributes = { + dtmfUrl: 'https://initial.com', + dtmfMethod: 'POST', + username: 'initialUsername', + password: 'initialPassword', + tag: 'initialTag' + }; + + const startRecordingAttributes = { + recordingAvailableUrl: 'https://initial.com', + recordingAvailableMethod: 'POST', + transcribe: true, + transcriptionAvailableUrl: 'https://initial.com', + transcriptionAvailableMethod: 'POST', + username: 'initialUsername', + password: 'initialPassword', + tag: 'initialTag', + fileFormat: 'wav', + multiChannel: true + }; + + const startStreamAttributes = { + name: 'initialName', + tracks: 'inbound', + destination: 'wss://initial.com', + streamEventUrl: 'https://initial.com', + streamEventMethod: 'POST', + username: 'initialUsername', + password: 'initialPassword' + }; + + const startTranscriptionAttributes = { + name: 'initialName', + tracks: 'inbound', + transcriptionEventUrl: 'https://initial.com', + transcriptionEventMethod: 'POST', + username: 'initialUsername', + password: 'initialPassword', + destination: 'wss://initial.com', + stabilized: true + }; + + const stopStreamAttributes = { + name: 'initialName' + }; + + const stopTranscriptionAttributes = { + name: 'initialName' + }; + + const customParam1 = new Bxml.CustomParam({ name: 'customParamName1', value: 'customParamValue1' }); + const streamParam1 = new Bxml.StreamParam({ name: 'streamParamName1', value: 'streamParamValue1' }); + + const startGather = new Bxml.StartGather(startGatherAttributes); + const startRecording = new Bxml.StartRecording(startRecordingAttributes); + const pauseRecording = new Bxml.PauseRecording(); + const resumeRecording = new Bxml.ResumeRecording(); + const startStream = new Bxml.StartStream(startStreamAttributes, [streamParam1]); + const startTranscription = new Bxml.StartTranscription(startTranscriptionAttributes, [customParam1]); + const stopGather = new Bxml.StopGather(); + const stopRecording = new Bxml.StopRecording(); + const stopStream = new Bxml.StopStream(stopStreamAttributes); + const stopTranscription = new Bxml.StopTranscription(stopTranscriptionAttributes); + + const bxml = new Bxml.Bxml([startGather, stopGather, startRecording, pauseRecording, resumeRecording, stopRecording, startStream, stopStream, startTranscription, stopTranscription]); + + const { status: updateStatus } = + await callsApi.updateCallBxml(BW_ACCOUNT_ID, updateCallId, bxml.toBxml()); + expect(updateStatus).toEqual(204); + await sleep(SLEEP_TIME_S); + + const { status: completeStatus } = + await callsApi.updateCall(BW_ACCOUNT_ID, updateCallId, { state: CallStateEnum.Completed }); + expect(completeStatus).toEqual(200); + }); + + test('test forward', async () => { + const updateCallId = await createMantecaCall(callsApi); + activeCalls.push(updateCallId); + await sleep(SLEEP_TIME_S); + + const forwardAttributes = { + to: '+19195551234', + from: '+19195554321', + callTimeout: 5, + diversionTreatment: 'propagate', + diversionReason: 'user-busy', + uui: '93d6f3c0be5845960b744fa28015d8ede84bd1a4;encoding=base64,asdf;encoding=jwt' + }; + + const forward = new Bxml.Forward(forwardAttributes); + + const bxml = new Bxml.Response([forward]); + + const { status: updateStatus } = + await callsApi.updateCallBxml(BW_ACCOUNT_ID, updateCallId, bxml.toBxml()); + expect(updateStatus).toEqual(204); + await sleep(SLEEP_TIME_S); + + const { status: completeStatus } = + await callsApi.updateCall(BW_ACCOUNT_ID, updateCallId, { state: CallStateEnum.Completed }); + expect(completeStatus).toEqual(200); + + }); + + test('test redirect', async () => { + const updateCallId = await createMantecaCall(callsApi); + activeCalls.push(updateCallId); + await sleep(SLEEP_TIME_S); + + const redirectAttributes = { + redirectUrl: 'https://initial.com', + redirectMethod: 'POST', + redirectFallbackUrl: 'https://initial.com', + redirectFallbackMethod: 'POST', + username: 'initialUsername', + password: 'initialPassword', + fallbackUsername: 'initialFallbackUsername', + fallbackPassword: 'initialFallbackPassword', + tag: 'initialTag' + }; + + const redirect = new Bxml.Redirect(redirectAttributes); + + const bxml = new Bxml.Response([redirect]); + + const { status: updateStatus } = + await callsApi.updateCallBxml(BW_ACCOUNT_ID, updateCallId, bxml.toBxml()); + expect(updateStatus).toEqual(204); + await sleep(SLEEP_TIME_S); + + const { status: completeStatus } = + await callsApi.updateCall(BW_ACCOUNT_ID, updateCallId, { state: CallStateEnum.Completed }); + expect(completeStatus).toEqual(200); + }); +});