diff --git a/src/shared/services/member.service.spec.ts b/src/shared/services/member.service.spec.ts index a032f7b..d09644c 100644 --- a/src/shared/services/member.service.spec.ts +++ b/src/shared/services/member.service.spec.ts @@ -88,6 +88,65 @@ describe('MemberService', () => { ); }); + it('returns subjects from legacy wrapped Identity role responses', async () => { + httpServiceMock.get.mockImplementation( + (url: string, options: { params?: { filter?: string } }) => { + if ( + url === 'https://identity.test/roles' && + options.params?.filter === 'roleName=Project Manager' + ) { + return of({ + data: { + result: { + content: [{ id: '201', roleName: 'Project Manager' }], + }, + }, + }); + } + + if (url === 'https://identity.test/roles/201') { + return of({ + data: { + result: { + content: { + subjects: [ + { + subjectId: 5001, + handle: 'pm-one', + email: 'PM.One@Topcoder.com', + }, + ], + }, + }, + }, + }); + } + + return of({ data: {} }); + }, + ); + + const result = await service.getRoleSubjects('Project Manager'); + + expect(result).toEqual([ + { + userId: 5001, + handle: 'pm-one', + email: 'pm.one@topcoder.com', + }, + ]); + expect(httpServiceMock.get).toHaveBeenCalledWith( + 'https://identity.test/roles/201', + expect.objectContaining({ + params: expect.objectContaining({ + fields: 'subjects', + selector: 'subjects', + perPage: 200, + }), + }), + ); + }); + it('returns empty list when role list lookup fails', async () => { httpServiceMock.get.mockImplementation((url: string) => { if (url === 'https://identity.test/roles') { diff --git a/src/shared/services/member.service.ts b/src/shared/services/member.service.ts index b8f68e0..8c82139 100644 --- a/src/shared/services/member.service.ts +++ b/src/shared/services/member.service.ts @@ -13,15 +13,12 @@ export interface MemberRoleRecord { type RoleSubjectRecord = { subjectID?: string | number; + subjectId?: string | number; userId?: string | number; handle?: string; email?: string; }; -type RoleDetailResponse = { - subjects?: RoleSubjectRecord[]; -}; - /** * Member and identity enrichment service. * @@ -213,9 +210,9 @@ export class MemberService { * Looks up role members from Identity API by role name. * * Resolves `/roles?filter=roleName=` and then - * `/roles/:id?selector=subjects&perPage=200`, returning the merged - * `subjects` list - * normalized to `MemberDetail` shape. + * `/roles/:id` with subject expansion, returning the merged `subjects` list + * normalized to `MemberDetail` shape. Supports both direct array responses + * and the legacy `{ result: { content } }` Identity response wrapper. * * Role-detail lookups are tolerant to partial failures; one failing role id * does not prevent subjects from other matching roles from being returned. @@ -250,9 +247,9 @@ export class MemberService { }), ); - const roles = Array.isArray(rolesResponse.data) - ? (rolesResponse.data as MemberRoleRecord[]) - : []; + const roles = this.extractArrayPayload( + rolesResponse.data, + ); const roleIds = roles .filter( @@ -276,14 +273,14 @@ export class MemberService { 'Content-Type': 'application/json', }, params: { + fields: 'subjects', selector: 'subjects', perPage: 200, }, }), ); - const payload = roleResponse.data as RoleDetailResponse; - return Array.isArray(payload?.subjects) ? payload.subjects : []; + return this.extractRoleSubjects(roleResponse.data); }), ); @@ -310,7 +307,7 @@ export class MemberService { .trim() .toLowerCase(); const subjectId = String( - subject.subjectID || subject.userId || '', + subject.subjectID || subject.subjectId || subject.userId || '', ).trim(); const key = email || subjectId; @@ -319,7 +316,8 @@ export class MemberService { } uniqueSubjects.set(key, { - userId: subject.subjectID || subject.userId || null, + userId: + subject.subjectID || subject.subjectId || subject.userId || null, handle: subject.handle || null, email, }); @@ -337,4 +335,90 @@ export class MemberService { async getRoleSubjectsByRoleName(roleName: string): Promise { return this.getRoleSubjects(roleName); } + + /** + * Extracts an array payload from Identity responses. + * + * @param payload Identity API response body in direct or legacy wrapped form. + * @returns Extracted array or an empty array when the shape is unsupported. + */ + private extractArrayPayload(payload: unknown): T[] { + if (Array.isArray(payload)) { + return payload as T[]; + } + + if (!payload || typeof payload !== 'object') { + return []; + } + + const response = payload as { + content?: unknown; + result?: { + content?: unknown; + }; + }; + + if (Array.isArray(response.result?.content)) { + return response.result.content as T[]; + } + + if (Array.isArray(response.content)) { + return response.content as T[]; + } + + return []; + } + + /** + * Extracts role subjects from Identity role-detail responses. + * + * @param payload Identity role-detail body in direct, legacy wrapped, or list form. + * @returns Role subject records or an empty array when no subjects are present. + */ + private extractRoleSubjects(payload: unknown): RoleSubjectRecord[] { + const directSubjects = this.extractSubjectsFromObject(payload); + if (directSubjects.length > 0) { + return directSubjects; + } + + const listPayload = this.extractArrayPayload(payload); + if (listPayload.length > 0) { + return listPayload; + } + + if (!payload || typeof payload !== 'object') { + return []; + } + + const response = payload as { + content?: unknown; + result?: { + content?: unknown; + }; + }; + + const resultSubjects = this.extractSubjectsFromObject( + response.result?.content, + ); + if (resultSubjects.length > 0) { + return resultSubjects; + } + + return this.extractSubjectsFromObject(response.content); + } + + /** + * Extracts a `subjects` array from an object-like response fragment. + * + * @param payload Response fragment that may contain a `subjects` property. + * @returns Subjects array or an empty array when absent. + */ + private extractSubjectsFromObject(payload: unknown): RoleSubjectRecord[] { + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { + return []; + } + + const subjects = (payload as { subjects?: unknown }).subjects; + return Array.isArray(subjects) ? (subjects as RoleSubjectRecord[]) : []; + } }