From bc4cd2acb3fdf3e0f63115b1ef707de9dd4e72a7 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:55:20 +0200 Subject: [PATCH 1/4] DEVEXP-523: E2E Verification/Callbacks (#131) --- .../services/verification-event.service.ts | 7 +- .../tests/rest/v1/contact/contacts.steps.ts | 2 +- .../src/models/v1/identity/identity.ts | 2 - .../src/models/v1/mod-callbacks/index.ts | 1 + .../verification-callback-event.ts | 4 + .../verification-request-event/index.ts | 2 +- .../rest/v1/callbacks/callbacks-webhook.ts | 9 +- .../v1/callbacks/webhooks-events.steps.ts | 89 +++++++++++++++++++ 8 files changed, 103 insertions(+), 13 deletions(-) create mode 100644 packages/verification/src/models/v1/mod-callbacks/verification-callback-event.ts create mode 100644 packages/verification/tests/rest/v1/callbacks/webhooks-events.steps.ts diff --git a/examples/webhooks/src/services/verification-event.service.ts b/examples/webhooks/src/services/verification-event.service.ts index bb83f779..051978a7 100644 --- a/examples/webhooks/src/services/verification-event.service.ts +++ b/examples/webhooks/src/services/verification-event.service.ts @@ -1,14 +1,11 @@ import { Injectable } from '@nestjs/common'; import { Response } from 'express'; -import { - Verification, - VerificationCallback, -} from '@sinch/sdk-core'; +import { Verification } from '@sinch/sdk-core'; @Injectable() export class VerificationEventService { - handleEvent(event: VerificationCallback, res: Response) { + handleEvent(event: Verification.VerificationCallbackEvent, res: Response) { console.log(`:: INCOMING EVENT :: ${event.event}`); switch (event.event) { case 'VerificationRequestEvent': diff --git a/packages/conversation/tests/rest/v1/contact/contacts.steps.ts b/packages/conversation/tests/rest/v1/contact/contacts.steps.ts index 28cf9bc4..ad7f0969 100644 --- a/packages/conversation/tests/rest/v1/contact/contacts.steps.ts +++ b/packages/conversation/tests/rest/v1/contact/contacts.steps.ts @@ -23,7 +23,7 @@ Given('the Conversation service "Contacts" is available', function () { contactsApi = conversationService.contact; }); -When('I send a request to create an contact', async () => { +When('I send a request to create a contact', async () => { contact = await contactsApi.create({ contactCreateRequestBody: { channel_identities: [ diff --git a/packages/verification/src/models/v1/identity/identity.ts b/packages/verification/src/models/v1/identity/identity.ts index 36bbcebe..c1d44143 100644 --- a/packages/verification/src/models/v1/identity/identity.ts +++ b/packages/verification/src/models/v1/identity/identity.ts @@ -7,6 +7,4 @@ export interface Identity { type: 'number'; /** For type `number` use an [E.164](https://community.sinch.com/t5/Glossary/E-164/ta-p/7537)-compatible phone number. */ endpoint: string; - /** */ - verified?: boolean; } diff --git a/packages/verification/src/models/v1/mod-callbacks/index.ts b/packages/verification/src/models/v1/mod-callbacks/index.ts index 08d3d4ac..4f251c46 100644 --- a/packages/verification/src/models/v1/mod-callbacks/index.ts +++ b/packages/verification/src/models/v1/mod-callbacks/index.ts @@ -1,3 +1,4 @@ +export * from './verification-callback-event'; // 'Verification Request Event' received from Sinch server export * from './verification-request-event'; // Response to send to Sinch server for a 'Verification Request Event' diff --git a/packages/verification/src/models/v1/mod-callbacks/verification-callback-event.ts b/packages/verification/src/models/v1/mod-callbacks/verification-callback-event.ts new file mode 100644 index 00000000..2ce27cad --- /dev/null +++ b/packages/verification/src/models/v1/mod-callbacks/verification-callback-event.ts @@ -0,0 +1,4 @@ +import { VerificationRequestEvent } from './verification-request-event'; +import { VerificationResultEvent } from './verification-result-event'; + +export type VerificationCallbackEvent = VerificationRequestEvent | VerificationResultEvent; diff --git a/packages/verification/src/models/v1/mod-callbacks/verification-request-event/index.ts b/packages/verification/src/models/v1/mod-callbacks/verification-request-event/index.ts index 1fb30a07..7b8a4b6f 100644 --- a/packages/verification/src/models/v1/mod-callbacks/verification-request-event/index.ts +++ b/packages/verification/src/models/v1/mod-callbacks/verification-request-event/index.ts @@ -1 +1 @@ -export type { VerificationRequestEvent } from './verification-request-event'; +export type { VerificationRequestEvent, MethodEnum } from './verification-request-event'; diff --git a/packages/verification/src/rest/v1/callbacks/callbacks-webhook.ts b/packages/verification/src/rest/v1/callbacks/callbacks-webhook.ts index 5b8ff98a..5e66d930 100644 --- a/packages/verification/src/rest/v1/callbacks/callbacks-webhook.ts +++ b/packages/verification/src/rest/v1/callbacks/callbacks-webhook.ts @@ -1,10 +1,11 @@ -import { VerificationRequestEvent, VerificationResultEvent } from '../../../models'; +import { VerificationCallbackEvent, VerificationRequestEvent, VerificationResultEvent } from '../../../models'; import { CallbackProcessor, SinchClientParameters, validateAuthenticationHeader } from '@sinch/sdk-client'; import { IncomingHttpHeaders } from 'http'; +/** @deprecated - use Verification.VerificationCallback instead */ export type VerificationCallback = VerificationRequestEvent | VerificationResultEvent; -export class VerificationCallbackWebhooks implements CallbackProcessor{ +export class VerificationCallbackWebhooks implements CallbackProcessor{ private readonly sinchClientParameters: SinchClientParameters; @@ -39,9 +40,9 @@ export class VerificationCallbackWebhooks implements CallbackProcessor { + formattedHeaders = {}; + response.headers.forEach((value, name) => { + formattedHeaders[name.toLowerCase()] = value; + }); + rawEvent = await response.text(); + event = verificationCallbackWebhook.parseEvent(JSON.parse(rawEvent)); +}; + +Given('the Verification Webhooks handler is available', () => { + verificationCallbackWebhook = new VerificationCallbackWebhooks({ + applicationKey: 'appKey', + applicationSecret: 'appSecret', + }); +}); + +When('I send a request to trigger a "Verification Request" event', async () => { + const response = await fetch('http://localhost:3018/webhooks/verification/verification-request-event'); + await processEvent(response); +}); + +Then('the header of the Verification event "Verification Request" contains a valid authorization', () => { + assert.ok(verificationCallbackWebhook.validateAuthenticationHeader( + formattedHeaders, + rawEvent, + '/webhooks/verification', + 'POST')); +}); + +Then('the Verification event describes a "Verification Request" event type', () => { + const verificationRequestEvent = event as Verification.VerificationRequestEvent; + assert.equal(verificationRequestEvent.id, '1ce0ffee-c0de-5eed-d00d-f00dfeed1337'); + assert.equal(verificationRequestEvent.event, 'VerificationRequestEvent'); + const smsVerificationMethod: Verification.MethodEnum = 'sms'; + assert.equal(verificationRequestEvent.method, smsVerificationMethod); + const identity: Verification.Identity = { + type: 'number', + endpoint: '+33612345678', + }; + assert.equal(verificationRequestEvent.identity.type, identity.type); + assert.equal(verificationRequestEvent.identity.endpoint, identity.endpoint); + const smsPrice: Verification.Price = { + currencyId: 'EUR', + amount: 0.0453, + }; + assert.deepEqual(verificationRequestEvent.price, smsPrice); + const smsRate: Verification.Price = { + currencyId: 'EUR', + amount: 0, + }; + assert.deepEqual(verificationRequestEvent.rate, smsRate); +}); + +When('I send a request to trigger a "Verification Result" event', async () => { + const response = await fetch('http://localhost:3018/webhooks/verification/verification-result-event'); + await processEvent(response); +}); + +Then('the header of the Verification event "Verification Result" contains a valid authorization', () => { + assert.ok(verificationCallbackWebhook.validateAuthenticationHeader( + formattedHeaders, + rawEvent, + '/webhooks/verification', + 'POST')); +}); + +Then('the Verification event describes a "Verification Result" event type', () => { + const verificationRequestEvent = event as Verification.VerificationResultEvent; + assert.equal(verificationRequestEvent.id, '1ce0ffee-c0de-5eed-d00d-f00dfeed1337'); + assert.equal(verificationRequestEvent.event, 'VerificationResultEvent'); + const smsVerificationMethod: Verification.MethodEnum = 'sms'; + assert.equal(verificationRequestEvent.method, smsVerificationMethod); + const identity: Verification.Identity = { + type: 'number', + endpoint: '+33612345678', + }; + assert.equal(verificationRequestEvent.identity.type, identity.type); + assert.equal(verificationRequestEvent.identity.endpoint, identity.endpoint); +}); From 5d22da2c70199fd659f551be1373bf013ecf28a3 Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:37:42 +0200 Subject: [PATCH 2/4] DEVEXP-550_Single cucumber statement for conversation webhooks events (#132) --- .../webhooks-events/webhooks-events.steps.ts | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/conversation/tests/rest/v1/webhooks-events/webhooks-events.steps.ts b/packages/conversation/tests/rest/v1/webhooks-events/webhooks-events.steps.ts index 82f87357..01b8334b 100644 --- a/packages/conversation/tests/rest/v1/webhooks-events/webhooks-events.steps.ts +++ b/packages/conversation/tests/rest/v1/webhooks-events/webhooks-events.steps.ts @@ -15,7 +15,6 @@ const processEvent = async (response: Response) => { formattedHeaders[name.toLowerCase()] = value; }); rawEvent = await response.text(); - rawEvent = rawEvent.replace(/\s+/g, ''); event = conversationCallbackWebhook.parseEvent(JSON.parse(rawEvent)); }; @@ -23,15 +22,16 @@ Given('the Conversation Webhooks handler is available', () => { conversationCallbackWebhook = new ConversationCallbackWebhooks(APP_SECRET); }); -Then('the Conversation event header contains a valid signature', () => { - assert.ok(conversationCallbackWebhook.validateAuthenticationHeader(formattedHeaders, rawEvent)); -}); - When('I send a request to trigger a "CAPABILITY" event', async () => { const response = await fetch('http://localhost:3014/webhooks/conversation/capability-lookup'); await processEvent(response); }); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +Then('the header of the Conversation event {string} contains a valid signature', (_event) => { + assert.ok(conversationCallbackWebhook.validateAuthenticationHeader(formattedHeaders, rawEvent)); +}); + Then('the Conversation event describes a "CAPABILITY" event type', () => { const capabilityEvent = event as Conversation.CapabilityEvent; assert.ok(capabilityEvent.capability_notification); @@ -123,11 +123,16 @@ Then('the Conversation event describes a "CONVERSATION_STOP" event type', () => assert.equal(conversationStopEvent.trigger, expectedTrigger); }); -When('I send a request to trigger a "EVENT_DELIVERY" event with a FAILED status', async () => { +When('I send a request to trigger a "EVENT_DELIVERY" event with a "FAILED" status', async () => { const response = await fetch('http://localhost:3014/webhooks/conversation/event-delivery-report/failed'); await processEvent(response); }); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +Then('the header of the Conversation event {} with a {} status contains a valid signature', (_event, _status) => { + assert.ok(conversationCallbackWebhook.validateAuthenticationHeader(formattedHeaders, rawEvent)); +}); + Then('the Conversation event describes a "EVENT_DELIVERY" event type', () => { const eventDeliveryEvent = event as Conversation.EventDelivery; assert.ok(eventDeliveryEvent.event_delivery_report); @@ -144,7 +149,7 @@ Then('the Conversation event describes a FAILED event delivery status and its re assert.equal(eventDeliveryReport.reason.code, expectedReasonCode); }); -When('I send a request to trigger a "EVENT_DELIVERY" event with a DELIVERED status', async () => { +When('I send a request to trigger a "EVENT_DELIVERY" event with a "DELIVERED" status', async () => { const response = await fetch('http://localhost:3014/webhooks/conversation/event-delivery-report/succeeded'); await processEvent(response); }); @@ -161,12 +166,12 @@ Then('the Conversation event describes a "EVENT_INBOUND" event type', () => { assert.equal(eventInbound.trigger, expectedTrigger); }); -When('I send a request to trigger a "MESSAGE_DELIVERY" event with a FAILED status', async () => { +When('I send a request to trigger a "MESSAGE_DELIVERY" event with a "FAILED" status', async () => { const response = await fetch('http://localhost:3014/webhooks/conversation/message-delivery-report/failed'); await processEvent(response); }); -When('I send a request to trigger a "MESSAGE_DELIVERY" event with a QUEUED status', async () => { +When('I send a request to trigger a "MESSAGE_DELIVERY" event with a "QUEUED_ON_CHANNEL" status', async () => { const response = await fetch('http://localhost:3014/webhooks/conversation/message-delivery-report/succeeded'); await processEvent(response); }); @@ -212,12 +217,17 @@ Then('the Conversation event describes a "MESSAGE_INBOUND_SMART_CONVERSATION_RED assert.equal(messageInboundSmartConversationRedactionEvent.trigger, expectedTrigger); }); -When('I send a request to trigger a "MESSAGE_SUBMIT" event for a media message', async () => { +When('I send a request to trigger a "MESSAGE_SUBMIT" event for a "media" message', async () => { const response = await fetch('http://localhost:3014/webhooks/conversation/message-submit/media'); await processEvent(response); }); -Then('the Conversation event describes a "MESSAGE_SUBMIT" event type for a media message', () => { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +Then('the header of the Conversation event {} for a {} message contains a valid signature', (_event, _messageType) => { + assert.ok(conversationCallbackWebhook.validateAuthenticationHeader(formattedHeaders, rawEvent)); +}); + +Then('the Conversation event describes a "MESSAGE_SUBMIT" event type for a "media" message', () => { const messageSubmitEvent = event as Conversation.MessageSubmitEvent; assert.ok(messageSubmitEvent.message_submit_notification); const expectedTrigger: Conversation.WebhookTrigger = 'MESSAGE_SUBMIT'; @@ -225,12 +235,12 @@ Then('the Conversation event describes a "MESSAGE_SUBMIT" event type for a media assert.ok(messageSubmitEvent.message_submit_notification.submitted_message?.media_message); }); -When('I send a request to trigger a "MESSAGE_SUBMIT" event for a text message', async () => { +When('I send a request to trigger a "MESSAGE_SUBMIT" event for a "text" message', async () => { const response = await fetch('http://localhost:3014/webhooks/conversation/message-submit/text'); await processEvent(response); }); -Then('the Conversation event describes a "MESSAGE_SUBMIT" event type for a text message', () => { +Then('the Conversation event describes a "MESSAGE_SUBMIT" event type for a "text" message', () => { const messageSubmitEvent = event as Conversation.MessageSubmitEvent; assert.ok(messageSubmitEvent.message_submit_notification); const expectedTrigger: Conversation.WebhookTrigger = 'MESSAGE_SUBMIT'; @@ -238,12 +248,12 @@ Then('the Conversation event describes a "MESSAGE_SUBMIT" event type for a text assert.ok(messageSubmitEvent.message_submit_notification.submitted_message?.text_message); }); -When('I send a request to trigger a "SMART_CONVERSATIONS" event for a media message', async () => { - const response = await fetch('http://localhost:3014/webhooks/conversation/smart-conversation/media'); +When('I send a request to trigger a "SMART_CONVERSATIONS" event for a "media" message', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/smart-conversations/media'); await processEvent(response); }); -Then('the Conversation event describes a "SMART_CONVERSATIONS" event type for a media message', () => { +Then('the Conversation event describes a "SMART_CONVERSATIONS" event type for a "media" message', () => { const smartConversationsEvent = event as Conversation.SmartConversationsEvent; assert.ok(smartConversationsEvent.smart_conversation_notification); const expectedTrigger: Conversation.WebhookTrigger = 'SMART_CONVERSATIONS'; @@ -252,12 +262,12 @@ Then('the Conversation event describes a "SMART_CONVERSATIONS" event type for a assert.ok(smartConversationsEvent.smart_conversation_notification.analysis_results?.ml_offensive_analysis_result); }); -When('I send a request to trigger a "SMART_CONVERSATIONS" event for a text message', async () => { - const response = await fetch('http://localhost:3014/webhooks/conversation/smart-conversation/text'); +When('I send a request to trigger a "SMART_CONVERSATIONS" event for a "text" message', async () => { + const response = await fetch('http://localhost:3014/webhooks/conversation/smart-conversations/text'); await processEvent(response); }); -Then('the Conversation event describes a "SMART_CONVERSATIONS" event type for a text message', () => { +Then('the Conversation event describes a "SMART_CONVERSATIONS" event type for a "text" message', () => { const smartConversationsEvent = event as Conversation.SmartConversationsEvent; assert.ok(smartConversationsEvent.smart_conversation_notification); const expectedTrigger: Conversation.WebhookTrigger = 'SMART_CONVERSATIONS'; From 4b3a899faea5bc96fb5a76ebcdeabf32bcf85bcc Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Mon, 23 Sep 2024 20:35:35 +0200 Subject: [PATCH 3/4] DEVEXP-524: E2E Voice/Callouts (#133) --- .github/workflows/run-ci.yaml | 11 +- packages/voice/cucumber.js | 8 ++ packages/voice/package.json | 3 +- .../conference-callout-request.ts | 3 +- .../tests/rest/v1/callouts/callouts.steps.ts | 121 ++++++++++++++++++ 5 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 packages/voice/cucumber.js create mode 100644 packages/voice/tests/rest/v1/callouts/callouts.steps.ts diff --git a/.github/workflows/run-ci.yaml b/.github/workflows/run-ci.yaml index 846bee5f..cae140be 100644 --- a/.github/workflows/run-ci.yaml +++ b/.github/workflows/run-ci.yaml @@ -32,10 +32,6 @@ jobs: token: ${{ secrets.PAT_CI }} fetch-depth: 0 path: sinch-sdk-mockserver - - name: Build custom Docker image - run: | - cd sinch-sdk-mockserver - docker build -t sinch-sdk-mockserver -f Dockerfile . - name: Install Docker Compose run: | sudo apt-get update @@ -44,11 +40,6 @@ jobs: run: | cd sinch-sdk-mockserver docker-compose up -d - - name: Wait for the mock servers to be healthy - run: | - cd sinch-sdk-mockserver - chmod +x ./scripts/healthcheck.sh - ./scripts/healthcheck.sh - name: Create target directories for feature files run: | mkdir -p ./packages/fax/tests/e2e/features @@ -57,6 +48,7 @@ jobs: mkdir -p ./packages/elastic-sip-trunking/tests/e2e/features mkdir -p ./packages/sms/tests/e2e/features mkdir -p ./packages/verification/tests/e2e/features + mkdir -p ./packages/voice/tests/e2e/features - name: Copy feature files run: | cp sinch-sdk-mockserver/features/fax/*.feature ./packages/fax/tests/e2e/features/ @@ -65,6 +57,7 @@ jobs: cp sinch-sdk-mockserver/features/elastic-sip-trunking/*.feature ./packages/elastic-sip-trunking/tests/e2e/features/ cp sinch-sdk-mockserver/features/sms/*.feature ./packages/sms/tests/e2e/features/ cp sinch-sdk-mockserver/features/verification/*.feature ./packages/verification/tests/e2e/features/ + cp sinch-sdk-mockserver/features/voice/*.feature ./packages/voice/tests/e2e/features/ - name: Run e2e tests run: | yarn install diff --git a/packages/voice/cucumber.js b/packages/voice/cucumber.js new file mode 100644 index 00000000..691a9809 --- /dev/null +++ b/packages/voice/cucumber.js @@ -0,0 +1,8 @@ +module.exports = { + default: [ + 'tests/e2e/features/**/*.feature', + '--require-module ts-node/register', + '--require tests/rest/v1/**/*.steps.ts', + `--format-options '{"snippetInterface": "synchronous"}'`, + ].join(' '), +}; diff --git a/packages/voice/package.json b/packages/voice/package.json index 473628fc..3a5528f4 100644 --- a/packages/voice/package.json +++ b/packages/voice/package.json @@ -25,7 +25,8 @@ "scripts": { "build": "yarn run clean && yarn run compile", "clean": "rimraf dist tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo", - "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests tsconfig.build.tsbuildinfo" + "compile": "tsc -p tsconfig.build.json && tsc -p tsconfig.tests.json && rimraf dist/tests tsconfig.build.tsbuildinfo", + "test:e2e": "cucumber-js" }, "dependencies": { "@sinch/sdk-client": "^1.1.0" diff --git a/packages/voice/src/models/v1/conference-callout-request/conference-callout-request.ts b/packages/voice/src/models/v1/conference-callout-request/conference-callout-request.ts index 6d7583da..54a96829 100644 --- a/packages/voice/src/models/v1/conference-callout-request/conference-callout-request.ts +++ b/packages/voice/src/models/v1/conference-callout-request/conference-callout-request.ts @@ -1,5 +1,6 @@ import { Destination } from '../destination'; import { ConferenceDtmfOptions } from '../conference-dtmf-options'; +import { MusicOnHold } from '../enums'; /** * The conference callout calls a phone number or a user. When the call is answered, it's connected to a conference room. @@ -28,7 +29,7 @@ export interface ConferenceCalloutRequest { /** The text that will be spoken as a greeting. */ greeting?: string; /** Means "music-on-hold." It's an optional parameter that specifies what the first participant should listen to while they're alone in the conference, waiting for other participants to join. It can take one of these pre-defined values:
  • `ring` (progress tone)
  • `music1` (music file)
  • `music2` (music file)
  • `music3` (music file)

If no “music-on-hold” is specified, the user will only hear silence. */ - mohClass?: string; + mohClass?: MusicOnHold; /** Used to input custom data. */ custom?: string; /** can be either “pstn” for PSTN endpoint or “mxp” for data (app or web) clients. */ diff --git a/packages/voice/tests/rest/v1/callouts/callouts.steps.ts b/packages/voice/tests/rest/v1/callouts/callouts.steps.ts new file mode 100644 index 00000000..4f2662dd --- /dev/null +++ b/packages/voice/tests/rest/v1/callouts/callouts.steps.ts @@ -0,0 +1,121 @@ +import { CalloutsApi, VoiceService, Voice } from '../../../../src'; +import { Given, When, Then } from '@cucumber/cucumber'; +import * as assert from 'assert'; + +let calloutsApi: CalloutsApi; +let ttsCallResponse: Voice.CalloutResponse; + +Given('the Voice service "Callouts" is available', () => { + const voiceService = new VoiceService({ + applicationKey: 'appKey', + applicationSecret: 'appSecret', + voiceHostname: 'http://localhost:3019', + }); + calloutsApi = voiceService.callouts; +}); + +When('I send a request to make a TTS call', async () => { + ttsCallResponse = await calloutsApi.tts({ + ttsCalloutRequestBody: { + method: 'ttsCallout', + ttsCallout: { + cli: '+12015555555', + destination: { + type: 'number', + endpoint: '+12017777777', + }, + locale: 'en-US', + text: 'Hello, this is a call from Sinch.', + }, + }, + }); +}); + +Then('the callout response contains the TTS call ID', () => { + assert.equal(ttsCallResponse.callId, '1ce0ffee-ca11-ca11-ca11-abcdef000001'); +}); + +When('I send a request to make a Conference call with the "Callout" service', async () => { + ttsCallResponse = await calloutsApi.conference({ + conferenceCalloutRequestBody: { + method: 'conferenceCallout', + conferenceCallout: { + cli: '+12015555555', + destination: { + type: 'number', + endpoint: '+12017777777', + }, + conferenceId: 'myConferenceId-E2E', + locale: 'en-US', + greeting: 'Welcome to this conference call.', + mohClass: 'music1', + }, + }, + }); +}); + +Then('the callout response contains the Conference call ID', () => { + assert.equal(ttsCallResponse.callId, '1ce0ffee-ca11-ca11-ca11-abcdef000002'); +}); + +When('I send a request to make a Custom call', async () => { + ttsCallResponse = await calloutsApi.custom({ + customCalloutRequestBody: { + method: 'customCallout', + customCallout: { + cli: '+12015555555', + destination: { + type: 'number', + endpoint: '+12017777777', + }, + custom: 'Custom text', + ice: Voice.customCalloutHelper.formatIceResponse( + Voice.iceActionHelper.connectPstn({ + number: '+12017777777', + cli: '+12015555555', + }), + Voice.iceInstructionHelper.say('Welcome to Sinch.', 'en-US/male'), + Voice.iceInstructionHelper.startRecording({ + destinationUrl: 'To specify', + credentials: 'To specify', + }), + ), + ace: Voice.customCalloutHelper.formatAceResponse( + Voice.aceActionHelper.runMenu({ + locale: 'Kimberly', + enableVoice: true, + barge: true, + menus: [ + { + id: 'main', + mainPrompt: '#tts[Welcome to the main menu. Press 1 to confirm order or 2 to cancel]', + repeatPrompt: '#tts[We didn\'t get your input, please try again]', + timeoutMills: 5000, + options: [ + { + dtmf: '1', + action: 'menu(confirm)', + }, + { + dtmf: '2', + action: 'return(cancel)', + }, + ], + }, + { + id: 'confirm', + mainPrompt: '#tts[Thank you for confirming your order. Enter your 4-digit PIN.]', + maxDigits: 4, + }, + ], + }), + ), + pie: 'https://callback-server.com/voice', + }, + }, + }); +}); + +Then('the callout response contains the Custom call ID', () => { + assert.equal(ttsCallResponse.callId, '1ce0ffee-ca11-ca11-ca11-abcdef000003'); +}); From dae67649a13aaa0b73d99a2cb1a2fc18d79acf8d Mon Sep 17 00:00:00 2001 From: Antoine SEIN <142824551+asein-sinch@users.noreply.github.com> Date: Mon, 23 Sep 2024 20:46:46 +0200 Subject: [PATCH 4/4] DEVEXP-525: E2E Voice/Calls (#134) --- .../src/models/v1/participant/participant.ts | 2 +- .../voice/tests/rest/v1/calls/calls.steps.ts | 118 ++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 packages/voice/tests/rest/v1/calls/calls.steps.ts diff --git a/packages/voice/src/models/v1/participant/participant.ts b/packages/voice/src/models/v1/participant/participant.ts index 82124509..004b70b1 100644 --- a/packages/voice/src/models/v1/participant/participant.ts +++ b/packages/voice/src/models/v1/participant/participant.ts @@ -4,7 +4,7 @@ export interface Participant { /** The type of the participant (caller or callee). */ - type?: string; + type?: 'number' | 'Number' | 'username' | 'Username' | 'sip' | 'did' | string; /** The phone number, user name, or other identifier of the participant (caller or callee). */ endpoint?: string; } diff --git a/packages/voice/tests/rest/v1/calls/calls.steps.ts b/packages/voice/tests/rest/v1/calls/calls.steps.ts new file mode 100644 index 00000000..d16d218f --- /dev/null +++ b/packages/voice/tests/rest/v1/calls/calls.steps.ts @@ -0,0 +1,118 @@ +import { CallsApi, VoiceService, Voice } from '../../../../src'; +import { Given, When, Then } from '@cucumber/cucumber'; +import * as assert from 'assert'; + +let callsApi: CallsApi; +let callInformation: Voice.GetCallInformation; +let updateCallResponse: void; +let error: any; +let manageWithCallLegResponse: void; + +Given('the Voice service "Calls" is available', () => { + const voiceService = new VoiceService({ + applicationKey: 'appKey', + applicationSecret: 'appSecret', + voiceHostname: 'http://localhost:3019', + }); + callsApi = voiceService.calls; +}); + +When('I send a request to get a call\'s information', async () => { + callInformation = await callsApi.get({ + callId: '1ce0ffee-ca11-ca11-ca11-abcdef000003', + }); +}); + +Then('the response contains the information about the call', () => { + assert.equal(callInformation.callId, '1ce0ffee-ca11-ca11-ca11-abcdef000003'); + const participant: Voice.Participant = { + type: 'Number', + endpoint: '+12017777777', + }; + assert.deepEqual(callInformation.to, participant); + assert.equal(callInformation.domain, 'pstn'); + assert.equal(callInformation.duration, 14); + assert.equal(callInformation.status, 'FINAL'); + const result: Voice.ResultEnum = 'ANSWERED'; + assert.equal(callInformation.result, result); + const reason: Voice.ReasonEnum = 'MANAGERHANGUP'; + assert.equal(callInformation.reason, reason); + assert.deepEqual(callInformation.timestamp, new Date('2024-06-06T17:36:00Z')); + assert.equal(callInformation.custom, 'Custom text'); + const userRate: Voice.VoicePrice = { + currencyId: 'EUR', + amount: 0.1758, + }; + assert.deepEqual(callInformation.userRate, userRate); + const debit: Voice.VoicePrice = { + currencyId: 'EUR', + amount: 0.1758, + }; + assert.deepEqual(callInformation.debit, debit); +}); + +When('I send a request to update a call', async () => { + updateCallResponse = await callsApi.update({ + callId: '1ce0ffee-ca11-ca11-ca11-abcdef000022', + updateCallRequestBody: { + instructions: [ + Voice.svamlInstructionHelper.buildSay( + 'Sorry, the conference has been cancelled. The call will end now.', + 'en-US', + ), + ], + action: Voice.svamlActionHelper.buildHangup(), + }, + }); +}); + +Then('the update call response contains no data', () => { + assert.deepEqual(updateCallResponse, {}); +}); + +When('I send a request to update a call that doesn\'t exist', async () => { + updateCallResponse = undefined; + try { + updateCallResponse = await callsApi.update({ + callId: 'not-existing-callId', + updateCallRequestBody: { + instructions: [ + Voice.svamlInstructionHelper.buildSay( + 'Sorry, the conference has been cancelled. The call will end now.', + 'en-US', + ), + ], + action: Voice.svamlActionHelper.buildHangup(), + }, + }); + } catch (e) { + error = e; + } +}); + +Then('the update call response contains a "not found" error', () => { + assert.equal(updateCallResponse, undefined); + const voiceError = JSON.parse(error.data) as Voice.VoiceError; + assert.equal(voiceError.errorCode, 40400); + assert.equal(voiceError.message, 'Call not found'); + assert.equal(voiceError.reference, '38188074-abcd-56ab-ab64-daf82fada8e8'); +}); + +When('I send a request to manage a call with callLeg', async () => { + manageWithCallLegResponse = await callsApi.manageWithCallLeg({ + callId: '1ce0ffee-ca11-ca11-ca11-abcdef000032', + callLeg: 'callee', + manageWithCallLegRequestBody: { + instructions: [ + Voice.svamlInstructionHelper.buildPlayFiles( + ['https://samples-files.com/samples/Audio/mp3/sample-file-4.mp3'], + ), + ], + action: Voice.svamlActionHelper.buildContinue(), + }, + }); +}); + +Then('the manage a call with callLeg response contains no data', () => { + assert.deepEqual(manageWithCallLegResponse, {}); +});