Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEVEXP-528: E2E Voice/Callbacks #137

Merged
merged 9 commits into from
Sep 24, 2024
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');
});
Loading