Skip to content

Commit

Permalink
DEVEXP-528: E2E Voice/Callbacks (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
asein-sinch authored Sep 24, 2024
1 parent baa0749 commit dc0b5bc
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 11 deletions.
7 changes: 2 additions & 5 deletions examples/webhooks/src/services/voice-event.service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { Injectable } from '@nestjs/common';
import { Response } from 'express';
import {
VoiceCallback,
Voice,
} from '@sinch/sdk-core';
import { Voice } from '@sinch/sdk-core';

@Injectable()
export class VoiceEventService {

handleEvent(event: VoiceCallback, res: Response) {
handleEvent(event: Voice.VoiceCallbackEvent, res: Response) {
console.log(`:: INCOMING EVENT :: ${event.event}`);
switch (event.event) {
case 'ice':
Expand Down
1 change: 1 addition & 0 deletions packages/voice/src/models/v1/mod-callbacks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './notify-request';
export * from './pie-request';
export * from './pie-response';
export * from './callback-response';
export * from './voice-callback-event';
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type { PieRequest, MenuResult } from './pie-request';
export type { PieRequest, MenuResult, PieInformationType } from './pie-request';
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ export interface MenuResult {
/** The ID of the menu that triggered the prompt input event. */
menuId?: string;
/** The type of information that's returned. */
type?: string;
type?: PieInformationType;
/** The value of the returned information. */
value?: string;
/** The type of input received. */
inputMethod?: string;
}

export type PieInformationType = 'error' | 'return' | 'sequence' | 'timeout' | 'hangup' | 'invalidinput';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IceRequest } from './ice-request';
import { AceRequest } from './ace-request';
import { DiceRequest } from './dice-request';
import { PieRequest } from './pie-request';
import { NotifyRequest } from './notify-request';

export type VoiceCallbackEvent = IceRequest | AceRequest | DiceRequest | PieRequest | NotifyRequest;
9 changes: 5 additions & 4 deletions packages/voice/src/rest/v1/callbacks/callbacks-webhook.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { AceRequest, DiceRequest, IceRequest, NotifyRequest, PieRequest } from '../../../models';
import { AceRequest, DiceRequest, IceRequest, NotifyRequest, PieRequest, VoiceCallbackEvent } from '../../../models';
import { CallbackProcessor, SinchClientParameters, validateAuthenticationHeader } from '@sinch/sdk-client';
import { IncomingHttpHeaders } from 'http';

/** @deprecated - use Voice.VoiceCallbackEvent instead */
export type VoiceCallback = IceRequest | AceRequest | DiceRequest | PieRequest | NotifyRequest;

export class VoiceCallbackWebhooks implements CallbackProcessor<VoiceCallback>{
export class VoiceCallbackWebhooks implements CallbackProcessor<VoiceCallbackEvent>{
private readonly sinchClientParameters: SinchClientParameters;

constructor(sinchClientParameters: SinchClientParameters) {
Expand Down Expand Up @@ -38,9 +39,9 @@ export class VoiceCallbackWebhooks implements CallbackProcessor<VoiceCallback>{
* Reviver for a Voice Event.
* This method ensures the object can be treated as a Voice Event and should be called before any action is taken to manipulate the object.
* @param {any} eventBody - The event body containing the voice event notification.
* @return {VoiceCallback} - The parsed voice event object.
* @return {VoiceCallbackEvent} - The parsed voice event object.
*/
public parseEvent(eventBody: any): VoiceCallback {
public parseEvent(eventBody: any): VoiceCallbackEvent {
if (eventBody.timestamp) {
eventBody.timestamp = new Date(eventBody.timestamp);
}
Expand Down
158 changes: 158 additions & 0 deletions packages/voice/tests/rest/v1/callbacks/webhooks-events.steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { VoiceCallbackWebhooks, Voice } from '../../../../src';
import { Given, Then, When } from '@cucumber/cucumber';
import assert from 'assert';
import { IncomingHttpHeaders } from 'http';

let voiceCallbackWebhooks: VoiceCallbackWebhooks;
let rawEvent: any;
let event: Voice.VoiceCallbackEvent;
let formattedHeaders: IncomingHttpHeaders;

const processEvent = async (response: Response) => {
formattedHeaders = {};
response.headers.forEach((value, name) => {
formattedHeaders[name.toLowerCase()] = value;
});
rawEvent = await response.text();
event = voiceCallbackWebhooks.parseEvent(JSON.parse(rawEvent));
};


Given('the Voice Webhooks handler is available', () => {
voiceCallbackWebhooks = new VoiceCallbackWebhooks({
applicationKey: 'appKey',
applicationSecret: 'appSecret',
});
});

When('I send a request to trigger a "PIE" event with a "return" type', async () => {
const response = await fetch('http://localhost:3019/webhooks/voice/pie-return');
await processEvent(response);
});

Then('the header of the "PIE" event with a "return" type contains a valid authorization', () => {
assert.ok(voiceCallbackWebhooks.validateAuthenticationHeader(
formattedHeaders,
rawEvent,
'/webhooks/voice',
'POST'));
});

Then('the Voice event describes a "PIE" event with a "return" type', () => {
const pieEvent = event as Voice.PieRequest;
assert.equal(pieEvent.callid, '1ce0ffee-ca11-ca11-ca11-abcdef000013');
assert.equal(pieEvent.event, 'pie');
const menuResult: Voice.MenuResult = {
type: 'return',
value: 'cancel',
menuId: 'main',
inputMethod: 'dtmf',
};
assert.ok(pieEvent.menuResult);
assert.equal(pieEvent.menuResult.type, menuResult.type);
assert.equal(pieEvent.menuResult.value, menuResult.value);
assert.equal(pieEvent.menuResult.menuId, menuResult.menuId);
assert.equal(pieEvent.menuResult.inputMethod, menuResult.inputMethod);
assert.equal(pieEvent.version, 1);
assert.equal(pieEvent.custom, 'Custom text');
assert.equal(pieEvent.applicationKey, 'f00dcafe-abba-c0de-1dea-dabb1ed4caf3');
});

When('I send a request to trigger a "PIE" event with a "sequence" type', async () => {
const response = await fetch('http://localhost:3019/webhooks/voice/pie-sequence');
await processEvent(response);
});

Then('the header of the "PIE" event with a "sequence" type contains a valid authorization', () => {
assert.ok(voiceCallbackWebhooks.validateAuthenticationHeader(
formattedHeaders,
rawEvent,
'/webhooks/voice',
'POST'));
});

Then('the Voice event describes a "PIE" event with a "sequence" type', () => {
const pieEvent = event as Voice.PieRequest;
assert.equal(pieEvent.callid, '1ce0ffee-ca11-ca11-ca11-abcdef000023');
assert.equal(pieEvent.event, 'pie');
const menuResult: Voice.MenuResult = {
type: 'sequence',
value: '1234',
menuId: 'confirm',
inputMethod: 'dtmf',
};
assert.ok(pieEvent.menuResult);
assert.equal(pieEvent.menuResult.type, menuResult.type);
assert.equal(pieEvent.menuResult.value, menuResult.value);
assert.equal(pieEvent.menuResult.menuId, menuResult.menuId);
assert.equal(pieEvent.menuResult.inputMethod, menuResult.inputMethod);
assert.equal(pieEvent.version, 1);
assert.equal(pieEvent.custom, 'Custom text');
assert.equal(pieEvent.applicationKey, 'f00dcafe-abba-c0de-1dea-dabb1ed4caf3');
});

When('I send a request to trigger a "DICE" event', async () => {
const response = await fetch('http://localhost:3019/webhooks/voice/dice');
await processEvent(response);
});

Then('the header of the "DICE" event contains a valid authorization', () => {
assert.ok(voiceCallbackWebhooks.validateAuthenticationHeader(
formattedHeaders,
rawEvent,
'/webhooks/voice',
'POST'));
});

Then('the Voice event describes a "DICE" event', () => {
const diceEvent = event as Voice.DiceRequest;
assert.equal(diceEvent.callid, '1ce0ffee-ca11-ca11-ca11-abcdef000033');
assert.equal(diceEvent.event, 'dice');
const reason: Voice.ReasonEnum = 'MANAGERHANGUP';
assert.equal(diceEvent.reason, reason);
const result: Voice.ResultEnum = 'ANSWERED';
assert.equal(diceEvent.result, result);
assert.equal(diceEvent.version, 1);
assert.equal(diceEvent.custom, 'Custom text');
const debit: Voice.VoicePrice = {
currencyId: 'EUR',
amount: 0.0095,
};
assert.deepEqual(diceEvent.userRate, debit);
const userRate: Voice.VoicePrice = {
currencyId: 'EUR',
amount: 0.0095,
};
assert.deepEqual(diceEvent.userRate, userRate);
const destinationParticipant: Voice.Participant = {
type: 'number',
endpoint: '12017777777',
};
assert.deepEqual(diceEvent.to, destinationParticipant);
assert.equal(diceEvent.applicationKey, 'f00dcafe-abba-c0de-1dea-dabb1ed4caf3');
assert.equal(diceEvent.duration, 12);
assert.equal(diceEvent.from, '12015555555');
});

When('I send a request to trigger a "ACE" event', async () => {
const response = await fetch('http://localhost:3019/webhooks/voice/ace');
await processEvent(response);
});

Then('the header of the "ACE" event contains a valid authorization', () => {
assert.ok(voiceCallbackWebhooks.validateAuthenticationHeader(
formattedHeaders,
rawEvent,
'/webhooks/voice',
'POST'));
});

Then('the Voice event describes a "ACE" event', () => {
const aceEvent = event as Voice.AceRequest;
assert.equal(aceEvent.callid, '1ce0ffee-ca11-ca11-ca11-abcdef000043');
assert.equal(aceEvent.event, 'ace');
assert.deepEqual(aceEvent.timestamp, new Date('2024-06-06T17:10:34Z'));
assert.equal(aceEvent.version, 1);
assert.equal(aceEvent.custom, 'Custom text');
assert.equal(aceEvent.applicationKey, 'f00dcafe-abba-c0de-1dea-dabb1ed4caf3');
});

0 comments on commit dc0b5bc

Please sign in to comment.