Skip to content

Commit

Permalink
feat(video-conferencing-service): SFO-103,SFO-107 implement GET atten…
Browse files Browse the repository at this point in the history
…dee list, allow webhook to handle more events (#44)

* feat(video-conferencing-service): sFO-103 implement GET attendee list

SFO-103

* refactor(video-conferencing-service): sFO-103 fix sonar issue

SFO-103

* test(video-conferencing-service): sFO-103 add unit test for GET attendees

SFO-103

* fix(video-conferencing-service): sFO-103 able to get attendees for session that has ended

SFO-103

* test(video-conferencing-service): sFO-103 remove test for get attendees which checks error for finis

SFO-103

* feat(video-conferencing-service): sFO-107 allow webhook to handle more events

SFO-107

* fix(video-conferencing-service): sFO-103 SFO-107 fix GET attendees and webhook

SFO-103, SFO-107

* refactor(video-conferencing-service): sFO-103 SFO-107 fix sonar issues

SFO-103, SFO-107

* refactor(video-conferencing-service): sFO-103 SFO-107 fix sonar issues

SFO-103 SFO-107

Co-authored-by: samarpan-b <[email protected]>
  • Loading branch information
anitsingh-sf and samarpan-b authored Jul 10, 2020
1 parent 58f821d commit e2b7fad
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 23 deletions.
30 changes: 29 additions & 1 deletion services/video-conferencing-service/src/__tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {VonageEnums} from '../enums/video-chat.enum';
import moment from 'moment';
import {sinon} from '@loopback/testlab';

const meetingLink = 'dummy-meeting-link';
const meetingLink = 'dummy-meeting-link-id';
const sessionId = 'dummy-session-id';

export function getVideoChatSession(
Expand Down Expand Up @@ -214,6 +214,17 @@ export function getWebhookPayload(
);
}

export const stream = {
id: 'd053fcc8-c681-41d5-8ec2-7a9e1434a21f',
createdAt: 1591599253840,
connection: {
id: 'd053fcc8-c681-41d5-8ec2-7a9e1434a21f',
createdAt: 2470257688144,
data: 'TOKENDATA',
},
videoType: 'camera',
};

export function getSessionAttendeesModel() {
const data = {
sessionId: sessionId,
Expand All @@ -223,3 +234,20 @@ export function getSessionAttendeesModel() {
};
return new SessionAttendees(data);
}

export function getAttendeesList() {
return [
new SessionAttendees({
sessionId: sessionId,
attendee: 'User1',
createdOn: getDate('July 01, 2019 00:00:00'),
isDeleted: false,
}),
new SessionAttendees({
sessionId: sessionId,
attendee: 'User2',
createdOn: getDate('July 01, 2019 00:00:00'),
isDeleted: false,
}),
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
getVideoChatSession,
getWebhookPayload,
setUpMockProvider,
getAttendeesList,
stream,
} from '../../helpers';

describe('Session APIs', () => {
Expand Down Expand Up @@ -278,14 +280,23 @@ describe('Session APIs', () => {
sinon.assert.calledOnce(auidtLogCreate);
});

it('updates the attendee for event connectionCreated when attendee re-connects', async () => {
it('updates the metaData and isDeleted status for event connectionCreated if the attendee already exists', async () => {
setUp({});
const webhookPayload = getWebhookPayload({});
const findOne = sessionAttendeesRepo.stubs.findOne;
findOne.resolves(getSessionAttendeesModel());
const updateById = sessionAttendeesRepo.stubs.updateById;
updateById.resolves();
await controller.checkWebhookPayload(webhookPayload);
sinon.assert.calledOnce(updateById);
sinon.assert.calledOnce(auidtLogCreate);
});

it('updates the metaData and isDeleted status for event connectionDestroyed if the attendee already exists', async () => {
setUp({});
const webhookPayload = getWebhookPayload({
connection: {
id: 'd053fcc8-c681-41d5-8ec2-7a9e1434a21f',
createdAt: 2470257688144,
data: 'TOKENDATA',
},
event: 'connectionDestroyed',
reason: 'clientDisconnected',
});
const findOne = sessionAttendeesRepo.stubs.findOne;
findOne.resolves(getSessionAttendeesModel());
Expand All @@ -296,17 +307,76 @@ describe('Session APIs', () => {
sinon.assert.calledOnce(auidtLogCreate);
});

it('audit logs for any other event', async () => {
it('updates the metaData and isDeleted status for event streamCreated if the attendee already exists', async () => {
setUp({});
const webhookPayload = getWebhookPayload({
event: 'connectionDestroyed',
event: 'streamCreated',
stream: stream,
});
const findOne = sessionAttendeesRepo.stubs.findOne;
findOne.resolves(getSessionAttendeesModel());
const updateById = sessionAttendeesRepo.stubs.updateById;
updateById.resolves();
await controller.checkWebhookPayload(webhookPayload);
sinon.assert.calledOnce(updateById);
sinon.assert.calledOnce(auidtLogCreate);
});

it('updates the metaData and isDeleted status for event streamDestroyed if the attendee already exists', async () => {
setUp({});
const webhookPayload = getWebhookPayload({
event: 'streamDestroyed',
reason: 'clientDisconnected',
stream: stream,
});
const findOne = sessionAttendeesRepo.stubs.findOne;
findOne.resolves(getSessionAttendeesModel());
const updateById = sessionAttendeesRepo.stubs.updateById;
updateById.resolves();
await controller.checkWebhookPayload(webhookPayload);
sinon.assert.calledOnce(updateById);
sinon.assert.calledOnce(auidtLogCreate);
});
});

describe('GET /session/{meetingLinkId}/attendees', () => {
it('returns a list of all attendees given a valid meeting link', async () => {
setUp({});
const findOne = videoChatSessionRepo.stubs.findOne;
findOne.resolves(getVideoChatSession({}));
const find = sessionAttendeesRepo.stubs.find;
find.resolves(getAttendeesList());
const result = await controller.getAttendeesList(meetingLinkId, 'false');
expect(result).to.eql(getAttendeesList());
sinon.assert.calledWith(findOne, {where: {meetingLink: meetingLinkId}});
sinon.assert.calledWith(find, {where: {sessionId: 'dummy-session-id'}});
});

it('returns an error if the meeting link does not exist', async () => {
setUp({});
const findOne = videoChatSessionRepo.stubs.findOne;
findOne.resolves();
const error = await controller
.getAttendeesList(meetingLinkId, 'false')
.catch(err => err);
expect(error).instanceOf(Error);
});

it('returns active attendees only if areActive is true', async () => {
setUp({});
const findOne = videoChatSessionRepo.stubs.findOne;
findOne.resolves(getVideoChatSession({}));
const find = sessionAttendeesRepo.stubs.find;
find.resolves(getAttendeesList());
const result = await controller.getAttendeesList(meetingLinkId, 'true');
expect(result).to.eql(getAttendeesList());
sinon.assert.calledWith(findOne, {where: {meetingLink: meetingLinkId}});
sinon.assert.calledWith(find, {
where: {sessionId: 'dummy-session-id', isDeleted: false},
});
});
});

function setUp(providerStub: Partial<VideoChatInterface>) {
config = {
apiKey: 'dummy',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {inject} from '@loopback/context';
import {repository} from '@loopback/repository';
import {param, patch, post, requestBody, HttpErrors} from '@loopback/rest';
import {param, patch, post, requestBody, HttpErrors, get} from '@loopback/rest';
import {authorize} from 'loopback4-authorization';
import {
MeetingOptions,
Expand All @@ -14,7 +14,7 @@ import {PermissionKeys} from '../enums/permission-keys.enum';
import {STATUS_CODE, CONTENT_TYPE} from '@sourceloop/core';
import moment from 'moment';
import cryptoRandomString from 'crypto-random-string';
import {VideoChatSession} from '../models';
import {VideoChatSession, SessionAttendees} from '../models';
import {
AuditLogsRepository,
VideoChatSessionRepository,
Expand Down Expand Up @@ -275,28 +275,58 @@ export class VideoChatSessionController {
sessionId,
} = webhookPayload;

if (event === VonageEnums.SessionWebhookEvents.ConnectionCreated) {
const sessionAttendeeDetail = await this.sessionAttendeesRepository.findOne(
{
where: {
attendee: data,
},
const sessionAttendeeDetail = await this.sessionAttendeesRepository.findOne(
{
where: {
sessionId: sessionId,
attendee: data,
},
);
if (!sessionAttendeeDetail) {
},
);
if (!sessionAttendeeDetail) {
if (event === VonageEnums.SessionWebhookEvents.ConnectionCreated) {
await this.sessionAttendeesRepository.create({
sessionId: sessionId,
attendee: data,
createdOn: new Date(),
isDeleted: false,
extMetadata: {webhookPayload: webhookPayload},
});
} else {
}
} else {
const updatedAttendee = {
modifiedOn: new Date(),
isDeleted: sessionAttendeeDetail.isDeleted,
extMetadata: {webhookPayload: webhookPayload},
};

if (event === VonageEnums.SessionWebhookEvents.ConnectionCreated) {
updatedAttendee.isDeleted = false;
await this.sessionAttendeesRepository.updateById(
sessionAttendeeDetail.id,
{
modifiedOn: new Date(),
},
updatedAttendee,
);
} else if (event === VonageEnums.SessionWebhookEvents.StreamCreated) {
await this.sessionAttendeesRepository.updateById(
sessionAttendeeDetail.id,
updatedAttendee,
);
} else if (event === VonageEnums.SessionWebhookEvents.StreamDestroyed) {
await this.processStreamDestroyedEvent(
webhookPayload,
sessionAttendeeDetail,
updatedAttendee,
);
} else if (
event === VonageEnums.SessionWebhookEvents.ConnectionDestroyed
) {
updatedAttendee.isDeleted = true;
await this.sessionAttendeesRepository.updateById(
sessionAttendeeDetail.id,
updatedAttendee,
);
} else {
//DO NOTHING
}
}
await this.auditLogRepository.create(
Expand Down Expand Up @@ -325,4 +355,86 @@ export class VideoChatSessionController {
);
}
}

async processStreamDestroyedEvent(
webhookPayload: VonageSessionWebhookPayload,
sessionAttendeeDetail: SessionAttendees,
updatedAttendee: Partial<SessionAttendees>,
) {
if (
webhookPayload.reason === 'forceUnpublished' ||
webhookPayload.reason === 'mediaStopped'
) {
await this.sessionAttendeesRepository.updateById(
sessionAttendeeDetail.id,
updatedAttendee,
);
} else {
updatedAttendee.isDeleted = true;
await this.sessionAttendeesRepository.updateById(
sessionAttendeeDetail.id,
updatedAttendee,
);
}
}

@authenticate(STRATEGY.BEARER)
@authorize([PermissionKeys.GetAttendees])
@get('/session/{meetingLinkId}/attendees', {
parameters: [{name: 'active', schema: {type: 'string'}, in: 'query'}],
responses: {
[STATUS_CODE.OK]: {
content: {
[CONTENT_TYPE.TEXT]: {schema: {type: 'array'}},
},
},
},
})
async getAttendeesList(
@param.path.string('meetingLinkId') meetingLinkId: string,
@param.query.string('active') active: string,
): Promise<SessionAttendees[]> {
const auditLogPayload = {
action: 'session',
actionType: 'session-attendees-list',
before: {meetingLinkId},
actedAt: moment().format(),
after: {},
};
let errorMessage: string;

const videoSessionDetail = await this.videoChatSessionRepository.findOne({
where: {
meetingLink: meetingLinkId,
},
});

if (!videoSessionDetail) {
errorMessage = 'Meeting Not Found';
auditLogPayload.after = {errorMessage};
await this.auditLogRepository.create(auditLogPayload);
throw new HttpErrors.NotFound(errorMessage);
}

let whereFilter = {};
if (active === 'true') {
whereFilter = {
sessionId: videoSessionDetail?.sessionId,
isDeleted: false,
};
} else {
whereFilter = {
sessionId: videoSessionDetail?.sessionId,
};
}

const sessionAttendeeList = await this.sessionAttendeesRepository.find({
where: whereFilter,
});

auditLogPayload.after = {response: 'get attendees successful'};
await this.auditLogRepository.create(auditLogPayload);

return sessionAttendeeList;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const enum PermissionKeys {
DeleteArchive = 'DeleteMeetingArchive',
StopMeeting = 'StopMeeting',
SetUploadTarget = 'SetMeetingUploadTarget',
GetAttendees = 'GetMeetingAttendees',
}

0 comments on commit e2b7fad

Please sign in to comment.