From 0da594c59b5551e590758c45174674729f64b91b Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan Date: Fri, 13 Sep 2024 18:26:30 +0530 Subject: [PATCH 1/2] fix(contacts): handle optional scim fields --- .../src/Contacts/ContactsClient.test.ts | 38 +++++++++++++++++++ .../calling/src/Contacts/ContactsClient.ts | 26 ++++++------- .../calling/src/Contacts/contactFixtures.ts | 30 ++++++++++++++- packages/calling/src/Contacts/types.ts | 4 +- packages/calling/src/common/types.ts | 24 ++++++------ 5 files changed, 93 insertions(+), 29 deletions(-) diff --git a/packages/calling/src/Contacts/ContactsClient.test.ts b/packages/calling/src/Contacts/ContactsClient.test.ts index 304ab3c5bf4..01b6fc6b4a7 100644 --- a/packages/calling/src/Contacts/ContactsClient.test.ts +++ b/packages/calling/src/Contacts/ContactsClient.test.ts @@ -48,6 +48,8 @@ import { mockContactGroupListOne, mockContactGroupListTwo, mockAvatarURL, + mockSCIMMinListResponse, + mockContactMinimum, } from './contactFixtures'; describe('ContactClient Tests', () => { @@ -724,4 +726,40 @@ describe('ContactClient Tests', () => { expect(contactClient['contacts']).toEqual(mockContactListOne); }); + + it('test resolveContacts function for a minimal contact with few details', () => { + const contact = contactClient['resolveCloudContacts']( + {userId: mockContactMinimum}, + mockSCIMMinListResponse.body + ); + + expect(contact).toEqual([ + { + avatarURL: '', + avatarUrlDomain: undefined, + contactId: 'userId', + contactType: 'CLOUD', + department: undefined, + displayName: undefined, + emails: undefined, + encryptionKeyUrl: 'kms://cisco.com/keys/dcf18f9d-155e-44ff-ad61-c8a69b7103ab', + firstName: undefined, + groups: ['1561977e-3443-4ccf-a591-69686275d7d2'], + lastName: undefined, + manager: undefined, + ownerId: 'ownerId', + phoneNumbers: undefined, + sipAddresses: undefined, + }, + ]); + }); + + it('test resolveContacts function encountering an error', () => { + const contact = contactClient['resolveCloudContacts']( + {userId: mockContactMinimum}, + mockSCIMMinListResponse + ); + + expect(contact).toEqual(null); + }); }); diff --git a/packages/calling/src/Contacts/ContactsClient.ts b/packages/calling/src/Contacts/ContactsClient.ts index 28bf3c1a934..5bb05d67899 100644 --- a/packages/calling/src/Contacts/ContactsClient.ts +++ b/packages/calling/src/Contacts/ContactsClient.ts @@ -36,9 +36,6 @@ import { } from './types'; import {scimQuery, serviceErrorCodeHandler} from '../common/Utils'; -import Logger from '../Logger'; -import ExtendedError from '../Errors/catalog/ExtendedError'; -import {ERROR_TYPE} from '../Errors/types'; /** * `ContactsClient` module is designed to offer a set of APIs for retrieving and updating contacts and groups from the contacts-service. @@ -264,6 +261,10 @@ export class ContactsClient implements IContacts { contactsDataMap: ContactIdContactInfo, inputList: SCIMListResponse ): Contact[] | null { + const loggerContext = { + file: CONTACTS_FILE, + method: 'resolveCloudContacts', + }; const finalContactList: Contact[] = []; try { @@ -272,16 +273,15 @@ export class ContactsClient implements IContacts { const filteredContact = inputList.Resources.filter((item) => item.id === contactList[n])[0]; const {displayName, emails, phoneNumbers, photos} = filteredContact; - const {sipAddresses} = filteredContact[SCIM_WEBEXIDENTITY_USER]; - const firstName = filteredContact.name.givenName; - const lastName = filteredContact.name.familyName; - const manager = filteredContact[SCIM_ENTERPRISE_USER].manager.displayName; - const department = filteredContact[SCIM_ENTERPRISE_USER].department; - - let avatarURL = ''; - if (photos?.length) { - avatarURL = photos[0].value; + let sipAddresses; + if (filteredContact[SCIM_WEBEXIDENTITY_USER]) { + sipAddresses = filteredContact[SCIM_WEBEXIDENTITY_USER].sipAddresses; } + const firstName = filteredContact.name?.givenName; + const lastName = filteredContact.name?.familyName; + const manager = filteredContact[SCIM_ENTERPRISE_USER]?.manager?.displayName; + const department = filteredContact[SCIM_ENTERPRISE_USER]?.department; + const avatarURL = photos?.length ? photos[0].value : ''; const {contactType, avatarUrlDomain, encryptionKeyUrl, ownerId, groups} = contactsDataMap[contactList[n]]; @@ -307,7 +307,7 @@ export class ContactsClient implements IContacts { finalContactList.push(cloudContact); } } catch (error: any) { - Logger.error(new ExtendedError(error.message, {}, ERROR_TYPE.DEFAULT), {}); + log.warn('Error occurred while parsing resolved contacts', loggerContext); return null; } diff --git a/packages/calling/src/Contacts/contactFixtures.ts b/packages/calling/src/Contacts/contactFixtures.ts index 605925e7010..4295a607411 100644 --- a/packages/calling/src/Contacts/contactFixtures.ts +++ b/packages/calling/src/Contacts/contactFixtures.ts @@ -272,6 +272,21 @@ export const mockContactGroupListTwo = [ }, ]; +export const mockContactMinimum = { + contactId: 'userId', + contactType: 'CLOUD', + encryptionKeyUrl: 'kms://cisco.com/keys/dcf18f9d-155e-44ff-ad61-c8a69b7103ab', + groups: ['1561977e-3443-4ccf-a591-69686275d7d2'], + ownerId: 'ownerId', +}; + +export const scimUserMinimum = { + schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'], + id: 'userId', + userName: 'userName', + userType: 'user', +}; + const scimUser1 = { schemas: [ 'urn:ietf:params:scim:schemas:core:2.0:User', @@ -448,13 +463,24 @@ export const mockSCIMListResponse = { statusCode: 200, body: { schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'], - totalResults: 11, - itemsPerPage: 11, + totalResults: 2, + itemsPerPage: 2, startIndex: 1, Resources: [scimUser1, scimUser2NoPhoto], }, }; +export const mockSCIMMinListResponse = { + statusCode: 200, + body: { + schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'], + totalResults: 1, + itemsPerPage: 1, + startIndex: 1, + Resources: [scimUserMinimum], + }, +}; + export const mockKmsKey = { uri: 'kms://kms-cisco.wbx2.com/keys/16095024-612d-4424-ba51-57cad2402e14', }; diff --git a/packages/calling/src/Contacts/types.ts b/packages/calling/src/Contacts/types.ts index 7c4484117a2..443f0149034 100644 --- a/packages/calling/src/Contacts/types.ts +++ b/packages/calling/src/Contacts/types.ts @@ -29,7 +29,7 @@ export type Contact = { /** * Unique identifier of the contact. */ - contactId?: string; + contactId: string; /** * Indicates the type of the contact, can be `CLOUD` or `CUSTOM`. */ @@ -41,7 +41,7 @@ export type Contact = { /** * This represents the display name of the contact. */ - displayName: string; + displayName?: string; /** * This represents the array of different email addresses of the contact. */ diff --git a/packages/calling/src/common/types.ts b/packages/calling/src/common/types.ts index 0e708ccd3e8..02605e6a1f0 100644 --- a/packages/calling/src/common/types.ts +++ b/packages/calling/src/common/types.ts @@ -220,8 +220,8 @@ interface WebexIdentityMeta { organizationId: string; } interface WebexIdentityUser { - sipAddresses: URIAddress[]; - meta: WebexIdentityMeta; + sipAddresses?: URIAddress[]; + meta?: WebexIdentityMeta; } interface Manager { @@ -231,24 +231,24 @@ interface Manager { } interface EnterpriseUser { - department: string; - manager: Manager; + department?: string; + manager?: Manager; } interface Resource { schemas: string[]; id: string; userName: string; - active: boolean; - name: Name; - displayName: string; - emails: URIAddress[]; + active?: boolean; + name?: Name; + displayName?: string; + emails?: URIAddress[]; userType: string; - phoneNumbers: PhoneNumber[]; + phoneNumbers?: PhoneNumber[]; photos?: ContactDetail[]; - addresses: Address[]; - [SCIM_WEBEXIDENTITY_USER]: WebexIdentityUser; - [SCIM_ENTERPRISE_USER]: EnterpriseUser; + addresses?: Address[]; + [SCIM_WEBEXIDENTITY_USER]?: WebexIdentityUser; + [SCIM_ENTERPRISE_USER]?: EnterpriseUser; } export interface SCIMListResponse { From 9a6ce09d2ad1c0f083f8d2db911466db1e913682 Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan Date: Fri, 13 Sep 2024 18:50:07 +0530 Subject: [PATCH 2/2] fix(utils): fix optional fields in utils and added ut --- packages/calling/src/common/Utils.test.ts | 24 +++++++++++++++++++++ packages/calling/src/common/Utils.ts | 6 +++--- packages/calling/src/common/testUtil.ts | 26 +++++++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/calling/src/common/Utils.test.ts b/packages/calling/src/common/Utils.test.ts index 3014240caa8..f35d45ef89b 100644 --- a/packages/calling/src/common/Utils.test.ts +++ b/packages/calling/src/common/Utils.test.ts @@ -8,6 +8,7 @@ import { getSamplePeopleListResponse, getSampleRawAndParsedMediaStats, getMobiusDiscoveryResponse, + getSampleMinimumScimResponse, } from './testUtil'; import { CallDirection, @@ -1048,6 +1049,29 @@ describe('resolveContact tests', () => { }); }); + it('Resolve with minimal response from SCIM', () => { + const callingPartyInfo = {} as CallingPartyInfo; + const scimResponse = getSampleMinimumScimResponse(); + + // scimResponse.Resources[0].photos = []; + const webexSpy = jest.spyOn(webex, 'request').mockResolvedValue({ + statusCode: 200, + body: scimResponse, + }); + + callingPartyInfo.userExternalId = {$: 'userExternalId'}; + resolveContact(callingPartyInfo).then((displayInfo) => { + expect(displayInfo?.name).toBeUndefined(); + expect(displayInfo?.num).toBeUndefined(); + expect(displayInfo?.avatarSrc).toStrictEqual('unknown'); + expect(displayInfo?.id).toStrictEqual(getSampleMinimumScimResponse().Resources[0].id); + + const query = scimUrl + encodeURIComponent(`id eq "${callingPartyInfo.userExternalId?.$}"`); + + expect(webexSpy).toBeCalledOnceWith(expect.objectContaining({uri: query})); + }); + }); + it('Resolve by name', () => { const callingPartyInfo = {} as CallingPartyInfo; const webexSpy = jest diff --git a/packages/calling/src/common/Utils.ts b/packages/calling/src/common/Utils.ts index 68b7fb0211b..44e4577d902 100644 --- a/packages/calling/src/common/Utils.ts +++ b/packages/calling/src/common/Utils.ts @@ -1234,12 +1234,12 @@ export async function resolveCallerIdDisplay(filter: string) { /* Pick only the primary number OR 2nd preference Work */ const numberObj = - scimResource.phoneNumbers.find((num) => num.primary) || - scimResource.phoneNumbers.find((num) => num.type.toLowerCase() === 'work'); + scimResource.phoneNumbers?.find((num) => num.primary) || + scimResource.phoneNumbers?.find((num) => num.type.toLowerCase() === 'work'); if (numberObj) { displayResult.num = numberObj.value; - } else if (scimResource.phoneNumbers.length > 0) { + } else if (scimResource.phoneNumbers && scimResource.phoneNumbers.length > 0) { /* When no primary number exists OR PA-ID/From failed to populate, we take the first number */ log.info('Failure to resolve caller information. Setting number as caller ID', { file: UTILS_FILE, diff --git a/packages/calling/src/common/testUtil.ts b/packages/calling/src/common/testUtil.ts index 0a8f2195f72..dff60f0e25f 100644 --- a/packages/calling/src/common/testUtil.ts +++ b/packages/calling/src/common/testUtil.ts @@ -260,6 +260,32 @@ export const getSampleScimResponse = () => { }; }; +export const getSampleMinimumScimResponse = () => { + return { + totalResults: '1', + itemsPerPage: '1', + startIndex: '1', + schemas: ['urn:scim:schemas:core:1.0'], + Resources: [ + { + userName: 'atlas.test.wxcwebrtc+user8@gmail.com', + id: 'userExternalId', + meta: { + created: '2022-03-16T16:13:53.847Z', + lastModified: '2022-05-31T14:39:12.782Z', + lastLoginTime: '2022-05-31T14:39:12.780Z', + version: 'W/"66025591113"', + location: + 'https://identitybts.webex.com/identity/scim/1704d30d-a131-4bc7-9449-948487643793/v1/Users/652fe0c7-05ce-4acd-8bda-9a080830187f', + organizationID: '1704d30d-a131-4bc7-9449-948487643793', + creator: '97fe25e3-d3e8-400e-856b-5b0cd5b0c790', + modifier: '8c7abf2f-0c8e-49cf-b8e4-693d4ec7daee', + }, + }, + ], + }; +}; + /** * Returns a sample people list response object. */