[Cases] Use the new internal users API in the UI (#150432)

## Summary

This PR updates the UI to use the new `users` API introduced in
https://github.com/elastic/kibana/pull/149663. I changed the backend
response to accommodate the needs of the UI.

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Christos Nasikas 2023-02-11 12:10:29 +02:00 committed by GitHub
parent 870bc3895a
commit 4f95b7c7ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1147 additions and 578 deletions

View file

@ -7,23 +7,37 @@
import * as rt from 'io-ts';
const UserWithoutProfileUidRt = rt.type({
email: rt.union([rt.undefined, rt.null, rt.string]),
full_name: rt.union([rt.undefined, rt.null, rt.string]),
username: rt.union([rt.undefined, rt.null, rt.string]),
});
export const UserRt = rt.intersection([
rt.type({
email: rt.union([rt.undefined, rt.null, rt.string]),
full_name: rt.union([rt.undefined, rt.null, rt.string]),
username: rt.union([rt.undefined, rt.null, rt.string]),
}),
UserWithoutProfileUidRt,
rt.partial({ profile_uid: rt.string }),
]);
export const UserWithProfileInfoRt = rt.intersection([
rt.type({
user: UserWithoutProfileUidRt,
}),
rt.partial({ uid: rt.string }),
rt.partial({
avatar: rt.partial({ initials: rt.string, color: rt.string, imageUrl: rt.string }),
}),
]);
export const UsersRt = rt.array(UserRt);
export type User = rt.TypeOf<typeof UserRt>;
export type UserWithProfileInfo = rt.TypeOf<typeof UserWithProfileInfoRt>;
export const GetCaseUsersResponseRt = rt.type({
assignees: rt.array(UserRt),
unassignedUsers: rt.array(UserRt),
participants: rt.array(UserRt),
assignees: rt.array(UserWithProfileInfoRt),
unassignedUsers: rt.array(UserWithProfileInfoRt),
participants: rt.array(UserWithProfileInfoRt),
reporter: UserWithProfileInfoRt,
});
export type GetCaseUsersResponse = rt.TypeOf<typeof GetCaseUsersResponseRt>;

View file

@ -9,10 +9,16 @@ type SnakeToCamelCaseString<S extends string> = S extends `${infer T}_${infer U}
? `${T}${Capitalize<SnakeToCamelCaseString<U>>}`
: S;
type SnakeToCamelCaseArray<T> = T extends Array<infer ArrayItem>
? Array<SnakeToCamelCase<ArrayItem>>
: T;
export type SnakeToCamelCase<T> = T extends Record<string, unknown>
? {
[K in keyof T as SnakeToCamelCaseString<K & string>]: SnakeToCamelCase<T[K]>;
}
: T extends unknown[]
? SnakeToCamelCaseArray<T>
: T;
export enum CASE_VIEW_PAGE_TABS {

View file

@ -30,6 +30,7 @@ import type {
CommentResponseExternalReferenceType,
CommentResponseTypePersistableState,
GetCaseConnectorsResponse,
GetCaseUsersResponse,
} from '../api';
import type { PUSH_CASES_CAPABILITY } from '../constants';
import type { SnakeToCamelCase } from '../types';
@ -87,6 +88,7 @@ export type CasesStatus = SnakeToCamelCase<CasesStatusResponse>;
export type CasesMetrics = SnakeToCamelCase<CasesMetricsResponse>;
export type CaseUpdateRequest = SnakeToCamelCase<CasePatchRequest>;
export type CaseConnectors = SnakeToCamelCase<GetCaseConnectorsResponse>;
export type CaseUsers = GetCaseUsersResponse;
export interface ResolvedCase {
case: Case;
@ -147,7 +149,7 @@ export enum SortFieldCase {
title = 'title',
}
export type ElasticUser = SnakeToCamelCase<User>;
export type CaseUser = SnakeToCamelCase<User>;
export interface FetchCasesProps extends ApiProps {
queryParams?: QueryParams;

View file

@ -15,7 +15,7 @@ import { createAppMockRenderer } from '../../common/mock';
import '../../common/mock/match_media';
import { useCaseViewNavigation, useUrlParams } from '../../common/navigation/hooks';
import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors';
import { basicCaseClosed, connectorsMock } from '../../containers/mock';
import { basicCaseClosed, connectorsMock, getCaseUsersMockResponse } from '../../containers/mock';
import type { UseGetCase } from '../../containers/use_get_case';
import { useGetCase } from '../../containers/use_get_case';
import { useGetCaseMetrics } from '../../containers/use_get_case_metrics';
@ -24,7 +24,7 @@ import { useGetTags } from '../../containers/use_get_tags';
import { usePostPushToService } from '../../containers/use_post_push_to_service';
import { useGetCaseConnectors } from '../../containers/use_get_case_connectors';
import { useUpdateCase } from '../../containers/use_update_case';
import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles';
import { useGetCaseUsers } from '../../containers/use_get_case_users';
import { CaseViewPage } from './case_view_page';
import {
caseData,
@ -49,6 +49,7 @@ jest.mock('../../containers/use_get_case');
jest.mock('../../containers/configure/use_get_supported_action_connectors');
jest.mock('../../containers/use_post_push_to_service');
jest.mock('../../containers/use_get_case_connectors');
jest.mock('../../containers/use_get_case_users');
jest.mock('../../containers/user_profiles/use_bulk_get_user_profiles');
jest.mock('../user_actions/timestamp', () => ({
UserActionTimestamp: () => <></>,
@ -67,7 +68,7 @@ const usePostPushToServiceMock = usePostPushToService as jest.Mock;
const useGetCaseConnectorsMock = useGetCaseConnectors as jest.Mock;
const useGetCaseMetricsMock = useGetCaseMetrics as jest.Mock;
const useGetTagsMock = useGetTags as jest.Mock;
const useBulkGetUserProfilesMock = useBulkGetUserProfiles as jest.Mock;
const useGetCaseUsersMock = useGetCaseUsers as jest.Mock;
const mockGetCase = (props: Partial<UseGetCase> = {}) => {
const data = {
@ -99,6 +100,7 @@ describe('CaseViewPage', () => {
const data = caseProps.caseData;
let appMockRenderer: AppMockRenderer;
const caseConnectors = getCaseConnectorsMockResponse();
const caseUsers = getCaseUsersMockResponse();
beforeEach(() => {
mockGetCase();
@ -113,10 +115,11 @@ describe('CaseViewPage', () => {
});
useGetConnectorsMock.mockReturnValue({ data: connectorsMock, isLoading: false });
useGetTagsMock.mockReturnValue({ data: [], isLoading: false });
useBulkGetUserProfilesMock.mockReturnValue({ data: new Map(), isLoading: false });
const license = licensingMock.createLicense({
license: { type: 'platinum' },
});
useGetCaseUsersMock.mockReturnValue({ isLoading: false, data: caseUsers });
appMockRenderer = createAppMockRenderer({ license });
});

View file

@ -8,9 +8,9 @@
import {
alertComment,
basicCase,
caseUserActions,
connectorsMock,
getAlertUserAction,
getCaseUsersMockResponse,
getUserAction,
} from '../../../containers/mock';
import React from 'react';
import type { AppMockRenderer } from '../../../common/mock';
@ -23,12 +23,14 @@ import { useFindCaseUserActions } from '../../../containers/use_find_case_user_a
import { usePostPushToService } from '../../../containers/use_post_push_to_service';
import { useGetSupportedActionConnectors } from '../../../containers/configure/use_get_supported_action_connectors';
import { useGetTags } from '../../../containers/use_get_tags';
import { useBulkGetUserProfiles } from '../../../containers/user_profiles/use_bulk_get_user_profiles';
import { useGetCaseConnectors } from '../../../containers/use_get_case_connectors';
import { useGetCaseUsers } from '../../../containers/use_get_case_users';
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
import { waitForComponentToUpdate } from '../../../common/test_utils';
import { waitFor } from '@testing-library/dom';
import { waitFor, within } from '@testing-library/dom';
import { getCaseConnectorsMockResponse } from '../../../common/mock/connectors';
import { defaultUseFindCaseUserActions } from '../mocks';
import { ActionTypes } from '../../../../common/api';
jest.mock('../../../containers/use_find_case_user_actions');
jest.mock('../../../containers/configure/use_get_supported_action_connectors');
@ -41,6 +43,7 @@ jest.mock('../../../containers/use_get_action_license');
jest.mock('../../../containers/use_get_tags');
jest.mock('../../../containers/user_profiles/use_bulk_get_user_profiles');
jest.mock('../../../containers/use_get_case_connectors');
jest.mock('../../../containers/use_get_case_users');
(useGetTags as jest.Mock).mockReturnValue({ data: ['coke', 'pepsi'], refetch: jest.fn() });
@ -74,39 +77,28 @@ const caseViewProps: CaseViewProps = {
},
],
};
const fetchCaseUserActions = jest.fn();
const pushCaseToExternalService = jest.fn();
const defaultUseFindCaseUserActions = {
data: {
caseUserActions: [...caseUserActions, getAlertUserAction()],
participants: [caseData.createdBy],
},
refetch: fetchCaseUserActions,
isLoading: false,
isError: false,
};
export const caseProps = {
...caseViewProps,
caseData,
fetchCaseMetrics: jest.fn(),
};
const caseUsers = getCaseUsersMockResponse();
const useFindCaseUserActionsMock = useFindCaseUserActions as jest.Mock;
const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock;
const usePostPushToServiceMock = usePostPushToService as jest.Mock;
const useBulkGetUserProfilesMock = useBulkGetUserProfiles as jest.Mock;
const useGetCaseConnectorsMock = useGetCaseConnectors as jest.Mock;
const useGetCaseUsersMock = useGetCaseUsers as jest.Mock;
describe('Case View Page activity tab', () => {
const caseConnectors = getCaseConnectorsMockResponse();
beforeAll(() => {
useFindCaseUserActionsMock.mockReturnValue(defaultUseFindCaseUserActions);
useGetConnectorsMock.mockReturnValue({ data: connectorsMock, isLoading: false });
usePostPushToServiceMock.mockReturnValue({ isLoading: false, pushCaseToExternalService });
useBulkGetUserProfilesMock.mockReturnValue({ isLoading: false, data: new Map() });
useGetCaseConnectorsMock.mockReturnValue({
isLoading: false,
data: caseConnectors,
@ -124,6 +116,8 @@ describe('Case View Page activity tab', () => {
beforeEach(() => {
appMockRender = createAppMockRenderer();
useFindCaseUserActionsMock.mockReturnValue(defaultUseFindCaseUserActions);
useGetCaseUsersMock.mockReturnValue({ isLoading: false, data: caseUsers });
});
it('should render the activity content and main components', async () => {
@ -218,4 +212,244 @@ describe('Case View Page activity tab', () => {
expect(result.getByTestId('case-view-edit-connector')).toBeInTheDocument();
});
});
describe('Case users', () => {
describe('Participants', () => {
it('should render the participants correctly', async () => {
appMockRender = createAppMockRenderer();
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
const participantsSection = within(result.getByTestId('case-view-user-list-participants'));
await waitFor(() => {
expect(participantsSection.getByText('Participant 1')).toBeInTheDocument();
expect(participantsSection.getByText('participant_2@elastic.co')).toBeInTheDocument();
expect(participantsSection.getByText('participant_3')).toBeInTheDocument();
expect(participantsSection.getByText('P4')).toBeInTheDocument();
expect(participantsSection.getByText('Participant 5')).toBeInTheDocument();
});
});
it('should render Unknown users correctly', async () => {
appMockRender = createAppMockRenderer();
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
const participantsSection = within(result.getByTestId('case-view-user-list-participants'));
await waitFor(() => {
expect(participantsSection.getByText('Unknown')).toBeInTheDocument();
});
});
it('should render assignees in the participants section', async () => {
appMockRender = createAppMockRenderer();
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
const participantsSection = within(result.getByTestId('case-view-user-list-participants'));
await waitFor(() => {
expect(participantsSection.getByText('Unknown')).toBeInTheDocument();
expect(participantsSection.getByText('Fuzzy Marten')).toBeInTheDocument();
expect(participantsSection.getByText('elastic')).toBeInTheDocument();
expect(participantsSection.getByText('Misty Mackerel')).toBeInTheDocument();
});
});
});
describe('Reporter', () => {
it('should render the reporter correctly', async () => {
appMockRender = createAppMockRenderer();
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
const reporterSection = within(result.getByTestId('case-view-user-list-reporter'));
await waitFor(() => {
expect(reporterSection.getByText('Reporter 1')).toBeInTheDocument();
expect(reporterSection.getByText('R1')).toBeInTheDocument();
});
});
it('should render a reporter without uid correctly', async () => {
useGetCaseUsersMock.mockReturnValue({
isLoading: false,
data: {
...caseUsers,
reporter: {
user: {
email: 'reporter_no_uid@elastic.co',
full_name: 'Reporter No UID',
username: 'reporter_no_uid',
},
},
},
});
appMockRender = createAppMockRenderer();
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
const reporterSection = within(result.getByTestId('case-view-user-list-reporter'));
await waitFor(() => {
expect(reporterSection.getByText('Reporter No UID')).toBeInTheDocument();
});
});
it('fallbacks to the caseData reporter correctly', async () => {
useGetCaseUsersMock.mockReturnValue({
isLoading: false,
data: null,
});
appMockRender = createAppMockRenderer();
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
const reporterSection = within(result.getByTestId('case-view-user-list-reporter'));
await waitFor(() => {
expect(reporterSection.getByText('Leslie Knope')).toBeInTheDocument();
});
});
});
describe('Assignees', () => {
it('should render assignees in the participants section', async () => {
appMockRender = createAppMockRenderer({ license: platinumLicense });
const result = appMockRender.render(
<CaseViewActivity
{...caseProps}
caseData={{
...caseProps.caseData,
assignees: caseUsers.assignees.map((assignee) => ({
uid: assignee.uid ?? 'not-valid',
})),
}}
/>
);
const assigneesSection = within(await result.findByTestId('case-view-assignees'));
await waitFor(() => {
expect(assigneesSection.getByText('Unknown')).toBeInTheDocument();
expect(assigneesSection.getByText('Fuzzy Marten')).toBeInTheDocument();
expect(assigneesSection.getByText('elastic')).toBeInTheDocument();
expect(assigneesSection.getByText('Misty Mackerel')).toBeInTheDocument();
});
});
});
describe('User actions', () => {
it('renders the descriptions user correctly', async () => {
appMockRender = createAppMockRenderer();
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
const description = within(result.getByTestId('description-action'));
await waitFor(() => {
expect(description.getByText('Leslie Knope')).toBeInTheDocument();
});
});
it('renders the unassigned users correctly', async () => {
useFindCaseUserActionsMock.mockReturnValue({
...defaultUseFindCaseUserActions,
data: {
userActions: [getUserAction(ActionTypes.assignees, 'delete')],
},
});
appMockRender = createAppMockRenderer();
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
const userActions = within(result.getByTestId('user-actions'));
await waitFor(() => {
expect(userActions.getByText('cases_no_connectors')).toBeInTheDocument();
expect(userActions.getByText('Valid Chimpanzee')).toBeInTheDocument();
});
});
it('renders the assigned users correctly', async () => {
useFindCaseUserActionsMock.mockReturnValue({
...defaultUseFindCaseUserActions,
data: {
userActions: [
getUserAction(ActionTypes.assignees, 'add', {
payload: {
assignees: [
{ uid: 'not-valid' },
{ uid: 'u_3OgKOf-ogtr8kJ5B0fnRcqzXs2aQQkZLtzKEEFnKaYg_0' },
],
},
}),
],
},
});
appMockRender = createAppMockRenderer();
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
const userActions = within(result.getByTestId('user-actions'));
await waitFor(() => {
expect(userActions.getByText('Fuzzy Marten')).toBeInTheDocument();
expect(userActions.getByText('Unknown')).toBeInTheDocument();
});
});
it('renders the user action users correctly', async () => {
useFindCaseUserActionsMock.mockReturnValue({
...defaultUseFindCaseUserActions,
data: {
userActions: [
getUserAction('description', 'create'),
getUserAction('description', 'update', {
createdBy: {
...caseUsers.participants[0].user,
fullName: caseUsers.participants[0].user.full_name,
profileUid: caseUsers.participants[0].uid,
},
}),
getUserAction('comment', 'update', {
createdBy: {
...caseUsers.participants[1].user,
fullName: caseUsers.participants[1].user.full_name,
profileUid: caseUsers.participants[1].uid,
},
}),
getUserAction('description', 'update', {
createdBy: {
...caseUsers.participants[2].user,
fullName: caseUsers.participants[2].user.full_name,
profileUid: caseUsers.participants[2].uid,
},
}),
getUserAction('title', 'update', {
createdBy: {
...caseUsers.participants[3].user,
fullName: caseUsers.participants[3].user.full_name,
profileUid: caseUsers.participants[3].uid,
},
}),
getUserAction('tags', 'add', {
createdBy: {
...caseUsers.participants[4].user,
fullName: caseUsers.participants[4].user.full_name,
profileUid: caseUsers.participants[4].uid,
},
}),
],
},
});
appMockRender = createAppMockRenderer();
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
const userActions = within(result.getByTestId('user-actions'));
await waitFor(() => {
expect(userActions.getByText('Participant 1')).toBeInTheDocument();
expect(userActions.getByText('participant_2@elastic.co')).toBeInTheDocument();
expect(userActions.getByText('participant_3')).toBeInTheDocument();
expect(userActions.getByText('P4')).toBeInTheDocument();
expect(userActions.getByText('Participant 5')).toBeInTheDocument();
});
});
});
});
});

View file

@ -9,15 +9,15 @@
import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import { isEqual, uniq } from 'lodash';
import { isEqual } from 'lodash';
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import { useGetCaseUsers } from '../../../containers/use_get_case_users';
import { useGetCaseConnectors } from '../../../containers/use_get_case_connectors';
import { useCasesFeatures } from '../../../common/use_cases_features';
import { useGetCurrentUserProfile } from '../../../containers/user_profiles/use_get_current_user_profile';
import { useBulkGetUserProfiles } from '../../../containers/user_profiles/use_bulk_get_user_profiles';
import { useGetSupportedActionConnectors } from '../../../containers/configure/use_get_supported_action_connectors';
import type { CaseSeverity } from '../../../../common/api';
import { useCaseViewNavigation } from '../../../common/navigation';
import type { UseFetchAlertData } from '../../../../common/ui/types';
import type { CaseUsers, UseFetchAlertData } from '../../../../common/ui/types';
import type { Case, CaseStatuses } from '../../../../common';
import { EditConnector } from '../../edit_connector';
import type { CasesNavigation } from '../../links';
@ -32,6 +32,41 @@ import { SeveritySidebarSelector } from '../../severity/sidebar_selector';
import { useFindCaseUserActions } from '../../../containers/use_find_case_user_actions';
import { AssignUsers } from './assign_users';
import type { Assignee } from '../../user_profiles/types';
import { convertToCaseUserWithProfileInfo } from '../../user_profiles/user_converter';
const buildUserProfilesMap = (users?: CaseUsers): Map<string, UserProfileWithAvatar> => {
const userProfiles = new Map();
if (!users) {
return userProfiles;
}
for (const user of [
...users.assignees,
...users.participants,
users.reporter,
...users.unassignedUsers,
]) {
/**
* If the user has a valid profile UID and a valid username
* then the backend successfully fetched the user profile
* information from the security plugin. Checking only for the
* profile UID is not enough as a user can use our API to add
* an assignee with a non existing UID.
*/
if (user.uid != null && user.user.username != null) {
userProfiles.set(user.uid, {
uid: user.uid,
user: user.user,
data: {
avatar: user.avatar,
},
});
}
}
return userProfiles;
};
export const CaseViewActivity = ({
ruleDetailsNavigation,
@ -47,7 +82,6 @@ export const CaseViewActivity = ({
useFetchAlertData: UseFetchAlertData;
}) => {
const { permissions } = useCasesContext();
const { getCaseViewUrl } = useCaseViewNavigation();
const { caseAssignmentAuthorized, pushToServiceAuthorized } = useCasesFeatures();
const { data: caseConnectors, isLoading: isLoadingCaseConnectors } = useGetCaseConnectors(
@ -58,18 +92,15 @@ export const CaseViewActivity = ({
caseData.id
);
const { data: caseUsers, isLoading: isLoadingCaseUsers } = useGetCaseUsers(caseData.id);
const userProfiles = buildUserProfilesMap(caseUsers);
const assignees = useMemo(
() => caseData.assignees.map((assignee) => assignee.uid),
[caseData.assignees]
);
const userActionProfileUids = Array.from(userActionsData?.profileUids?.values() ?? []);
const uidsToRetrieve = uniq([...userActionProfileUids, ...assignees]);
const { data: userProfiles, isFetching: isLoadingUserProfiles } = useBulkGetUserProfiles({
uids: uidsToRetrieve,
});
const { data: currentUserProfile, isFetching: isLoadingCurrentUserProfile } =
useGetCurrentUserProfile();
@ -87,9 +118,7 @@ export const CaseViewActivity = ({
});
const isLoadingAssigneeData =
(isLoading && loadingKey === 'assignees') ||
isLoadingUserProfiles ||
isLoadingCurrentUserProfile;
(isLoading && loadingKey === 'assignees') || isLoadingCaseUsers || isLoadingCurrentUserProfile;
const changeStatus = useCallback(
(status: CaseStatuses) =>
@ -100,14 +129,6 @@ export const CaseViewActivity = ({
[onUpdateField]
);
const emailContent = useMemo(
() => ({
subject: i18n.EMAIL_SUBJECT(caseData.title),
body: i18n.EMAIL_BODY(getCaseViewUrl({ detailName: caseData.id })),
}),
[caseData.title, getCaseViewUrl, caseData.id]
);
const onSubmitTags = useCallback(
(newTags) => onUpdateField({ key: 'tags', value: newTags }),
[onUpdateField]
@ -148,11 +169,16 @@ export const CaseViewActivity = ({
!isLoadingCaseConnectors &&
userActionsData &&
caseConnectors &&
userProfiles;
caseUsers;
const showConnectorSidebar =
pushToServiceAuthorized && userActionsData && caseConnectors && supportedActionConnectors;
const reporterAsArray =
caseUsers?.reporter != null
? [caseUsers.reporter]
: [convertToCaseUserWithProfileInfo(caseData.createdBy)];
return (
<>
<EuiFlexItem grow={6}>
@ -168,7 +194,7 @@ export const CaseViewActivity = ({
getRuleDetailsHref={ruleDetailsNavigation?.href}
onRuleDetailsClick={ruleDetailsNavigation?.onClick}
caseConnectors={caseConnectors}
caseUserActions={userActionsData.caseUserActions}
caseUserActions={userActionsData.userActions}
data={caseData}
actionsNavigation={actionsNavigation}
isLoadingDescription={isLoading && loadingKey === 'description'}
@ -211,18 +237,18 @@ export const CaseViewActivity = ({
/>
<UserList
dataTestSubj="case-view-user-list-reporter"
email={emailContent}
theCase={caseData}
headline={i18n.REPORTER}
users={[caseData.createdBy]}
users={reporterAsArray}
userProfiles={userProfiles}
/>
{userActionsData?.participants ? (
{caseUsers != null ? (
<UserList
dataTestSubj="case-view-user-list-participants"
email={emailContent}
theCase={caseData}
headline={i18n.PARTICIPANTS}
loading={isLoadingUserActions}
users={userActionsData.participants}
users={[...caseUsers.participants, ...caseUsers.assignees]}
userProfiles={userProfiles}
/>
) : null}

View file

@ -6,36 +6,82 @@
*/
import React from 'react';
import { shallow } from 'enzyme';
import { UserList } from './user_list';
import * as i18n from '../translations';
import { basicCase } from '../../../containers/mock';
import { useCaseViewNavigation } from '../../../common/navigation/hooks';
import type { AppMockRenderer } from '../../../common/mock';
import { createAppMockRenderer } from '../../../common/mock';
import userEvent from '@testing-library/user-event';
jest.mock('../../../common/navigation/hooks');
const useCaseViewNavigationMock = useCaseViewNavigation as jest.Mock;
describe('UserList ', () => {
const title = 'Case Title';
const caseLink = 'http://reddit.com';
const user = { username: 'username', fullName: 'Full Name', email: 'testemail@elastic.co' };
const title = basicCase.title;
const caseLink = 'https://example.com/cases/test';
const user = {
email: 'case_all@elastic.co',
fullName: 'Cases',
username: 'cases_all',
};
const open = jest.fn();
const getCaseViewUrl = jest.fn().mockReturnValue(caseLink);
let appMockRender: AppMockRenderer;
beforeEach(() => {
jest.clearAllMocks();
appMockRender = createAppMockRenderer();
useCaseViewNavigationMock.mockReturnValue({ getCaseViewUrl });
window.open = open;
});
it('triggers mailto when email icon clicked', () => {
const wrapper = shallow(
const result = appMockRender.render(
<UserList
email={{
subject: i18n.EMAIL_SUBJECT(title),
body: i18n.EMAIL_BODY(caseLink),
}}
theCase={basicCase}
headline={i18n.REPORTER}
users={[user]}
users={[
{
user: { ...user, full_name: user.fullName },
},
]}
/>
);
wrapper.find('[data-test-subj="user-list-email-button"]').simulate('click');
userEvent.click(result.getByTestId('user-list-email-button'));
expect(open).toBeCalledWith(
`mailto:${user.email}?subject=${i18n.EMAIL_SUBJECT(title)}&body=${i18n.EMAIL_BODY(caseLink)}`,
'_blank'
);
});
it('sort the users correctly', () => {
const result = appMockRender.render(
<UserList
theCase={basicCase}
headline={i18n.REPORTER}
users={[
{
user: { ...user, full_name: 'Cases' },
},
{
user: { ...user, username: 'elastic', email: 'elastic@elastic.co', full_name: null },
},
{
user: { ...user, username: 'test', full_name: null, email: null },
},
]}
/>
);
const userProfiles = result.getAllByTestId('user-profile-username');
expect(userProfiles[0].textContent).toBe('Cases');
expect(userProfiles[1].textContent).toBe('elastic@elastic.co');
expect(userProfiles[2].textContent).toBe('test');
});
});

View file

@ -7,6 +7,7 @@
import React, { useCallback } from 'react';
import { isEmpty } from 'lodash/fp';
import { sortBy } from 'lodash';
import {
EuiButtonIcon,
@ -20,20 +21,19 @@ import {
import styled, { css } from 'styled-components';
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import type { ElasticUser } from '../../../containers/types';
import { useCaseViewNavigation } from '../../../common/navigation';
import type { Case } from '../../../containers/types';
import * as i18n from '../translations';
import type { UserInfoWithAvatar } from '../../user_profiles/types';
import type { CaseUserWithProfileInfo, UserInfoWithAvatar } from '../../user_profiles/types';
import { HoverableUserWithAvatar } from '../../user_profiles/hoverable_user_with_avatar';
import { convertToUserInfo } from '../../user_profiles/user_converter';
import { getSortField } from '../../user_profiles/sort';
interface UserListProps {
email: {
subject: string;
body: string;
};
theCase: Case;
headline: string;
loading?: boolean;
users: ElasticUser[];
users: CaseUserWithProfileInfo[];
userProfiles?: Map<string, UserProfileWithAvatar>;
dataTestSubj?: string;
}
@ -67,8 +67,18 @@ const renderUsers = (
</MyFlexGroup>
));
const getEmailContent = ({ caseTitle, caseUrl }: { caseTitle: string; caseUrl: string }) => ({
subject: i18n.EMAIL_SUBJECT(caseTitle),
body: i18n.EMAIL_BODY(caseUrl),
});
export const UserList: React.FC<UserListProps> = React.memo(
({ email, headline, loading, users, userProfiles, dataTestSubj }) => {
({ theCase, userProfiles, headline, loading, users, dataTestSubj }) => {
const { getCaseViewUrl } = useCaseViewNavigation();
const caseUrl = getCaseViewUrl({ detailName: theCase.id });
const email = getEmailContent({ caseTitle: theCase.title, caseUrl });
const handleSendEmail = useCallback(
(emailAddress: string | undefined | null) => {
if (emailAddress && emailAddress != null) {
@ -82,8 +92,9 @@ export const UserList: React.FC<UserListProps> = React.memo(
);
const validUsers = getValidUsers(users, userProfiles ?? new Map());
const orderedUsers = sortBy(validUsers, getSortField);
if (validUsers.length === 0) {
if (orderedUsers.length === 0) {
return null;
}
@ -99,7 +110,7 @@ export const UserList: React.FC<UserListProps> = React.memo(
</EuiFlexItem>
</EuiFlexGroup>
)}
{renderUsers(validUsers, handleSendEmail)}
{renderUsers(orderedUsers, handleSendEmail)}
</EuiText>
</EuiFlexItem>
);
@ -109,11 +120,19 @@ export const UserList: React.FC<UserListProps> = React.memo(
UserList.displayName = 'UserList';
const getValidUsers = (
users: ElasticUser[],
users: CaseUserWithProfileInfo[],
userProfiles: Map<string, UserProfileWithAvatar>
): UserInfoWithAvatar[] => {
const validUsers = users.reduce<Map<string, UserInfoWithAvatar>>((acc, user) => {
const convertedUser = convertToUserInfo(user, userProfiles);
const userCamelCase = {
email: user.user.email,
fullName: user.user.full_name,
username: user.user.username,
profileUid: user.uid,
};
const convertedUser = convertToUserInfo(userCamelCase, userProfiles);
if (convertedUser != null) {
acc.set(convertedUser.key, convertedUser.userInfo);
}

View file

@ -100,10 +100,7 @@ export const defaultUpdateCaseState = {
};
export const defaultUseFindCaseUserActions = {
data: {
caseUserActions: [...caseUserActions, getAlertUserAction()],
participants: [caseData.createdBy],
},
data: { userActions: [...caseUserActions, getAlertUserAction()] },
refetch: jest.fn(),
isLoading: false,
isFetching: false,

View file

@ -7,13 +7,13 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import type { ElasticUser } from '../../containers/types';
import type { CaseUser } from '../../containers/types';
import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock';
import { HoverableAvatarResolver } from './hoverable_avatar_resolver';
describe('HoverableAvatarResolver', () => {
it('renders the avatar using the elastic user information when the profile is not present', async () => {
const user: ElasticUser = {
const user: CaseUser = {
username: 'user',
email: 'some.user@google.com',
fullName: 'Some Super User',
@ -25,7 +25,7 @@ describe('HoverableAvatarResolver', () => {
});
it('renders the avatar when the profile uid is not found', async () => {
const user: ElasticUser = {
const user: CaseUser = {
username: 'some_user',
profileUid: '123',
fullName: null,
@ -38,7 +38,7 @@ describe('HoverableAvatarResolver', () => {
});
it('renders the avatar using the profile', async () => {
const user: ElasticUser = {
const user: CaseUser = {
username: userProfiles[0].user.username,
profileUid: userProfiles[0].uid,
fullName: null,
@ -51,7 +51,7 @@ describe('HoverableAvatarResolver', () => {
});
it('renders the tooltip when hovering', async () => {
const user: ElasticUser = {
const user: CaseUser = {
username: userProfiles[0].user.username,
profileUid: userProfiles[0].uid,
fullName: null,

View file

@ -7,12 +7,12 @@
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import React from 'react';
import type { ElasticUser } from '../../containers/types';
import type { CaseUser } from '../../containers/types';
import { convertToUserInfo } from './user_converter';
import { HoverableAvatar } from './hoverable_avatar';
const HoverableAvatarResolverComponent: React.FC<{
user: ElasticUser;
user: CaseUser;
userProfiles?: Map<string, UserProfileWithAvatar>;
}> = ({ user, userProfiles }) => {
const { userInfo } = convertToUserInfo(user, userProfiles) ?? { userInfo: undefined };

View file

@ -7,13 +7,13 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import type { ElasticUser } from '../../containers/types';
import type { CaseUser } from '../../containers/types';
import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock';
import { HoverableUserWithAvatarResolver } from './hoverable_user_with_avatar_resolver';
describe('HoverableUserWithAvatarResolver', () => {
it('renders the avatar and display name using the full name', async () => {
const user: ElasticUser = {
const user: CaseUser = {
username: 'user',
email: 'some.user@google.com',
fullName: 'Some Super User',
@ -26,7 +26,7 @@ describe('HoverableUserWithAvatarResolver', () => {
});
it('renders the avatar and display name using the username when the profile uid is not found', async () => {
const user: ElasticUser = {
const user: CaseUser = {
username: 'some_user',
profileUid: '123',
fullName: null,
@ -40,7 +40,7 @@ describe('HoverableUserWithAvatarResolver', () => {
});
it('renders the avatar and display name using the profile', async () => {
const user: ElasticUser = {
const user: CaseUser = {
username: userProfiles[0].user.username,
profileUid: userProfiles[0].uid,
fullName: null,
@ -54,7 +54,7 @@ describe('HoverableUserWithAvatarResolver', () => {
});
it('renders display name bolded by default', async () => {
const user: ElasticUser = {
const user: CaseUser = {
username: userProfiles[0].user.username,
profileUid: userProfiles[0].uid,
fullName: null,

View file

@ -7,14 +7,14 @@
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import React from 'react';
import type { ElasticUser } from '../../containers/types';
import type { CaseUser } from '../../containers/types';
import type { HoverableUserWithAvatarProps } from './hoverable_user_with_avatar';
import { HoverableUserWithAvatar } from './hoverable_user_with_avatar';
import { convertToUserInfo } from './user_converter';
const HoverableUserWithAvatarResolverComponent: React.FC<
{
user: ElasticUser;
user: CaseUser;
userProfiles?: Map<string, UserProfileWithAvatar>;
} & Pick<HoverableUserWithAvatarProps, 'boldName'>
> = ({ user, userProfiles, boldName = true }) => {

View file

@ -7,13 +7,13 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import type { ElasticUser } from '../../containers/types';
import type { CaseUser } from '../../containers/types';
import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock';
import { HoverableUsernameResolver } from './hoverable_username_resolver';
describe('HoverableUsernameResolver', () => {
it('renders the full name using the elastic user information when the profile is not present', async () => {
const user: ElasticUser = {
const user: CaseUser = {
username: 'user',
email: 'some.user@google.com',
fullName: 'Some Super User',
@ -25,7 +25,7 @@ describe('HoverableUsernameResolver', () => {
});
it('renders the username when the profile uid is not found', async () => {
const user: ElasticUser = {
const user: CaseUser = {
username: 'some_user',
profileUid: '123',
fullName: null,
@ -38,7 +38,7 @@ describe('HoverableUsernameResolver', () => {
});
it('renders the full name using the profile', async () => {
const user: ElasticUser = {
const user: CaseUser = {
username: userProfiles[0].user.username,
profileUid: userProfiles[0].uid,
fullName: null,
@ -51,7 +51,7 @@ describe('HoverableUsernameResolver', () => {
});
it('renders the full name bolded by default', async () => {
const user: ElasticUser = {
const user: CaseUser = {
username: userProfiles[0].user.username,
profileUid: userProfiles[0].uid,
fullName: null,

View file

@ -7,7 +7,7 @@
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import React from 'react';
import type { ElasticUser } from '../../containers/types';
import type { CaseUser } from '../../containers/types';
import type { HoverableUserWithAvatarProps } from './hoverable_user_with_avatar';
import { Username } from './username';
import { convertToUserInfo } from './user_converter';
@ -15,7 +15,7 @@ import { UserToolTip } from './user_tooltip';
const HoverableUsernameResolverComponent: React.FC<
{
user: ElasticUser;
user: CaseUser;
userProfiles?: Map<string, UserProfileWithAvatar>;
} & Pick<HoverableUserWithAvatarProps, 'boldName'>
> = ({ user, userProfiles, boldName = true }) => {

View file

@ -9,12 +9,14 @@ import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import { sortBy } from 'lodash';
import { NO_ASSIGNEES_VALUE } from '../all_cases/assignees_filter';
import type { CurrentUserProfile } from '../types';
import type { AssigneesFilteringSelection } from './types';
import { UNKNOWN } from './translations';
import type { AssigneesFilteringSelection, UserInfoWithAvatar } from './types';
export const getSortField = (profile: UserProfileWithAvatar) =>
profile.user.full_name?.toLowerCase() ??
profile.user.email?.toLowerCase() ??
profile.user.username.toLowerCase();
export const getSortField = (profile: UserProfileWithAvatar | UserInfoWithAvatar) =>
profile.user?.full_name?.toLowerCase() ??
profile.user?.email?.toLowerCase() ??
profile.user?.username.toLowerCase() ??
UNKNOWN;
export const moveCurrentUserToBeginning = <T extends { uid: string }>(
currentUserProfile?: T,

View file

@ -6,6 +6,7 @@
*/
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import type { UserWithProfileInfo } from '../../../common/api';
export interface Assignee {
uid: string;
@ -18,3 +19,4 @@ export interface AssigneeWithProfile extends Assignee {
export type UserInfoWithAvatar = Partial<Pick<UserProfileWithAvatar, 'user' | 'data'>>;
export type AssigneesFilteringSelection = UserProfileWithAvatar | null;
export type CaseUserWithProfileInfo = UserWithProfileInfo;

View file

@ -6,80 +6,104 @@
*/
import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock';
import { convertToUserInfo } from './user_converter';
import { convertToCaseUserWithProfileInfo, convertToUserInfo } from './user_converter';
describe('convertToUserInfo', () => {
it('returns undefined when the username is an empty string and the profile uid is not defined', () => {
expect(convertToUserInfo({ username: '', email: null, fullName: null })).toBeUndefined();
});
describe('user_converter', () => {
describe('convertToUserInfo', () => {
it('returns undefined when the username is an empty string and the profile uid is not defined', () => {
expect(convertToUserInfo({ username: '', email: null, fullName: null })).toBeUndefined();
});
it('returns a key of 123 and empty user info when the username is an empty string and the profile uid is not found', () => {
expect(
convertToUserInfo({ username: '', profileUid: '123', email: null, fullName: null })
).toEqual({
key: '123',
userInfo: {},
it('returns a key of 123 and empty user info when the username is an empty string and the profile uid is not found', () => {
expect(
convertToUserInfo({ username: '', profileUid: '123', email: null, fullName: null })
).toEqual({
key: '123',
userInfo: {},
});
});
it('returns the profile uid as the key and full profile when the profile uid is found', () => {
expect(
convertToUserInfo(
{ profileUid: userProfiles[0].uid, email: null, fullName: null, username: null },
userProfilesMap
)
).toMatchInlineSnapshot(`
Object {
"key": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
"userInfo": Object {
"data": Object {},
"enabled": true,
"uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
"user": Object {
"email": "damaged_raccoon@elastic.co",
"full_name": "Damaged Raccoon",
"username": "damaged_raccoon",
},
},
}
`);
});
it('returns the username as the key and the user info using the existing elastic user information', () => {
expect(convertToUserInfo({ username: 'sam', fullName: 'Sam Smith', email: 'sam@sam.com' }))
.toMatchInlineSnapshot(`
Object {
"key": "sam",
"userInfo": Object {
"user": Object {
"email": "sam@sam.com",
"full_name": "Sam Smith",
"username": "sam",
},
},
}
`);
});
it('returns the username as the key and the user info using the existing elastic user information when the profile uid is not found', () => {
expect(
convertToUserInfo({
username: 'sam',
fullName: 'Sam Smith',
email: 'sam@sam.com',
profileUid: '123',
})
).toMatchInlineSnapshot(`
Object {
"key": "sam",
"userInfo": Object {
"user": Object {
"email": "sam@sam.com",
"full_name": "Sam Smith",
"username": "sam",
},
},
}
`);
});
});
it('returns the profile uid as the key and full profile when the profile uid is found', () => {
expect(
convertToUserInfo(
{ profileUid: userProfiles[0].uid, email: null, fullName: null, username: null },
userProfilesMap
)
).toMatchInlineSnapshot(`
Object {
"key": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
"userInfo": Object {
"data": Object {},
"enabled": true,
"uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
describe('convertToCaseUserWithProfileInfo', () => {
it('converts a CaseUse to a CaseUserWithProfileInfo correctly', () => {
expect(
convertToCaseUserWithProfileInfo({
username: 'test',
email: 'test@elastic.co',
fullName: 'Test',
profileUid: 'test-id',
})
).toMatchInlineSnapshot(`
Object {
"uid": "test-id",
"user": Object {
"email": "damaged_raccoon@elastic.co",
"full_name": "Damaged Raccoon",
"username": "damaged_raccoon",
"email": "test@elastic.co",
"full_name": "Test",
"username": "test",
},
},
}
`);
});
it('returns the username as the key and the user info using the existing elastic user information', () => {
expect(convertToUserInfo({ username: 'sam', fullName: 'Sam Smith', email: 'sam@sam.com' }))
.toMatchInlineSnapshot(`
Object {
"key": "sam",
"userInfo": Object {
"user": Object {
"email": "sam@sam.com",
"full_name": "Sam Smith",
"username": "sam",
},
},
}
`);
});
it('returns the username as the key and the user info using the existing elastic user information when the profile uid is not found', () => {
expect(
convertToUserInfo({
username: 'sam',
fullName: 'Sam Smith',
email: 'sam@sam.com',
profileUid: '123',
})
).toMatchInlineSnapshot(`
Object {
"key": "sam",
"userInfo": Object {
"user": Object {
"email": "sam@sam.com",
"full_name": "Sam Smith",
"username": "sam",
},
},
}
`);
}
`);
});
});
});

View file

@ -7,11 +7,11 @@
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import { isEmpty } from 'lodash';
import type { ElasticUser } from '../../containers/types';
import type { UserInfoWithAvatar } from './types';
import type { CaseUser } from '../../containers/types';
import type { CaseUserWithProfileInfo, UserInfoWithAvatar } from './types';
export const convertToUserInfo = (
user: ElasticUser,
user: CaseUser,
userProfiles?: Map<string, UserProfileWithAvatar>
): { key: string; userInfo: UserInfoWithAvatar } | undefined => {
const username = user.username;
@ -35,7 +35,7 @@ export const convertToUserInfo = (
const isValidString = (value?: string | null): value is string => !isEmpty(value);
const createWithUsername = (username: string, user: ElasticUser) => {
const createWithUsername = (username: string, user: CaseUser) => {
return {
key: username,
userInfo: {
@ -43,3 +43,8 @@ const createWithUsername = (username: string, user: ElasticUser) => {
},
};
};
export const convertToCaseUserWithProfileInfo = (user: CaseUser): CaseUserWithProfileInfo => ({
uid: user.profileUid,
user: { email: user.email, full_name: user.fullName, username: user.username },
});

View file

@ -26,8 +26,14 @@ import {
pushedCase,
tags,
findCaseUserActionsResponse,
getCaseUsersMockResponse,
} from '../mock';
import type { CaseConnectors, CaseUpdateRequest, ResolvedCase } from '../../../common/ui/types';
import type {
CaseConnectors,
CaseUpdateRequest,
CaseUsers,
ResolvedCase,
} from '../../../common/ui/types';
import { SeverityAll } from '../../../common/ui/types';
import type {
CasePatchRequest,
@ -146,3 +152,6 @@ export const getCaseConnectors = async (
caseId: string,
signal: AbortSignal
): Promise<CaseConnectors> => Promise.resolve(getCaseConnectorsMockResponse());
export const getCaseUsers = async (caseId: string, signal: AbortSignal): Promise<CaseUsers> =>
Promise.resolve(getCaseUsersMockResponse());

View file

@ -14,6 +14,7 @@ import type {
FetchCasesProps,
ResolvedCase,
FindCaseUserActions,
CaseUsers,
} from '../../common/ui/types';
import { SeverityAll, SortFieldCase, StatusAll } from '../../common/ui/types';
import type {
@ -39,6 +40,7 @@ import {
getCaseFindUserActionsUrl,
getCaseCommentDeleteUrl,
getCaseConnectorsUrl,
getCaseUsersUrl,
} from '../../common/api';
import {
CASE_REPORTERS_URL,
@ -406,3 +408,10 @@ export const getCaseConnectors = async (
{}
);
};
export const getCaseUsers = async (caseId: string, signal: AbortSignal): Promise<CaseUsers> => {
return KibanaServices.get().http.fetch<CaseUsers>(getCaseUsersUrl(caseId), {
method: 'GET',
signal,
});
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { ElasticUser } from '../types';
import type { CaseUser } from '../types';
import type {
ActionConnector,
ActionTypeConnector,
@ -38,11 +38,11 @@ export interface CaseConfigure {
closureType: ClosureType;
connector: CasesConfigure['connector'];
createdAt: string;
createdBy: ElasticUser;
createdBy: CaseUser;
error: string | null;
mappings: CaseConnectorMapping[];
updatedAt: string;
updatedBy: ElasticUser;
updatedBy: CaseUser;
version: string;
owner: string;
}

View file

@ -25,6 +25,7 @@ export const casesQueriesKeys = {
caseMetrics: (id: string, features: SingleCaseMetricsFeature[]) =>
[...casesQueriesKeys.case(id), 'metrics', features] as const,
caseConnectors: (id: string) => [...casesQueriesKeys.case(id), 'connectors'],
caseUsers: (id: string) => [...casesQueriesKeys.case(id), 'users'],
userActions: (id: string) => [...casesQueriesKeys.case(id), 'user-actions'] as const,
userProfiles: () => [...casesQueriesKeys.users, 'user-profiles'] as const,
userProfilesList: (ids: string[]) => [...casesQueriesKeys.userProfiles(), ids] as const,

View file

@ -16,6 +16,7 @@ import type {
ExternalReferenceComment,
PersistableComment,
FindCaseUserActions,
CaseUsers,
} from '../../common/ui/types';
import type {
CaseConnector,
@ -957,3 +958,110 @@ export const getPersistableStateAttachment = (
...viewObject,
}),
});
export const getCaseUsersMockResponse = (): CaseUsers => {
return {
participants: [
{
user: {
email: 'participant_1@elastic.co',
full_name: 'Participant 1',
username: 'participant_1',
},
},
{
user: {
email: 'participant_2@elastic.co',
full_name: null,
username: 'participant_2',
},
},
{
user: {
email: null,
full_name: null,
username: 'participant_3',
},
},
{
user: {
email: null,
full_name: null,
username: 'participant_4',
},
uid: 'participant_4_uid',
avatar: { initials: 'P4' },
},
{
user: {
email: 'participant_5@elastic.co',
full_name: 'Participant 5',
username: 'participant_5',
},
uid: 'participant_5_uid',
},
],
reporter: {
user: {
email: 'reporter_1@elastic.co',
full_name: 'Reporter 1',
username: 'reporter_1',
},
uid: 'reporter_1_uid',
avatar: { initials: 'R1' },
},
assignees: [
{
user: {
email: null,
full_name: null,
username: null,
},
uid: 'u_62h24XVQzG4-MuH1-DqPmookrJY23aRa9h4fyULR6I8_0',
},
{
user: {
email: null,
full_name: null,
username: 'elastic',
},
uid: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0',
},
{
user: {
email: 'fuzzy_marten@profiles.elastic.co',
full_name: 'Fuzzy Marten',
username: 'fuzzy_marten',
},
uid: 'u_3OgKOf-ogtr8kJ5B0fnRcqzXs2aQQkZLtzKEEFnKaYg_0',
},
{
user: {
email: 'misty_mackerel@profiles.elastic.co',
full_name: 'Misty Mackerel',
username: 'misty_mackerel',
},
uid: 'u_BXf_iGxcnicv4l-3-ER7I-XPpfanAFAap7uls86xV7A_0',
},
],
unassignedUsers: [
{
user: {
email: '',
full_name: '',
username: 'cases_no_connectors',
},
uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0',
},
{
user: {
email: 'valid_chimpanzee@profiles.elastic.co',
full_name: 'Valid Chimpanzee',
username: 'valid_chimpanzee',
},
uid: 'u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0',
},
],
};
};

View file

@ -8,14 +8,7 @@
import { renderHook } from '@testing-library/react-hooks';
import type { UseFindCaseUserActions } from './use_find_case_user_actions';
import { useFindCaseUserActions } from './use_find_case_user_actions';
import {
basicCase,
caseUserActions,
elasticUser,
findCaseUserActionsResponse,
getUserAction,
} from './mock';
import { Actions } from '../../common/api';
import { basicCase, findCaseUserActionsResponse } from './mock';
import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { testQueryClient } from '../common/mock';
@ -53,9 +46,10 @@ describe('UseFindCaseUserActions', () => {
expect.objectContaining({
...initialData,
data: {
caseUserActions: [...findCaseUserActionsResponse.userActions],
participants: [elasticUser],
profileUids: new Set(),
userActions: [...findCaseUserActionsResponse.userActions],
total: 20,
perPage: 1000,
page: 1,
},
isError: false,
isLoading: false,
@ -80,137 +74,4 @@ describe('UseFindCaseUserActions', () => {
expect(spy).toHaveBeenCalledWith(basicCase.id, expect.any(AbortSignal));
expect(addError).toHaveBeenCalled();
});
describe('getProfileUids', () => {
it('aggregates the uids from the createdBy field of a user action', async () => {
jest.spyOn(api, 'findCaseUserActions').mockReturnValue(
Promise.resolve({
page: 1,
perPage: 1000,
total: 20,
userActions: [getUserAction('pushed', Actions.add, { createdBy: { profileUid: '456' } })],
})
);
const { result, waitForNextUpdate } = renderHook<string, UseFindCaseUserActions>(
() => useFindCaseUserActions(basicCase.id),
{ wrapper }
);
await waitForNextUpdate();
expect(result.current.data?.profileUids).toMatchInlineSnapshot(`
Set {
"456",
}
`);
});
it('aggregates the uids from a push', async () => {
jest.spyOn(api, 'findCaseUserActions').mockReturnValue(
Promise.resolve({
page: 1,
perPage: 1000,
total: 20,
userActions: [
getUserAction('pushed', Actions.add, {
payload: { externalService: { pushedBy: { profileUid: '123' } } },
}),
],
})
);
const { result, waitForNextUpdate } = renderHook<string, UseFindCaseUserActions>(
() => useFindCaseUserActions(basicCase.id),
{ wrapper }
);
await waitForNextUpdate();
expect(result.current.data?.profileUids).toMatchInlineSnapshot(`
Set {
"123",
}
`);
});
it('aggregates the uids from an assignment add user action', async () => {
jest.spyOn(api, 'findCaseUserActions').mockReturnValue(
Promise.resolve({
page: 1,
perPage: 1000,
total: 20,
userActions: [...caseUserActions, getUserAction('assignees', Actions.add)],
})
);
const { result, waitForNextUpdate } = renderHook<string, UseFindCaseUserActions>(
() => useFindCaseUserActions(basicCase.id),
{ wrapper }
);
await waitForNextUpdate();
expect(result.current.data?.profileUids).toMatchInlineSnapshot(`
Set {
"u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
"u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0",
}
`);
});
it('ignores duplicate uids', async () => {
jest.spyOn(api, 'findCaseUserActions').mockReturnValue(
Promise.resolve({
page: 1,
perPage: 1000,
total: 20,
userActions: [
...caseUserActions,
getUserAction('assignees', Actions.add),
getUserAction('assignees', Actions.add),
],
})
);
const { result, waitForNextUpdate } = renderHook<string, UseFindCaseUserActions>(
() => useFindCaseUserActions(basicCase.id),
{ wrapper }
);
await waitForNextUpdate();
expect(result.current.data?.profileUids).toMatchInlineSnapshot(`
Set {
"u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
"u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0",
}
`);
});
it('aggregates the uids from an assignment delete user action', async () => {
jest.spyOn(api, 'findCaseUserActions').mockReturnValue(
Promise.resolve({
page: 1,
perPage: 1000,
total: 20,
userActions: [...caseUserActions, getUserAction('assignees', Actions.delete)],
})
);
const { result, waitForNextUpdate } = renderHook<string, UseFindCaseUserActions>(
() => useFindCaseUserActions(basicCase.id),
{ wrapper }
);
await waitForNextUpdate();
expect(result.current.data?.profileUids).toMatchInlineSnapshot(`
Set {
"u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
"u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0",
}
`);
});
});
});

View file

@ -5,64 +5,22 @@
* 2.0.
*/
import { isEmpty, uniqBy } from 'lodash/fp';
import { useQuery } from '@tanstack/react-query';
import type { CaseUserActions } from '../../common/ui/types';
import { ActionTypes } from '../../common/api';
import type { FindCaseUserActions } from '../../common/ui/types';
import { findCaseUserActions } from './api';
import { isPushedUserAction } from '../../common/utils/user_actions';
import type { ServerError } from '../types';
import { useToasts } from '../common/lib/kibana';
import { ERROR_TITLE } from './translations';
import { casesQueriesKeys } from './constants';
export const getProfileUids = (userActions: CaseUserActions[]) => {
const uids = userActions.reduce<Set<string>>((acc, userAction) => {
if (userAction.type === ActionTypes.assignees) {
const uidsFromPayload = userAction.payload.assignees.map((assignee) => assignee.uid);
for (const uid of uidsFromPayload) {
acc.add(uid);
}
}
if (
isPushedUserAction<'camelCase'>(userAction) &&
userAction.payload.externalService.pushedBy.profileUid != null
) {
acc.add(userAction.payload.externalService.pushedBy.profileUid);
}
if (userAction.createdBy.profileUid != null) {
acc.add(userAction.createdBy.profileUid);
}
return acc;
}, new Set());
return uids;
};
export const useFindCaseUserActions = (caseId: string) => {
const toasts = useToasts();
const abortCtrlRef = new AbortController();
return useQuery(
return useQuery<FindCaseUserActions, ServerError>(
casesQueriesKeys.userActions(caseId),
async () => {
const response = await findCaseUserActions(caseId, abortCtrlRef.signal);
const participants = !isEmpty(response.userActions)
? uniqBy('createdBy.username', response.userActions).map((cau) => cau.createdBy)
: [];
const caseUserActions = !isEmpty(response.userActions) ? response.userActions : [];
const profileUids = getProfileUids(caseUserActions);
return {
caseUserActions,
participants,
profileUids,
};
return findCaseUserActions(caseId, abortCtrlRef.signal);
},
{
onError: (error: ServerError) => {

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useQuery } from '@tanstack/react-query';
import { casesQueriesKeys } from './constants';
import type { ServerError } from '../types';
import { useCasesToast } from '../common/use_cases_toast';
import { getCaseUsers } from './api';
import * as i18n from './translations';
import type { CaseUsers } from './types';
export const useGetCaseUsers = (caseId: string) => {
const { showErrorToast } = useCasesToast();
return useQuery<CaseUsers, ServerError>(
casesQueriesKeys.caseUsers(caseId),
() => {
const abortCtrlRef = new AbortController();
return getCaseUsers(caseId, abortCtrlRef.signal);
},
{
onError: (error: ServerError) => {
showErrorToast(error, { title: i18n.ERROR_TITLE });
},
}
);
};
export type UseGetCaseUsers = ReturnType<typeof useGetCaseUsers>;

View file

@ -9,6 +9,7 @@ import { uniqBy, isEmpty } from 'lodash';
import type { UserProfile } from '@kbn/security-plugin/common';
import type { IBasePath } from '@kbn/core-http-browser';
import type { SecurityPluginStart } from '@kbn/security-plugin/server';
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import { CASE_VIEW_PAGE_TABS } from '../../../common/types';
import { isPushedUserAction } from '../../../common/utils/user_actions';
import type {
@ -434,8 +435,9 @@ export const getDurationForUpdate = ({
export const getUserProfiles = async (
securityStartPlugin: SecurityPluginStart,
uids: Set<string>
): Promise<Map<string, UserProfile>> => {
uids: Set<string>,
dataPath?: string
): Promise<Map<string, UserProfileWithAvatar>> => {
if (uids.size <= 0) {
return new Map();
}
@ -443,9 +445,10 @@ export const getUserProfiles = async (
const userProfiles =
(await securityStartPlugin.userProfiles.bulkGet({
uids,
dataPath,
})) ?? [];
return userProfiles.reduce<Map<string, UserProfile>>((acc, profile) => {
return userProfiles.reduce<Map<string, UserProfileWithAvatar>>((acc, profile) => {
acc.set(profile.uid, profile);
return acc;
}, new Map());

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import type { UserProfile } from '@kbn/security-plugin/common';
import type { GetCaseUsersResponse, User } from '../../../common/api';
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import type { GetCaseUsersResponse, User, UserWithProfileInfo } from '../../../common/api';
import { GetCaseUsersResponseRt } from '../../../common/api';
import type { OwnerEntity } from '../../authorization';
import { Operations } from '../../authorization';
@ -51,14 +51,17 @@ export const getUsers = async (
.map((participant) => participant.user.profile_uid) as string[];
const assigneesUids = theCase.case.assignees.map((assignee) => assignee.uid);
const reporter = theCase.case.created_by;
const reporterProfileIdAsArray = reporter.profile_uid != null ? [reporter.profile_uid] : [];
const userProfileUids = new Set([
...assignedAndUnassignedUsers,
...participantsUids,
...assigneesUids,
...reporterProfileIdAsArray,
]);
const userProfiles = await getUserProfiles(securityStartPlugin, userProfileUids);
const userProfiles = await getUserProfiles(securityStartPlugin, userProfileUids, 'avatar');
const participantsResponse = convertUserInfoToResponse(
userProfiles,
@ -69,15 +72,19 @@ export const getUsers = async (
);
const assigneesResponse = convertUserInfoToResponse(userProfiles, theCase.case.assignees);
const reporterResponse = convertUserInfoToResponse(userProfiles, [
{ uid: reporter.profile_uid, user: reporter },
]);
/**
* To avoid duplicates, a user that is
* a participant or an assignee should not be
* a participant or an assignee or a reporter should not be
* part of the assignedAndUnassignedUsers Set
*/
const unassignedUsers = removeAllFromSet(assignedAndUnassignedUsers, [
...participantsUids,
...assigneesUids,
...reporterProfileIdAsArray,
]);
const unassignedUsersResponse = convertUserInfoToResponse(
@ -91,6 +98,7 @@ export const getUsers = async (
participants: participantsResponse,
assignees: assigneesResponse,
unassignedUsers: unassignedUsersResponse,
reporter: reporterResponse[0],
};
return GetCaseUsersResponseRt.encode(results);
@ -104,9 +112,9 @@ export const getUsers = async (
};
const convertUserInfoToResponse = (
userProfiles: Map<string, UserProfile>,
userProfiles: Map<string, UserProfileWithAvatar>,
usersInfo: Array<{ uid: string | undefined; user?: User }>
): User[] => {
): UserWithProfileInfo[] => {
const response = [];
for (const info of usersInfo) {
@ -117,17 +125,20 @@ const convertUserInfoToResponse = (
};
const getUserInformation = (
userProfiles: Map<string, UserProfile>,
userProfiles: Map<string, UserProfileWithAvatar>,
uid: string | undefined,
userInfo?: User
): User => {
): UserWithProfileInfo => {
const userProfile = uid != null ? userProfiles.get(uid) : undefined;
return {
email: userProfile?.user.email ?? userInfo?.email,
full_name: userProfile?.user.full_name ?? userInfo?.full_name,
username: userProfile?.user.username ?? userInfo?.username,
profile_uid: userProfile?.uid ?? uid ?? userInfo?.profile_uid,
user: {
email: userProfile?.user.email ?? userInfo?.email ?? null,
full_name: userProfile?.user.full_name ?? userInfo?.full_name ?? null,
username: userProfile?.user.username ?? userInfo?.username ?? null,
},
avatar: userProfile?.data.avatar,
uid: userProfile?.uid ?? uid ?? userInfo?.profile_uid,
};
};

View file

@ -8,34 +8,38 @@
import type SuperTest from 'supertest';
import { parse as parseCookie, Cookie } from 'tough-cookie';
import { UserProfileBulkGetParams, UserProfileServiceStart } from '@kbn/security-plugin/server';
import { INTERNAL_SUGGEST_USER_PROFILES_URL } from '@kbn/cases-plugin/common/constants';
import { SuggestUserProfilesRequest } from '@kbn/cases-plugin/common/api';
import { UserProfileService } from '@kbn/cases-plugin/server/services';
import { UserProfileAvatarData } from '@kbn/security-plugin/common';
import { UserProfile, UserProfileWithAvatar } from '@kbn/user-profile-components';
import { superUser } from '../authentication/users';
import { User } from '../authentication/types';
import { getSpaceUrlPrefix } from './helpers';
import { FtrProviderContext as CommonFtrProviderContext } from '../../ftr_provider_context';
import { getUserInfo } from '../authentication';
type BulkGetUserProfilesParams = Omit<UserProfileBulkGetParams, 'uids'> & { uids: string[] };
interface BulkGetUserProfilesParams<T> {
uids: string[];
dataPath?: T;
}
export const generateFakeAssignees = (num: number) =>
Array.from(Array(num).keys()).map((uid) => {
return { uid: `${uid}` };
});
export const bulkGetUserProfiles = async ({
export const bulkGetUserProfiles = async <T extends string>({
supertest,
req,
expectedHttpCode = 200,
auth = { user: superUser, space: null },
}: {
supertest: SuperTest.SuperTest<SuperTest.Test>;
req: BulkGetUserProfilesParams;
req: BulkGetUserProfilesParams<T>;
expectedHttpCode?: number;
auth?: { user: User; space: string | null };
}): ReturnType<UserProfileServiceStart['bulkGet']> => {
}): Promise<Array<T extends 'avatar' ? UserProfileWithAvatar : UserProfile>> => {
const { uids, ...restParams } = req;
const uniqueIDs = [...new Set(uids)];
@ -70,6 +74,31 @@ export const suggestUserProfiles = async ({
return profiles;
};
/**
* Updates the avatar of a user.
* The API needs a valid user session.
* The session acts as the user identifier
* whose the avatar is updated.
*/
export const updateUserProfileAvatar = async ({
supertest,
req,
expectedHttpCode = 200,
headers = {},
}: {
supertest: SuperTest.SuperTest<SuperTest.Test>;
req: UserProfileAvatarData;
expectedHttpCode?: number;
headers?: Record<string, unknown>;
}): Promise<void> => {
await supertest
.post('/internal/security/user_profile/_data')
.set('kbn-xsrf', 'true')
.set(headers)
.send({ avatar: req })
.expect(expectedHttpCode);
};
export const loginUsers = async ({
supertest,
users = [superUser],

View file

@ -8,6 +8,7 @@
import expect from '@kbn/expect';
import { Cookie } from 'tough-cookie';
import { UserProfile } from '@kbn/security-plugin/common';
import { GetCaseUsersResponseRt } from '@kbn/cases-plugin/common/api';
import { securitySolutionOnlyAllSpacesRole } from '../../../../common/lib/authentication/roles';
import { getPostCaseRequest } from '../../../../common/lib/mock';
import {
@ -18,6 +19,7 @@ import {
getCaseUsers,
loginUsers,
bulkGetUserProfiles,
updateUserProfileAvatar,
} from '../../../../common/lib/api';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import { createUsersAndRoles, deleteUsersAndRoles } from '../../../../common/lib/authentication';
@ -56,12 +58,15 @@ export default ({ getService }: FtrProviderContext): void => {
title: 'new title',
});
const { participants, assignees, unassignedUsers } = await getCaseUsers({
const { participants, assignees, unassignedUsers, reporter } = await getCaseUsers({
caseId: postedCase.id,
supertest,
});
expect(participants).to.eql([{ username: 'elastic', full_name: null, email: null }]);
expect(participants).to.eql([
{ user: { username: 'elastic', full_name: null, email: null } },
]);
expect(reporter).to.eql({ user: { username: 'elastic', full_name: null, email: null } });
expect(assignees).to.eql([]);
expect(unassignedUsers).to.eql([]);
});
@ -91,14 +96,19 @@ export default ({ getService }: FtrProviderContext): void => {
caseId: '163d5820-1284-21ed-81af-63a2bdfb2bf9',
});
const { participants, assignees, unassignedUsers } = await getCaseUsers({
const { participants, assignees, unassignedUsers, reporter } = await getCaseUsers({
caseId: theCase.id,
supertest,
});
expect(participants).to.eql([{ username: null, full_name: null, email: null }]);
expect(assignees).to.eql([{ profile_uid: 'abc' }]);
expect(unassignedUsers).to.eql([{ profile_uid: 'dfg' }]);
expect(participants).to.eql([{ user: { username: null, full_name: null, email: null } }]);
expect(reporter).to.eql({ user: { username: null, full_name: null, email: null } });
expect(assignees).to.eql([
{ uid: 'abc', user: { username: null, full_name: null, email: null } },
]);
expect(unassignedUsers).to.eql([
{ uid: 'dfg', user: { username: null, full_name: null, email: null } },
]);
});
});
@ -141,10 +151,27 @@ export default ({ getService }: FtrProviderContext): void => {
createCase(supertestWithoutAuth, getPostCaseRequest(), 200, null, secOnlyHeaders),
]);
/**
* Update superUser profile avatar.
* Need for schema and response verification
*/
await updateUserProfileAvatar({
supertest,
req: {
initials: 'ES',
color: '#6092C0',
imageUrl: 'my-image',
},
headers: superUserHeaders,
});
const userProfiles = await bulkGetUserProfiles({
supertest,
// @ts-expect-error: profile uids are defined for both users
req: { uids: [superUserCase.created_by.profile_uid, secUserCase.created_by.profile_uid] },
req: {
// @ts-expect-error: profile uids are defined for both users
uids: [superUserCase.created_by.profile_uid, secUserCase.created_by.profile_uid],
dataPath: 'avatar',
},
});
superUserProfile = userProfiles[0];
@ -159,156 +186,251 @@ export default ({ getService }: FtrProviderContext): void => {
);
});
it('returns only the creator of the case', async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest(),
200,
null,
superUserHeaders
);
describe('schema', () => {
it('ensures that the schema of security plugin is as expected', async () => {
const res = await bulkGetUserProfiles({
supertest: supertestWithoutAuth,
req: {
uids: [superUserProfile.uid],
dataPath: 'avatar',
},
});
const { participants, assignees, unassignedUsers } = await getCaseUsers({
caseId: postedCase.id,
supertest,
const userProfile = res[0];
try {
const userToValidate = {
user: {
email: userProfile.user.email ?? null,
full_name: userProfile.user.full_name ?? null,
username: userProfile.user.username ?? null,
},
avatar: userProfile.data.avatar,
uid: userProfile.uid,
};
GetCaseUsersResponseRt.encode({
assignees: [userToValidate],
unassignedUsers: [userToValidate],
participants: [userToValidate],
reporter: userToValidate,
});
} catch (error) {
throw new Error(
`Failed encoding GetCaseUsersResponse schema. Schema mismatch between Case users and Security user profiles. Error message: ${error}`
);
}
});
expect(participants).to.eql([
{
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
profile_uid: superUserProfile.uid,
},
]);
expect(assignees).to.eql([]);
expect(unassignedUsers).to.eql([]);
});
it('returns one participant if it is the only one that participates to the case', async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest(),
200,
null,
superUserHeaders
);
describe('case users', () => {
it('returns only the creator of the case', async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest(),
200,
null,
superUserHeaders
);
await changeCaseTitle({
supertest,
caseId: postedCase.id,
version: postedCase.version,
title: 'new title',
headers: superUserHeaders,
const { participants, assignees, unassignedUsers, reporter } = await getCaseUsers({
caseId: postedCase.id,
supertest,
});
expect(participants).to.eql([
{
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
},
avatar: superUserProfile.data.avatar,
uid: superUserProfile.uid,
},
]);
expect(reporter).to.eql({
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
},
avatar: superUserProfile.data.avatar,
uid: superUserProfile.uid,
});
expect(assignees).to.eql([]);
expect(unassignedUsers).to.eql([]);
});
const { participants, assignees, unassignedUsers } = await getCaseUsers({
caseId: postedCase.id,
supertest,
it('returns one participant if it is the only one that participates to the case', async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest(),
200,
null,
superUserHeaders
);
await changeCaseTitle({
supertest,
caseId: postedCase.id,
version: postedCase.version,
title: 'new title',
headers: superUserHeaders,
});
const { participants, assignees, unassignedUsers, reporter } = await getCaseUsers({
caseId: postedCase.id,
supertest,
});
expect(participants).to.eql([
{
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
},
avatar: superUserProfile.data.avatar,
uid: superUserProfile.uid,
},
]);
expect(reporter).to.eql({
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
},
avatar: superUserProfile.data.avatar,
uid: superUserProfile.uid,
});
expect(assignees).to.eql([]);
expect(unassignedUsers).to.eql([]);
});
expect(participants).to.eql([
{
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
profile_uid: superUserProfile.uid,
},
]);
it('returns all participants of the case', async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest(),
200,
null,
superUserHeaders
);
expect(assignees).to.eql([]);
expect(unassignedUsers).to.eql([]);
});
await changeCaseTitle({
supertest,
caseId: postedCase.id,
version: postedCase.version,
title: 'new title',
headers: secOnlyHeaders,
});
it('returns all participants of the case', async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest(),
200,
null,
superUserHeaders
);
const { participants, assignees, unassignedUsers, reporter } = await getCaseUsers({
caseId: postedCase.id,
supertest,
});
await changeCaseTitle({
supertest,
caseId: postedCase.id,
version: postedCase.version,
title: 'new title',
headers: secOnlyHeaders,
expect(participants).to.eql([
{
user: {
username: secUserProfile.user.username,
full_name: secUserProfile.user.full_name,
email: secUserProfile.user.email,
},
uid: secUserProfile.uid,
},
{
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
},
avatar: superUserProfile.data.avatar,
uid: superUserProfile.uid,
},
]);
expect(reporter).to.eql({
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
},
avatar: superUserProfile.data.avatar,
uid: superUserProfile.uid,
});
expect(assignees).to.eql([]);
expect(unassignedUsers).to.eql([]);
});
const { participants, assignees, unassignedUsers } = await getCaseUsers({
caseId: postedCase.id,
supertest,
it('does not return duplicate participants', async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest(),
200,
null,
superUserHeaders
);
const updatedCases = await changeCaseTitle({
supertest,
caseId: postedCase.id,
version: postedCase.version,
title: 'new title',
headers: secOnlyHeaders,
});
await changeCaseDescription({
supertest,
caseId: updatedCases[0].id,
version: updatedCases[0].version,
description: 'new desc',
headers: secOnlyHeaders,
});
const { participants, assignees, unassignedUsers, reporter } = await getCaseUsers({
caseId: postedCase.id,
supertest,
});
expect(participants).to.eql([
{
user: {
username: secUserProfile.user.username,
full_name: secUserProfile.user.full_name,
email: secUserProfile.user.email,
},
uid: secUserProfile.uid,
},
{
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
},
avatar: superUserProfile.data.avatar,
uid: superUserProfile.uid,
},
]);
expect(reporter).to.eql({
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
},
avatar: superUserProfile.data.avatar,
uid: superUserProfile.uid,
});
expect(assignees).to.eql([]);
expect(unassignedUsers).to.eql([]);
});
expect(participants).to.eql([
{
username: secUserProfile.user.username,
full_name: secUserProfile.user.full_name,
email: secUserProfile.user.email,
profile_uid: secUserProfile.uid,
},
{
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
profile_uid: superUserProfile.uid,
},
]);
expect(assignees).to.eql([]);
expect(unassignedUsers).to.eql([]);
});
it('does not return duplicate participants', async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest(),
200,
null,
superUserHeaders
);
const updatedCases = await changeCaseTitle({
supertest,
caseId: postedCase.id,
version: postedCase.version,
title: 'new title',
headers: secOnlyHeaders,
});
await changeCaseDescription({
supertest,
caseId: updatedCases[0].id,
version: updatedCases[0].version,
description: 'new desc',
headers: secOnlyHeaders,
});
const { participants, assignees, unassignedUsers } = await getCaseUsers({
caseId: postedCase.id,
supertest,
});
expect(participants).to.eql([
{
username: secUserProfile.user.username,
full_name: secUserProfile.user.full_name,
email: secUserProfile.user.email,
profile_uid: secUserProfile.uid,
},
{
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
profile_uid: superUserProfile.uid,
},
]);
expect(assignees).to.eql([]);
expect(unassignedUsers).to.eql([]);
});
});

View file

@ -108,32 +108,47 @@ export default ({ getService }: FtrProviderContext): void => {
headers: superUserHeaders,
});
const { participants, assignees, unassignedUsers } = await getCaseUsers({
const { participants, assignees, unassignedUsers, reporter } = await getCaseUsers({
caseId: postedCase.id,
supertest,
});
expect(participants).to.eql([
{
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
profile_uid: superUserProfile.uid,
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
},
uid: superUserProfile.uid,
},
]);
expect(assignees).to.eql([
{
expect(reporter).to.eql({
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
profile_uid: superUserProfile.uid,
},
uid: superUserProfile.uid,
});
expect(assignees).to.eql([
{
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
},
uid: superUserProfile.uid,
},
{
username: secUserProfile.user.username,
full_name: secUserProfile.user.full_name,
email: secUserProfile.user.email,
profile_uid: secUserProfile.uid,
user: {
username: secUserProfile.user.username,
full_name: secUserProfile.user.full_name,
email: secUserProfile.user.email,
},
uid: secUserProfile.uid,
},
]);
@ -167,35 +182,50 @@ export default ({ getService }: FtrProviderContext): void => {
headers: superUserHeaders,
});
const { participants, assignees, unassignedUsers } = await getCaseUsers({
const { participants, assignees, unassignedUsers, reporter } = await getCaseUsers({
caseId: postedCase.id,
supertest,
});
expect(participants).to.eql([
{
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
profile_uid: superUserProfile.uid,
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
},
uid: superUserProfile.uid,
},
]);
expect(assignees).to.eql([
{
expect(reporter).to.eql({
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
profile_uid: superUserProfile.uid,
},
uid: superUserProfile.uid,
});
expect(assignees).to.eql([
{
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
},
uid: superUserProfile.uid,
},
]);
expect(unassignedUsers).to.eql([
{
username: secUserProfile.user.username,
full_name: secUserProfile.user.full_name,
email: secUserProfile.user.email,
profile_uid: secUserProfile.uid,
user: {
username: secUserProfile.user.username,
full_name: secUserProfile.user.full_name,
email: secUserProfile.user.email,
},
uid: secUserProfile.uid,
},
]);
});
@ -236,32 +266,47 @@ export default ({ getService }: FtrProviderContext): void => {
headers: superUserHeaders,
});
const { participants, assignees, unassignedUsers } = await getCaseUsers({
const { participants, assignees, unassignedUsers, reporter } = await getCaseUsers({
caseId: postedCase.id,
supertest,
});
expect(participants).to.eql([
{
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
profile_uid: superUserProfile.uid,
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
},
uid: superUserProfile.uid,
},
]);
expect(assignees).to.eql([
{
expect(reporter).to.eql({
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
profile_uid: superUserProfile.uid,
},
uid: superUserProfile.uid,
});
expect(assignees).to.eql([
{
user: {
username: superUserProfile.user.username,
full_name: superUserProfile.user.full_name,
email: superUserProfile.user.email,
},
uid: superUserProfile.uid,
},
{
username: secUserProfile.user.username,
full_name: secUserProfile.user.full_name,
email: secUserProfile.user.email,
profile_uid: secUserProfile.uid,
user: {
username: secUserProfile.user.username,
full_name: secUserProfile.user.full_name,
email: secUserProfile.user.email,
},
uid: secUserProfile.uid,
},
]);