Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/shared/services/member.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
112 changes: 98 additions & 14 deletions src/shared/services/member.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -213,9 +210,9 @@ export class MemberService {
* Looks up role members from Identity API by role name.
*
* Resolves `/roles?filter=roleName=<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.
Expand Down Expand Up @@ -250,9 +247,9 @@ export class MemberService {
}),
);

const roles = Array.isArray(rolesResponse.data)
? (rolesResponse.data as MemberRoleRecord[])
: [];
const roles = this.extractArrayPayload<MemberRoleRecord>(
rolesResponse.data,
);

const roleIds = roles
.filter(
Expand All @@ -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);
}),
);

Expand All @@ -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;
Expand All @@ -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,
});
Expand All @@ -337,4 +335,90 @@ export class MemberService {
async getRoleSubjectsByRoleName(roleName: string): Promise<MemberDetail[]> {
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<T>(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<RoleSubjectRecord>(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[]) : [];
}
}
Loading