[Security Solution][Endpoint] Bug fixes and unit test cases for the Trusted Apps list view under Policy Details (#113865)

* test: rename mock utility
* Tests: artifact grid test coverage
* Fix: CardCompressedHeader should use CardCompressedHeaderLayout
* Tests: ArtifactEntryCollapsibleCard test coverage
* Context Menu adjustments to test ids and to avoid react console errors/warnings
* add test id to truncate wrapper in ContextMenuItemWithRouterSupport
* Tests for ContextMenuWithRouterSupport
* new mocks test utils
* HTTP mocks for Policy Details Trusted apps list page
* tests for policy trusted apps selectors
* Refactor: move reusable fleet http mocks to `page/mocks`
* HTTP mocks for fleet get package policy and Agent status + mock for all Policy Details APIs
* Tests: Policy Details Trusted Apps List
* Moved `seededUUIDv4()` to `BaseDataGenerator` and changed trusted apps generator to use it
* change `createStartServicesMock` to optionally accept `coreStart` as input
* Show api load errors on policy TA list
This commit is contained in:
Paul Tavares 2021-10-12 09:20:11 -04:00 committed by GitHub
parent c3f1e0de54
commit 4436ed2f71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1211 additions and 265 deletions

View file

@ -127,6 +127,11 @@ export class BaseDataGenerator<GeneratedDoc extends {} = {}> {
return uuid.v4();
}
/** generate a seeded random UUID v4 */
protected seededUUIDv4(): string {
return uuid.v4({ random: [...this.randomNGenerator(255, 16)] });
}
/** Generate a random number up to the max provided */
protected randomN(max: number): number {
return Math.floor(this.random() * max);

View file

@ -45,7 +45,7 @@ export class TrustedAppGenerator extends BaseDataGenerator<TrustedApp> {
return merge(
this.generateTrustedAppForCreate(),
{
id: this.randomUUID(),
id: this.seededUUIDv4(),
version: this.randomString(5),
created_at: this.randomPastDate(),
updated_at: new Date().toISOString(),

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import uuid from 'uuid';
import seedrandom from 'seedrandom';
import semverLte from 'semver/functions/lte';
import { assertNever } from '@kbn/std';
@ -32,9 +31,10 @@ import {
import {
GetAgentPoliciesResponseItem,
GetPackagesResponse,
} from '../../../fleet/common/types/rest_spec';
import { EsAssetReference, KibanaAssetReference } from '../../../fleet/common/types/models';
import { agentPolicyStatuses } from '../../../fleet/common/constants';
EsAssetReference,
KibanaAssetReference,
agentPolicyStatuses,
} from '../../../fleet/common';
import { firstNonNullValue } from './models/ecs_safety_helpers';
import { EventOptions } from './types/generator';
import { BaseDataGenerator } from './data_generators/base_data_generator';
@ -406,6 +406,7 @@ const alertsDefaultDataStream = {
export class EndpointDocGenerator extends BaseDataGenerator {
commonInfo: HostInfo;
sequence: number = 0;
/**
* The EndpointDocGenerator parameters
*
@ -523,6 +524,7 @@ export class EndpointDocGenerator extends BaseDataGenerator {
data_stream: metadataDataStream,
};
}
/**
* Creates a malware alert from the simulated host represented by this EndpointDocGenerator
* @param ts - Timestamp to put in the event
@ -744,6 +746,7 @@ export class EndpointDocGenerator extends BaseDataGenerator {
}
return newAlert;
}
/**
* Creates an alert from the simulated host represented by this EndpointDocGenerator
* @param ts - Timestamp to put in the event
@ -900,6 +903,7 @@ export class EndpointDocGenerator extends BaseDataGenerator {
};
return newAlert;
}
/**
* Returns the default DLLs used in alerts
*/
@ -1871,10 +1875,6 @@ export class EndpointDocGenerator extends BaseDataGenerator {
};
}
private seededUUIDv4(): string {
return uuid.v4({ random: [...this.randomNGenerator(255, 16)] });
}
private randomHostPolicyResponseActionNames(): string[] {
return this.randomArray(this.randomN(8), () =>
this.randomChoice([

View file

@ -8,10 +8,10 @@
import { renderHook, RenderHookResult, RenderResult } from '@testing-library/react-hooks';
import { useHttp, useCurrentUser } from '../../lib/kibana';
import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges';
import { fleetGetCheckPermissionsHttpMock } from '../../../management/pages/endpoint_hosts/mocks';
import { securityMock } from '../../../../../security/public/mocks';
import { appRoutesService } from '../../../../../fleet/common';
import { AuthenticatedUser } from '../../../../../security/common';
import { fleetGetCheckPermissionsHttpMock } from '../../../management/pages/mocks';
jest.mock('../../lib/kibana');

View file

@ -91,8 +91,9 @@ export const createUseUiSetting$Mock = () => {
];
};
export const createStartServicesMock = (): StartServices => {
const core = coreMock.createStart();
export const createStartServicesMock = (
core: ReturnType<typeof coreMock.createStart> = coreMock.createStart()
): StartServices => {
core.uiSettings.get.mockImplementation(createUseUiSettingMock());
const { storage } = createSecuritySolutionStorageMock();
const data = dataPluginMock.createStartContract();

View file

@ -24,8 +24,8 @@ import { ExperimentalFeatures } from '../../../../common/experimental_features';
import { PLUGIN_ID } from '../../../../../fleet/common';
import { APP_ID, APP_PATH } from '../../../../common/constants';
import { KibanaContextProvider, KibanaServices } from '../../lib/kibana';
import { fleetGetPackageListHttpMock } from '../../../management/pages/endpoint_hosts/mocks';
import { getDeepLinks } from '../../../app/deep_links';
import { fleetGetPackageListHttpMock } from '../../../management/pages/mocks';
type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult;
@ -98,10 +98,7 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
const depsStart = depsStartMock();
const middlewareSpy = createSpyMiddleware();
const { storage } = createSecuritySolutionStorageMock();
const startServices: StartServices = {
...createStartServicesMock(),
...coreStart,
};
const startServices: StartServices = createStartServicesMock(coreStart);
const storeReducer = {
...SUB_PLUGINS_REDUCER,

View file

@ -15,10 +15,11 @@ import {
EuiIconProps,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import uuid from 'uuid';
import {
ContextMenuItemNavByRouter,
ContextMenuItemNavByRouterProps,
} from '../context_menu_with_router_support/context_menu_item_nav_by_router';
} from '../context_menu_with_router_support';
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
export interface ActionsContextMenuProps {
@ -48,6 +49,7 @@ export const ActionsContextMenu = memo<ActionsContextMenuProps>(
return (
<ContextMenuItemNavByRouter
{...itemProps}
key={uuid.v4()}
onClick={(ev) => {
handleCloseMenu();
if (itemProps.onClick) {

View file

@ -5,58 +5,20 @@
* 2.0.
*/
import { TrustedAppGenerator } from '../../../../common/endpoint/data_generators/trusted_app_generator';
import { cloneDeep } from 'lodash';
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint';
import React from 'react';
import { ArtifactCardGrid, ArtifactCardGridProps } from './artifact_card_grid';
// FIXME:PT refactor helpers below after merge of PR https://github.com/elastic/kibana/pull/113363
const getCommonItemDataOverrides = () => {
return {
name: 'some internal app',
description: 'this app is trusted by the company',
created_at: new Date('2021-07-01').toISOString(),
};
};
const getTrustedAppProvider = () =>
new TrustedAppGenerator('seed').generate(getCommonItemDataOverrides());
const getExceptionProvider = () => {
// cloneDeep needed because exception mock generator uses state across instances
return cloneDeep(
getExceptionListItemSchemaMock({
...getCommonItemDataOverrides(),
os_types: ['windows'],
updated_at: new Date().toISOString(),
created_by: 'Justa',
updated_by: 'Mara',
entries: [
{
field: 'process.hash.*',
operator: 'included',
type: 'match',
value: '1234234659af249ddf3e40864e9fb241',
},
{
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value: '/one/two/three',
},
],
tags: ['policy:all'],
})
);
};
import { fireEvent, act } from '@testing-library/react';
import {
getExceptionProviderMock,
getTrustedAppProviderMock,
} from '../artifact_entry_card/test_utils';
import { AnyArtifact } from '../artifact_entry_card';
describe.each([
['trusted apps', getTrustedAppProvider],
['exceptions/event filters', getExceptionProvider],
])('when using the ArtifactCardGrid component %s', (_, generateItem) => {
['trusted apps', getTrustedAppProviderMock],
['exceptions/event filters', getExceptionProviderMock],
])('when using the ArtifactCardGrid component with %s', (_, generateItem) => {
let appTestContext: AppContextTestRender;
let renderResult: ReturnType<AppContextTestRender['render']>;
let render: (
@ -64,34 +26,45 @@ describe.each([
) => ReturnType<AppContextTestRender['render']>;
let items: ArtifactCardGridProps['items'];
let pageChangeHandler: jest.Mock<ArtifactCardGridProps['onPageChange']>;
let expandCollapseHandler: jest.Mock<ArtifactCardGridProps['onExpandCollapse']>;
let cardComponentPropsProvider: Required<ArtifactCardGridProps>['cardComponentProps'];
let expandCollapseHandler: jest.MockedFunction<ArtifactCardGridProps['onExpandCollapse']>;
let cardComponentPropsProvider: jest.MockedFunction<
Required<ArtifactCardGridProps>['cardComponentProps']
>;
beforeEach(() => {
items = Array.from({ length: 5 }, () => generateItem());
pageChangeHandler = jest.fn();
expandCollapseHandler = jest.fn();
cardComponentPropsProvider = jest.fn().mockReturnValue({});
cardComponentPropsProvider = jest.fn((item) => {
return {
'data-test-subj': `card-${items.indexOf(item as AnyArtifact)}`,
};
});
appTestContext = createAppRootMockRenderer();
render = (props = {}) => {
renderResult = appTestContext.render(
<ArtifactCardGrid
{...{
items,
onPageChange: pageChangeHandler!,
onExpandCollapse: expandCollapseHandler!,
cardComponentProps: cardComponentPropsProvider,
'data-test-subj': 'testGrid',
...props,
}}
/>
);
const gridProps: ArtifactCardGridProps = {
items,
onPageChange: pageChangeHandler!,
onExpandCollapse: expandCollapseHandler!,
cardComponentProps: cardComponentPropsProvider,
pagination: {
pageSizeOptions: [5, 10],
pageSize: 5,
totalItemCount: items.length,
pageIndex: 0,
},
'data-test-subj': 'testGrid',
...props,
};
renderResult = appTestContext.render(<ArtifactCardGrid {...gridProps} />);
return renderResult;
};
});
it('should render the cards', () => {
cardComponentPropsProvider.mockImplementation(() => ({}));
render();
expect(renderResult.getAllByTestId('testGrid-card')).toHaveLength(5);
@ -100,24 +73,59 @@ describe.each([
it.each([
['header', 'testGrid-header'],
['expand/collapse placeholder', 'testGrid-header-expandCollapsePlaceHolder'],
['name column', 'testGrid-header-layout-title'],
['description column', 'testGrid-header-layout-description'],
['name column', 'testGrid-header-layout-titleHolder'],
['description column', 'testGrid-header-layout-descriptionHolder'],
['description column', 'testGrid-header-layout-cardActionsPlaceholder'],
])('should display the Grid Header - %s', (__, selector) => {
])('should display the Grid Header - %s', (__, testSubjId) => {
render();
expect(renderResult.getByTestId(selector)).not.toBeNull();
expect(renderResult.getByTestId(testSubjId)).not.toBeNull();
});
it.todo('should call onPageChange callback when paginating');
it('should call onPageChange callback when paginating', () => {
items = Array.from({ length: 15 }, () => generateItem());
render();
act(() => {
fireEvent.click(renderResult.getByTestId('pagination-button-next'));
});
it.todo('should use the props provided by cardComponentProps callback');
expect(pageChangeHandler).toHaveBeenCalledWith({ pageIndex: 1, pageSize: 5 });
});
it('should pass along the props provided by cardComponentProps callback', () => {
cardComponentPropsProvider.mockReturnValue({ 'data-test-subj': 'test-card' });
render();
expect(renderResult.getAllByTestId('test-card')).toHaveLength(5);
});
describe('and when cards are expanded/collapsed', () => {
it.todo('should call onExpandCollapse callback');
it('should call onExpandCollapse callback', () => {
render();
act(() => {
fireEvent.click(renderResult.getByTestId('card-0-header-expandCollapse'));
});
it.todo('should provide list of cards that are expanded and collapsed');
expect(expandCollapseHandler).toHaveBeenCalledWith({
expanded: [items[0]],
collapsed: items.slice(1),
});
});
it.todo('should show card expanded if card props defined it as such');
it('should show card expanded if card props defined it as such', () => {
const originalPropsProvider = cardComponentPropsProvider.getMockImplementation();
cardComponentPropsProvider.mockImplementation((item) => {
const props = originalPropsProvider!(item);
if (items.indexOf(item as AnyArtifact) === 1) {
props.expanded = true;
}
return props;
});
render();
expect(renderResult.getByTestId('card-1-criteriaConditions')).not.toBeNull();
});
});
});

View file

@ -63,7 +63,7 @@ export const GridHeader = memo<GridHeaderProps>(({ 'data-test-subj': dataTestSub
</strong>
</EuiText>
}
actionMenu={false}
actionMenu={true}
/>
</GridHeaderContainer>
);

View file

@ -11,11 +11,11 @@ import { ArtifactEntryCard, ArtifactEntryCardProps } from './artifact_entry_card
import { act, fireEvent, getByTestId } from '@testing-library/react';
import { AnyArtifact } from './types';
import { isTrustedApp } from './utils';
import { getTrustedAppProvider, getExceptionProvider } from './test_utils';
import { getTrustedAppProviderMock, getExceptionProviderMock } from './test_utils';
describe.each([
['trusted apps', getTrustedAppProvider],
['exceptions/event filters', getExceptionProvider],
['trusted apps', getTrustedAppProviderMock],
['exceptions/event filters', getExceptionProviderMock],
])('when using the ArtifactEntryCard component with %s', (_, generateItem) => {
let item: AnyArtifact;
let appTestContext: AppContextTestRender;
@ -48,10 +48,10 @@ describe.each([
'some internal app'
);
expect(renderResult.getByTestId('testCard-subHeader-touchedBy-createdBy').textContent).toEqual(
'Created byJJusta'
'Created byMMarty'
);
expect(renderResult.getByTestId('testCard-subHeader-touchedBy-updatedBy').textContent).toEqual(
'Updated byMMara'
'Updated byEEllamae'
);
});

View file

@ -13,11 +13,11 @@ import {
} from './artifact_entry_card_minified';
import { act, fireEvent } from '@testing-library/react';
import { AnyArtifact } from './types';
import { getTrustedAppProvider, getExceptionProvider } from './test_utils';
import { getTrustedAppProviderMock, getExceptionProviderMock } from './test_utils';
describe.each([
['trusted apps', getTrustedAppProvider],
['exceptions/event filters', getExceptionProvider],
['trusted apps', getTrustedAppProviderMock],
['exceptions/event filters', getExceptionProviderMock],
])('when using the ArtifactEntryCardMinified component with %s', (_, generateItem) => {
let item: AnyArtifact;
let appTestContext: AppContextTestRender;

View file

@ -0,0 +1,104 @@
/*
* 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 React from 'react';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint';
import { act, fireEvent } from '@testing-library/react';
import { AnyArtifact } from './types';
import { getTrustedAppProviderMock, getExceptionProviderMock } from './test_utils';
import {
ArtifactEntryCollapsibleCard,
ArtifactEntryCollapsibleCardProps,
} from './artifact_entry_collapsible_card';
describe.each([
['trusted apps', getTrustedAppProviderMock],
['exceptions/event filters', getExceptionProviderMock],
])('when using the ArtifactEntryCard component with %s', (_, generateItem) => {
let item: AnyArtifact;
let appTestContext: AppContextTestRender;
let renderResult: ReturnType<AppContextTestRender['render']>;
let render: (
props?: Partial<ArtifactEntryCollapsibleCardProps>
) => ReturnType<AppContextTestRender['render']>;
let handleOnExpandCollapse: jest.MockedFunction<
ArtifactEntryCollapsibleCardProps['onExpandCollapse']
>;
beforeEach(() => {
item = generateItem();
appTestContext = createAppRootMockRenderer();
handleOnExpandCollapse = jest.fn();
render = (props = {}) => {
const cardProps: ArtifactEntryCollapsibleCardProps = {
item,
onExpandCollapse: handleOnExpandCollapse,
'data-test-subj': 'testCard',
...props,
};
renderResult = appTestContext.render(<ArtifactEntryCollapsibleCard {...cardProps} />);
return renderResult;
};
});
it.each([
['expandCollapse button', 'testCard-header-expandCollapse'],
['name', 'testCard-header-titleHolder'],
['description', 'testCard-header-descriptionHolder'],
['assignment', 'testCard-header-effectScope'],
])('should show %s', (__, testSubjId) => {
render();
expect(renderResult.getByTestId(testSubjId)).not.toBeNull();
});
it('should NOT show actions menu if none are defined', async () => {
render();
expect(renderResult.queryByTestId('testCard-header-actions')).toBeNull();
});
it('should render card collapsed', () => {
render();
expect(renderResult.queryByTestId('testCard-header-criteriaConditions')).toBeNull();
});
it('should render card expanded', () => {
render({ expanded: true });
expect(renderResult.getByTestId('testCard-criteriaConditions')).not.toBeNull();
});
it('should call `onExpandCollapse` callback when button is clicked', () => {
render();
act(() => {
fireEvent.click(renderResult.getByTestId('testCard-header-expandCollapse'));
});
expect(handleOnExpandCollapse).toHaveBeenCalled();
});
it.each([
['title', 'testCard-header-titleHolder'],
['description', 'testCard-header-descriptionHolder'],
])('should truncate %s text when collapsed', (__, testSubjId) => {
render();
expect(renderResult.getByTestId(testSubjId).classList.contains('eui-textTruncate')).toBe(true);
});
it.each([
['title', 'testCard-header-titleHolder'],
['description', 'testCard-header-descriptionHolder'],
])('should NOT truncate %s text when expanded', (__, testSubjId) => {
render({ expanded: true });
expect(renderResult.getByTestId(testSubjId).classList.contains('eui-textTruncate')).toBe(false);
});
});

View file

@ -38,7 +38,6 @@ export const CardCompressedHeader = memo<CardCompressedHeaderProps>(
'data-test-subj': dataTestSubj,
}) => {
const getTestId = useTestIdGenerator(dataTestSubj);
const cssClassNames = useCollapsedCssClassNames(expanded);
const policyNavLinks = usePolicyNavLinks(artifact, policies);
const handleExpandCollapseClick = useCallback(() => {
@ -46,37 +45,31 @@ export const CardCompressedHeader = memo<CardCompressedHeaderProps>(
}, [onExpandCollapse]);
return (
<EuiFlexGroup responsive={false} alignItems="center" data-test-subj={dataTestSubj}>
<EuiFlexItem grow={false}>
<CardCompressedHeaderLayout
data-test-subj={dataTestSubj}
expanded={expanded}
expandToggle={
<CardExpandButton
expanded={expanded}
onClick={handleExpandCollapseClick}
data-test-subj={getTestId('expandCollapse')}
/>
</EuiFlexItem>
<EuiFlexItem className={cssClassNames}>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={2} className={cssClassNames} data-test-subj={getTestId('title')}>
<TextValueDisplay bold truncate={!expanded}>
{artifact.name}
</TextValueDisplay>
</EuiFlexItem>
<EuiFlexItem
grow={3}
className={cssClassNames}
data-test-subj={getTestId('description')}
>
<TextValueDisplay truncate={!expanded}>
{artifact.description || getEmptyValue()}
</TextValueDisplay>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EffectScope policies={policyNavLinks} data-test-subj={getTestId('effectScope')} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<CardActionsFlexItem actions={actions} data-test-subj={getTestId('actions')} />
</EuiFlexGroup>
}
name={
<TextValueDisplay bold truncate={!expanded}>
{artifact.name}
</TextValueDisplay>
}
description={
<TextValueDisplay truncate={!expanded}>
{artifact.description || getEmptyValue()}
</TextValueDisplay>
}
effectScope={
<EffectScope policies={policyNavLinks} data-test-subj={getTestId('effectScope')} />
}
actionMenu={<CardActionsFlexItem actions={actions} data-test-subj={getTestId('actions')} />}
/>
);
}
);
@ -106,8 +99,11 @@ export interface CardCompressedHeaderLayoutProps extends Pick<CommonProps, 'data
name: ReactNode;
description: ReactNode;
effectScope: ReactNode;
/** If no menu is shown, but you want the space for it be preserved, set prop to `false` */
actionMenu?: ReactNode | false;
/**
* The EuiFlexItem react node that contains the actions for the carc. If wanting to NOT include a menu,
* but still want the placeholder for it be preserved (ex. for the Grid headers), set prop to `true`
*/
actionMenu?: ReactNode | true;
/**
* When set to `true`, all padding and margin values will be set to zero for the top of the header
* layout, so that all content is flushed to the top
@ -137,7 +133,11 @@ export const CardCompressedHeaderLayout = memo<CardCompressedHeaderLayoutProps>(
data-test-subj={dataTestSubj}
className={flushTopCssClassname}
>
<EuiFlexItem grow={false} className={flushTopCssClassname}>
<EuiFlexItem
grow={false}
className={flushTopCssClassname}
data-test-subj={getTestId('expandCollapseHolder')}
>
{expandToggle}
</EuiFlexItem>
<EuiFlexItem className={cssClassNames + flushTopCssClassname}>
@ -145,27 +145,27 @@ export const CardCompressedHeaderLayout = memo<CardCompressedHeaderLayoutProps>(
<EuiFlexItem
grow={2}
className={cssClassNames + flushTopCssClassname}
data-test-subj={getTestId('title')}
data-test-subj={getTestId('titleHolder')}
>
{name}
</EuiFlexItem>
<EuiFlexItem
grow={3}
className={cssClassNames + flushTopCssClassname}
data-test-subj={getTestId('description')}
data-test-subj={getTestId('descriptionHolder')}
>
{description}
</EuiFlexItem>
<EuiFlexItem
grow={1}
data-test-subj={getTestId('effectScope')}
data-test-subj={getTestId('effectScopeHolder')}
className={flushTopCssClassname}
>
{effectScope}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{actionMenu === false ? (
{actionMenu === true ? (
<EuiFlexItem
grow={false}
data-test-subj={getTestId('cardActionsPlaceholder')}

View file

@ -6,10 +6,12 @@
*/
import { cloneDeep } from 'lodash';
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { TrustedAppGenerator } from '../../../../common/endpoint/data_generators/trusted_app_generator';
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { TrustedApp } from '../../../../common/endpoint/types';
export const getCommonItemDataOverrides = () => {
const getCommonItemDataOverrides = () => {
return {
name: 'some internal app',
description: 'this app is trusted by the company',
@ -17,18 +19,26 @@ export const getCommonItemDataOverrides = () => {
};
};
export const getTrustedAppProvider = () =>
export const getTrustedAppProviderMock = (): TrustedApp =>
new TrustedAppGenerator('seed').generate(getCommonItemDataOverrides());
export const getExceptionProvider = () => {
export const getExceptionProviderMock = (): ExceptionListItemSchema => {
// Grab the properties from the generated Trusted App that should be the same across both types
// eslint-disable-next-line @typescript-eslint/naming-convention
const { name, description, created_at, updated_at, updated_by, created_by, id } =
getTrustedAppProviderMock();
// cloneDeep needed because exception mock generator uses state across instances
return cloneDeep(
getExceptionListItemSchemaMock({
...getCommonItemDataOverrides(),
name,
description,
created_at,
updated_at,
updated_by,
created_by,
id,
os_types: ['windows'],
updated_at: new Date().toISOString(),
created_by: 'Justa',
updated_by: 'Mara',
entries: [
{
field: 'process.hash.*',

View file

@ -9,6 +9,7 @@ import React, { memo } from 'react';
import { EuiContextMenuItem, EuiContextMenuItemProps } from '@elastic/eui';
import { NavigateToAppOptions } from 'kibana/public';
import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
export interface ContextMenuItemNavByRouterProps extends EuiContextMenuItemProps {
/** The Kibana (plugin) app id */
@ -34,6 +35,7 @@ export const ContextMenuItemNavByRouter = memo<ContextMenuItemNavByRouterProps>(
...navigateOptions,
onClick,
});
const getTestId = useTestIdGenerator(otherMenuItemProps['data-test-subj']);
return (
<EuiContextMenuItem
@ -43,6 +45,7 @@ export const ContextMenuItemNavByRouter = memo<ContextMenuItemNavByRouterProps>(
{textTruncate ? (
<div
className="eui-textTruncate"
data-test-subj={getTestId('truncateWrapper')}
{
/* Add the html `title` prop if children is a string */
...('string' === typeof children ? { title: children } : {})

View file

@ -0,0 +1,133 @@
/*
* 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 React from 'react';
import { EuiButtonEmpty } from '@elastic/eui';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint';
import {
ContextMenuWithRouterSupport,
ContextMenuWithRouterSupportProps,
} from './context_menu_with_router_support';
import { act, fireEvent, waitForElementToBeRemoved } from '@testing-library/react';
import { APP_ID } from '../../../../common/constants';
describe('When using the ContextMenuWithRouterSupport component', () => {
let appTestContext: AppContextTestRender;
let renderResult: ReturnType<AppContextTestRender['render']>;
let render: (
props?: Partial<ContextMenuWithRouterSupportProps>
) => ReturnType<AppContextTestRender['render']>;
let items: ContextMenuWithRouterSupportProps['items'];
const clickMenuTriggerButton = () => {
act(() => {
fireEvent.click(renderResult.getByTestId('testMenu-triggerButton'));
});
};
const getContextMenuPanel = () => renderResult.queryByTestId('testMenu-popoverPanel');
beforeEach(() => {
appTestContext = createAppRootMockRenderer();
items = [
{
children: 'click me 1',
'data-test-subj': 'menu-item-one',
textTruncate: false,
},
{
children: 'click me 2',
navigateAppId: APP_ID,
navigateOptions: {
path: '/one/two/three',
},
href: 'http://some-url.elastic/one/two/three',
},
{
children: 'click me 3 with some very long text here that needs to be truncated',
textTruncate: true,
},
];
render = (overrideProps = {}) => {
const props: ContextMenuWithRouterSupportProps = {
items,
'data-test-subj': 'testMenu',
button: <EuiButtonEmpty data-test-subj="testMenu-triggerButton">{'Menu'}</EuiButtonEmpty>,
...overrideProps,
};
renderResult = appTestContext.render(<ContextMenuWithRouterSupport {...props} />);
return renderResult;
};
});
it('should toggle the context menu when button is clicked', () => {
render();
expect(getContextMenuPanel()).toBeNull();
clickMenuTriggerButton();
expect(getContextMenuPanel()).not.toBeNull();
});
it('should auto include test subjects on items if one is not defined by the menu item props', () => {
render();
clickMenuTriggerButton();
// this test id should be unchanged from what the Props for the item
expect(renderResult.getByTestId('menu-item-one')).not.toBeNull();
// these should have been auto-inserted
expect(renderResult.getByTestId('testMenu-item-1')).not.toBeNull();
expect(renderResult.getByTestId('testMenu-item-2')).not.toBeNull();
});
it('should truncate text of menu item when `textTruncate` prop is `true`', () => {
render({ maxWidth: undefined });
clickMenuTriggerButton();
expect(renderResult.getByTestId('testMenu-item-2-truncateWrapper')).not.toBeNull();
});
it('should close menu when a menu item is clicked and call menu item onclick callback', async () => {
render();
clickMenuTriggerButton();
await act(async () => {
const menuPanelRemoval = waitForElementToBeRemoved(getContextMenuPanel());
fireEvent.click(renderResult.getByTestId('menu-item-one'));
await menuPanelRemoval;
});
expect(getContextMenuPanel()).toBeNull();
});
it('should truncate menu and menu item content when `maxWidth` is used', () => {
render();
clickMenuTriggerButton();
expect(renderResult.getByTestId('menu-item-one-truncateWrapper')).not.toBeNull();
expect(renderResult.getByTestId('testMenu-item-1-truncateWrapper')).not.toBeNull();
expect(renderResult.getByTestId('testMenu-item-2-truncateWrapper')).not.toBeNull();
});
it('should navigate using the router when item is clicked', () => {
render();
clickMenuTriggerButton();
act(() => {
fireEvent.click(renderResult.getByTestId('testMenu-item-1'));
});
expect(appTestContext.coreStart.application.navigateToApp).toHaveBeenCalledWith(
APP_ID,
expect.objectContaining({ path: '/one/two/three' })
);
});
});

View file

@ -13,6 +13,7 @@ import {
EuiPopover,
EuiPopoverProps,
} from '@elastic/eui';
import uuid from 'uuid';
import {
ContextMenuItemNavByRouter,
ContextMenuItemNavByRouterProps,
@ -49,10 +50,12 @@ export const ContextMenuWithRouterSupport = memo<ContextMenuWithRouterSupportPro
}, [getTestId]);
const menuItems: EuiContextMenuPanelProps['items'] = useMemo(() => {
return items.map((itemProps) => {
return items.map((itemProps, index) => {
return (
<ContextMenuItemNavByRouter
{...itemProps}
key={uuid.v4()}
data-test-subj={itemProps['data-test-subj'] ?? getTestId(`item-${index}`)}
textTruncate={Boolean(maxWidth) || itemProps.textTruncate}
onClick={(ev) => {
handleCloseMenu();
@ -63,7 +66,7 @@ export const ContextMenuWithRouterSupport = memo<ContextMenuWithRouterSupportPro
/>
);
});
}, [handleCloseMenu, items, maxWidth]);
}, [getTestId, handleCloseMenu, items, maxWidth]);
type AdditionalPanelProps = Partial<EuiContextMenuPanelProps & HTMLAttributes<HTMLDivElement>>;
const additionalContextMenuPanelProps = useMemo<AdditionalPanelProps>(() => {
@ -86,7 +89,11 @@ export const ContextMenuWithRouterSupport = memo<ContextMenuWithRouterSupportPro
panelProps={panelProps}
button={
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<div className="eui-displayInlineBlock" onClick={handleToggleMenu}>
<div
className="eui-displayInlineBlock"
data-test-subj={getTestId('triggerButtonWrapper')}
onClick={handleToggleMenu}
>
{button}
</div>
}

View file

@ -6,3 +6,4 @@
*/
export * from './context_menu_with_router_support';
export * from './context_menu_item_nav_by_router';

View file

@ -24,3 +24,5 @@ export const EndpointsContainer = memo(() => {
});
EndpointsContainer.displayName = 'EndpointsContainer';
export { endpointListFleetApisHttpMock } from './mocks';
export { EndpointListFleetApisHttpMockInterface } from './mocks';

View file

@ -26,19 +26,21 @@ import {
HOST_METADATA_LIST_ROUTE,
} from '../../../../common/endpoint/constants';
import {
AGENT_POLICY_API_ROUTES,
appRoutesService,
CheckPermissionsResponse,
EPM_API_ROUTES,
GetAgentPoliciesResponse,
GetPackagesResponse,
} from '../../../../../fleet/common';
import {
PendingActionsHttpMockInterface,
pendingActionsHttpMock,
PendingActionsHttpMockInterface,
} from '../../../common/lib/endpoint_pending_actions/mocks';
import { METADATA_TRANSFORM_STATS_URL, TRANSFORM_STATES } from '../../../../common/constants';
import { TransformStatsResponse } from './types';
import {
fleetGetAgentPolicyListHttpMock,
FleetGetAgentPolicyListHttpMockInterface,
FleetGetAgentStatusHttpMockInterface,
fleetGetCheckPermissionsHttpMock,
FleetGetCheckPermissionsInterface,
FleetGetEndpointPackagePolicyHttpMockInterface,
fleetGetPackageListHttpMock,
FleetGetPackageListHttpMockInterface,
} from '../mocks';
type EndpointMetadataHttpMocksInterface = ResponseProvidersInterface<{
metadataList: () => HostResultList;
@ -149,88 +151,6 @@ export const endpointActivityLogHttpMock =
},
]);
export type FleetGetPackageListHttpMockInterface = ResponseProvidersInterface<{
packageList: () => GetPackagesResponse;
}>;
export const fleetGetPackageListHttpMock =
httpHandlerMockFactory<FleetGetPackageListHttpMockInterface>([
{
id: 'packageList',
method: 'get',
path: EPM_API_ROUTES.LIST_PATTERN,
handler() {
const generator = new EndpointDocGenerator('seed');
return {
response: [generator.generateEpmPackage()],
};
},
},
]);
export type FleetGetAgentPolicyListHttpMockInterface = ResponseProvidersInterface<{
agentPolicy: () => GetAgentPoliciesResponse;
}>;
export const fleetGetAgentPolicyListHttpMock =
httpHandlerMockFactory<FleetGetAgentPolicyListHttpMockInterface>([
{
id: 'agentPolicy',
path: AGENT_POLICY_API_ROUTES.LIST_PATTERN,
method: 'get',
handler: () => {
const generator = new EndpointDocGenerator('seed');
const endpointMetadata = generator.generateHostMetadata();
const agentPolicy = generator.generateAgentPolicy();
// Make sure that the Agent policy returned from the API has the Integration Policy ID that
// the endpoint metadata is using. This is needed especially when testing the Endpoint Details
// flyout where certain actions might be disabled if we know the endpoint integration policy no
// longer exists.
(agentPolicy.package_policies as string[]).push(
endpointMetadata.Endpoint.policy.applied.id
);
return {
items: [agentPolicy],
perPage: 10,
total: 1,
page: 1,
};
},
},
]);
export type FleetGetCheckPermissionsInterface = ResponseProvidersInterface<{
checkPermissions: () => CheckPermissionsResponse;
}>;
export const fleetGetCheckPermissionsHttpMock =
httpHandlerMockFactory<FleetGetCheckPermissionsInterface>([
{
id: 'checkPermissions',
path: appRoutesService.getCheckPermissionsPath(),
method: 'get',
handler: () => {
return {
error: undefined,
success: true,
};
},
},
]);
type FleetApisHttpMockInterface = FleetGetPackageListHttpMockInterface &
FleetGetAgentPolicyListHttpMockInterface &
FleetGetCheckPermissionsInterface;
/**
* Mocks all Fleet apis needed to render the Endpoint List/Details pages
*/
export const fleetApisHttpMock = composeHttpHandlerMocks<FleetApisHttpMockInterface>([
fleetGetPackageListHttpMock,
fleetGetAgentPolicyListHttpMock,
fleetGetCheckPermissionsHttpMock,
]);
type TransformHttpMocksInterface = ResponseProvidersInterface<{
metadataTransformStats: () => TransformStatsResponse;
}>;
@ -251,10 +171,24 @@ export const transformsHttpMocks = httpHandlerMockFactory<TransformHttpMocksInte
},
]);
export type EndpointListFleetApisHttpMockInterface = FleetGetPackageListHttpMockInterface &
FleetGetAgentPolicyListHttpMockInterface &
FleetGetCheckPermissionsInterface &
FleetGetAgentStatusHttpMockInterface &
FleetGetEndpointPackagePolicyHttpMockInterface;
/**
* Mocks all Fleet apis
*/
export const endpointListFleetApisHttpMock =
composeHttpHandlerMocks<EndpointListFleetApisHttpMockInterface>([
fleetGetPackageListHttpMock,
fleetGetAgentPolicyListHttpMock,
fleetGetCheckPermissionsHttpMock,
]);
type EndpointPageHttpMockInterface = EndpointMetadataHttpMocksInterface &
EndpointPolicyResponseHttpMockInterface &
EndpointActivityLogHttpMockInterface &
FleetApisHttpMockInterface &
EndpointListFleetApisHttpMockInterface &
PendingActionsHttpMockInterface &
TransformHttpMocksInterface;
/**
@ -264,7 +198,7 @@ export const endpointPageHttpMock = composeHttpHandlerMocks<EndpointPageHttpMock
endpointMetadataHttpMocks,
endpointPolicyResponseHttpMock,
endpointActivityLogHttpMock,
fleetApisHttpMock,
endpointListFleetApisHttpMock,
pendingActionsHttpMock,
transformsHttpMocks,
]);

View file

@ -0,0 +1,164 @@
/*
* 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 {
httpHandlerMockFactory,
ResponseProvidersInterface,
} from '../../../common/mock/endpoint/http_handler_mock_factory';
import {
AGENT_API_ROUTES,
AGENT_POLICY_API_ROUTES,
appRoutesService,
CheckPermissionsResponse,
EPM_API_ROUTES,
GetAgentPoliciesResponse,
GetAgentStatusResponse,
GetPackagesResponse,
PACKAGE_POLICY_API_ROUTES,
} from '../../../../../fleet/common';
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
import { GetPolicyListResponse, GetPolicyResponse } from '../policy/types';
export type FleetGetPackageListHttpMockInterface = ResponseProvidersInterface<{
packageList: () => GetPackagesResponse;
}>;
export const fleetGetPackageListHttpMock =
httpHandlerMockFactory<FleetGetPackageListHttpMockInterface>([
{
id: 'packageList',
method: 'get',
path: EPM_API_ROUTES.LIST_PATTERN,
handler() {
const generator = new EndpointDocGenerator('seed');
return {
response: [generator.generateEpmPackage()],
};
},
},
]);
export type FleetGetEndpointPackagePolicyHttpMockInterface = ResponseProvidersInterface<{
endpointPackagePolicy: () => GetPolicyResponse;
}>;
export const fleetGetEndpointPackagePolicyHttpMock =
httpHandlerMockFactory<FleetGetEndpointPackagePolicyHttpMockInterface>([
{
id: 'endpointPackagePolicy',
path: PACKAGE_POLICY_API_ROUTES.INFO_PATTERN,
method: 'get',
handler: () => {
return {
items: new EndpointDocGenerator('seed').generatePolicyPackagePolicy(),
};
},
},
]);
export type FleetGetEndpointPackagePolicyListHttpMockInterface = ResponseProvidersInterface<{
endpointPackagePolicyList: () => GetPolicyListResponse;
}>;
export const fleetGetEndpointPackagePolicyListHttpMock =
httpHandlerMockFactory<FleetGetEndpointPackagePolicyListHttpMockInterface>([
{
id: 'endpointPackagePolicyList',
path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN,
method: 'get',
handler: () => {
const generator = new EndpointDocGenerator('seed');
const items = Array.from({ length: 5 }, (_, index) => {
const policy = generator.generatePolicyPackagePolicy();
policy.name += ` ${index}`;
return policy;
});
return {
items,
total: 1,
page: 1,
perPage: 10,
};
},
},
]);
export type FleetGetAgentPolicyListHttpMockInterface = ResponseProvidersInterface<{
agentPolicy: () => GetAgentPoliciesResponse;
}>;
export const fleetGetAgentPolicyListHttpMock =
httpHandlerMockFactory<FleetGetAgentPolicyListHttpMockInterface>([
{
id: 'agentPolicy',
path: AGENT_POLICY_API_ROUTES.LIST_PATTERN,
method: 'get',
handler: () => {
const generator = new EndpointDocGenerator('seed');
const endpointMetadata = generator.generateHostMetadata();
const agentPolicy = generator.generateAgentPolicy();
// Make sure that the Agent policy returned from the API has the Integration Policy ID that
// the endpoint metadata is using. This is needed especially when testing the Endpoint Details
// flyout where certain actions might be disabled if we know the endpoint integration policy no
// longer exists.
(agentPolicy.package_policies as string[]).push(
endpointMetadata.Endpoint.policy.applied.id
);
return {
items: [agentPolicy],
perPage: 10,
total: 1,
page: 1,
};
},
},
]);
export type FleetGetCheckPermissionsInterface = ResponseProvidersInterface<{
checkPermissions: () => CheckPermissionsResponse;
}>;
export const fleetGetCheckPermissionsHttpMock =
httpHandlerMockFactory<FleetGetCheckPermissionsInterface>([
{
id: 'checkPermissions',
path: appRoutesService.getCheckPermissionsPath(),
method: 'get',
handler: () => {
return {
error: undefined,
success: true,
};
},
},
]);
export type FleetGetAgentStatusHttpMockInterface = ResponseProvidersInterface<{
agentStatus: () => GetAgentStatusResponse;
}>;
export const fleetGetAgentStatusHttpMock =
httpHandlerMockFactory<FleetGetAgentStatusHttpMockInterface>([
{
id: 'agentStatus',
path: AGENT_API_ROUTES.STATUS_PATTERN,
method: 'get',
handler: () => {
return {
results: {
total: 50,
inactive: 5,
online: 40,
error: 0,
offline: 5,
updating: 0,
other: 0,
events: 0,
},
};
},
},
]);

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './fleet_mocks';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { PolicyDetailsState } from '../../../types';
import { PolicyArtifactsState, PolicyDetailsState } from '../../../types';
import { initialPolicyDetailsState } from '../reducer';
import {
getAssignableArtifactsList,
@ -16,6 +16,12 @@ import {
getAssignableArtifactsListExist,
getAssignableArtifactsListExistIsLoading,
getUpdateArtifacts,
doesPolicyTrustedAppsListNeedUpdate,
isPolicyTrustedAppListLoading,
getPolicyTrustedAppList,
getPolicyTrustedAppsListPagination,
getTrustedAppsListOfAllPolicies,
getTrustedAppsAllPoliciesById,
} from './trusted_apps_selectors';
import { getCurrentArtifactsLocation, isOnPolicyTrustedAppsView } from './policy_common_selectors';
@ -27,15 +33,190 @@ import {
createFailedResourceState,
} from '../../../../../state';
import { MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH } from '../../../../../common/constants';
import { getMockListResponse, getAPIError, getMockCreateResponse } from '../../../test_utils';
import {
getMockListResponse,
getAPIError,
getMockCreateResponse,
getMockPolicyDetailsArtifactListUrlParams,
getMockPolicyDetailsArtifactsPageLocationUrlParams,
} from '../../../test_utils';
import { getGeneratedPolicyResponse } from '../../../../trusted_apps/store/mocks';
describe('policy trusted apps selectors', () => {
let initialState: ImmutableObject<PolicyDetailsState>;
const createArtifactsState = (
artifacts: Partial<PolicyArtifactsState> = {}
): ImmutableObject<PolicyDetailsState> => {
return {
...initialState,
artifacts: {
...initialState.artifacts,
...artifacts,
},
};
};
beforeEach(() => {
initialState = initialPolicyDetailsState();
});
describe('doesPolicyTrustedAppsListNeedUpdate()', () => {
it('should return true if state is not loaded', () => {
expect(doesPolicyTrustedAppsListNeedUpdate(initialState)).toBe(true);
});
it('should return true if it is loaded, but URL params were changed', () => {
expect(
doesPolicyTrustedAppsListNeedUpdate(
createArtifactsState({
location: getMockPolicyDetailsArtifactsPageLocationUrlParams({ page_index: 4 }),
assignedList: createLoadedResourceState({
location: getMockPolicyDetailsArtifactListUrlParams(),
artifacts: getMockListResponse(),
}),
})
)
).toBe(true);
});
it('should return false if state is loaded adn URL params are the same', () => {
expect(
doesPolicyTrustedAppsListNeedUpdate(
createArtifactsState({
location: getMockPolicyDetailsArtifactsPageLocationUrlParams(),
assignedList: createLoadedResourceState({
location: getMockPolicyDetailsArtifactListUrlParams(),
artifacts: getMockListResponse(),
}),
})
)
).toBe(false);
});
});
describe('isPolicyTrustedAppListLoading()', () => {
it('should return true when loading data', () => {
expect(
isPolicyTrustedAppListLoading(
createArtifactsState({
assignedList: createLoadingResourceState(createUninitialisedResourceState()),
})
)
).toBe(true);
});
it.each([
['uninitialized', createUninitialisedResourceState() as PolicyArtifactsState['assignedList']],
['loaded', createLoadedResourceState({}) as PolicyArtifactsState['assignedList']],
['failed', createFailedResourceState({}) as PolicyArtifactsState['assignedList']],
])('should return false when state is %s', (__, assignedListState) => {
expect(
isPolicyTrustedAppListLoading(createArtifactsState({ assignedList: assignedListState }))
).toBe(false);
});
});
describe('getPolicyTrustedAppList()', () => {
it('should return the list of trusted apps', () => {
const listResponse = getMockListResponse();
expect(
getPolicyTrustedAppList(
createArtifactsState({
location: getMockPolicyDetailsArtifactsPageLocationUrlParams(),
assignedList: createLoadedResourceState({
location: getMockPolicyDetailsArtifactListUrlParams(),
artifacts: listResponse,
}),
})
)
).toEqual(listResponse.data);
});
it('should return empty array if no data is loaded', () => {
expect(getPolicyTrustedAppList(initialState)).toEqual([]);
});
});
describe('getPolicyTrustedAppsListPagination()', () => {
it('should return default pagination data even if no api data is available', () => {
expect(getPolicyTrustedAppsListPagination(initialState)).toEqual({
pageIndex: 0,
pageSize: 10,
pageSizeOptions: [10, 20, 50],
totalItemCount: 0,
});
});
it('should return pagination data based on api response data', () => {
const listResponse = getMockListResponse();
listResponse.page = 6;
listResponse.per_page = 100;
listResponse.total = 1000;
expect(
getPolicyTrustedAppsListPagination(
createArtifactsState({
location: getMockPolicyDetailsArtifactsPageLocationUrlParams({
page_index: 5,
page_size: 100,
}),
assignedList: createLoadedResourceState({
location: getMockPolicyDetailsArtifactListUrlParams({
page_index: 5,
page_size: 100,
}),
artifacts: listResponse,
}),
})
)
).toEqual({
pageIndex: 5,
pageSize: 100,
pageSizeOptions: [10, 20, 50],
totalItemCount: 1000,
});
});
});
describe('getTrustedAppsListOfAllPolicies()', () => {
it('should return the loaded list of policies', () => {
const policiesApiResponse = getGeneratedPolicyResponse();
expect(
getTrustedAppsListOfAllPolicies(
createArtifactsState({
policies: createLoadedResourceState(policiesApiResponse),
})
)
).toEqual(policiesApiResponse.items);
});
it('should return an empty array of no policy data was loaded yet', () => {
expect(getTrustedAppsListOfAllPolicies(initialState)).toEqual([]);
});
});
describe('getTrustedAppsAllPoliciesById()', () => {
it('should return an empty object if no polices', () => {
expect(getTrustedAppsAllPoliciesById(initialState)).toEqual({});
});
it('should return an object with policy id and policy data', () => {
const policiesApiResponse = getGeneratedPolicyResponse();
expect(
getTrustedAppsAllPoliciesById(
createArtifactsState({
policies: createLoadedResourceState(policiesApiResponse),
})
)
).toEqual({ [policiesApiResponse.items[0].id]: policiesApiResponse.items[0] });
});
});
describe('isOnPolicyTrustedAppsPage()', () => {
it('when location is on policy trusted apps page', () => {
const isOnPage = isOnPolicyTrustedAppsView({

View file

@ -31,6 +31,7 @@ import {
LoadedResourceState,
} from '../../../../../state';
import { getCurrentArtifactsLocation } from './policy_common_selectors';
import { ServerApiError } from '../../../../../../common/types';
export const doesPolicyHaveTrustedApps = (
state: PolicyDetailsState
@ -212,3 +213,11 @@ export const getDoesAnyTrustedAppExistsIsLoading: PolicyDetailsSelector<boolean>
return isLoadingResourceState(doesAnyTrustedAppExists);
}
);
export const getPolicyTrustedAppListError: PolicyDetailsSelector<
Immutable<ServerApiError> | undefined
> = createSelector(getCurrentPolicyAssignedTrustedAppsState, (currentAssignedTrustedAppsState) => {
if (isFailedResourceState(currentAssignedTrustedAppsState)) {
return currentAssignedTrustedAppsState.error;
}
});

View file

@ -5,25 +5,4 @@
* 2.0.
*/
import {
GetTrustedAppsListResponse,
PostTrustedAppCreateResponse,
} from '../../../../../common/endpoint/types';
import { createSampleTrustedApps, createSampleTrustedApp } from '../../trusted_apps/test_utils';
export const getMockListResponse: () => GetTrustedAppsListResponse = () => ({
data: createSampleTrustedApps({}),
per_page: 100,
page: 1,
total: 100,
});
export const getMockCreateResponse: () => PostTrustedAppCreateResponse = () =>
createSampleTrustedApp(1) as unknown as unknown as PostTrustedAppCreateResponse;
export const getAPIError = () => ({
statusCode: 500,
error: 'Internal Server Error',
message: 'Something is not right',
});
export * from './mocks';

View file

@ -0,0 +1,123 @@
/*
* 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 {
composeHttpHandlerMocks,
httpHandlerMockFactory,
ResponseProvidersInterface,
} from '../../../../common/mock/endpoint/http_handler_mock_factory';
import {
GetTrustedAppsListRequest,
GetTrustedAppsListResponse,
PostTrustedAppCreateResponse,
} from '../../../../../common/endpoint/types';
import { TRUSTED_APPS_LIST_API } from '../../../../../common/endpoint/constants';
import { TrustedAppGenerator } from '../../../../../common/endpoint/data_generators/trusted_app_generator';
import { createSampleTrustedApps, createSampleTrustedApp } from '../../trusted_apps/test_utils';
import {
PolicyDetailsArtifactsPageListLocationParams,
PolicyDetailsArtifactsPageLocation,
} from '../types';
import {
fleetGetAgentStatusHttpMock,
FleetGetAgentStatusHttpMockInterface,
fleetGetEndpointPackagePolicyHttpMock,
FleetGetEndpointPackagePolicyHttpMockInterface,
fleetGetEndpointPackagePolicyListHttpMock,
FleetGetEndpointPackagePolicyListHttpMockInterface,
} from '../../mocks';
export const getMockListResponse: () => GetTrustedAppsListResponse = () => ({
data: createSampleTrustedApps({}),
per_page: 100,
page: 1,
total: 100,
});
export const getMockPolicyDetailsArtifactsPageLocationUrlParams = (
overrides: Partial<PolicyDetailsArtifactsPageLocation> = {}
): PolicyDetailsArtifactsPageLocation => {
return {
page_index: 0,
page_size: 10,
filter: '',
show: undefined,
...overrides,
};
};
export const getMockPolicyDetailsArtifactListUrlParams = (
overrides: Partial<PolicyDetailsArtifactsPageListLocationParams> = {}
): PolicyDetailsArtifactsPageListLocationParams => {
return {
page_index: 0,
page_size: 10,
filter: '',
...overrides,
};
};
export const getMockCreateResponse: () => PostTrustedAppCreateResponse = () =>
createSampleTrustedApp(1) as unknown as unknown as PostTrustedAppCreateResponse;
export const getAPIError = () => ({
statusCode: 500,
error: 'Internal Server Error',
message: 'Something is not right',
});
type PolicyDetailsTrustedAppsHttpMocksInterface = ResponseProvidersInterface<{
policyTrustedAppsList: () => GetTrustedAppsListResponse;
}>;
/**
* HTTP mocks that support the Trusted Apps tab of the Policy Details page
*/
export const policyDetailsTrustedAppsHttpMocks =
httpHandlerMockFactory<PolicyDetailsTrustedAppsHttpMocksInterface>([
{
id: 'policyTrustedAppsList',
path: TRUSTED_APPS_LIST_API,
method: 'get',
handler: ({ query }): GetTrustedAppsListResponse => {
const apiQueryParams = query as GetTrustedAppsListRequest;
const generator = new TrustedAppGenerator('seed');
const perPage = apiQueryParams.per_page ?? 10;
const data = Array.from({ length: Math.min(perPage, 50) }, () => generator.generate());
// Change the 3rd entry (index 2) to be policy specific
data[2].effectScope = {
type: 'policy',
policies: [
// IDs below are those generated by the `fleetGetEndpointPackagePolicyListHttpMock()` mock
'ddf6570b-9175-4a6d-b288-61a09771c647',
'b8e616ae-44fc-4be7-846c-ce8fa5c082dd',
],
};
return {
page: apiQueryParams.page ?? 1,
per_page: perPage,
total: 20,
data,
};
},
},
]);
export type PolicyDetailsPageAllApiHttpMocksInterface =
FleetGetEndpointPackagePolicyHttpMockInterface &
FleetGetAgentStatusHttpMockInterface &
FleetGetEndpointPackagePolicyListHttpMockInterface &
PolicyDetailsTrustedAppsHttpMocksInterface;
export const policyDetailsPageAllApiHttpMocks =
composeHttpHandlerMocks<PolicyDetailsPageAllApiHttpMocksInterface>([
fleetGetEndpointPackagePolicyHttpMock,
fleetGetAgentStatusHttpMock,
fleetGetEndpointPackagePolicyListHttpMock,
policyDetailsTrustedAppsHttpMocks,
]);

View file

@ -0,0 +1,253 @@
/*
* 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 {
AppContextTestRender,
createAppRootMockRenderer,
} from '../../../../../../common/mock/endpoint';
import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing';
import { PolicyTrustedAppsList } from './policy_trusted_apps_list';
import React from 'react';
import { policyDetailsPageAllApiHttpMocks } from '../../../test_utils';
import { isFailedResourceState, isLoadedResourceState } from '../../../../../state';
import { fireEvent, within, act, waitFor } from '@testing-library/react';
import { APP_ID } from '../../../../../../../common/constants';
describe('when rendering the PolicyTrustedAppsList', () => {
let appTestContext: AppContextTestRender;
let renderResult: ReturnType<AppContextTestRender['render']>;
let render: (waitForLoadedState?: boolean) => Promise<ReturnType<AppContextTestRender['render']>>;
let mockedApis: ReturnType<typeof policyDetailsPageAllApiHttpMocks>;
let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction'];
const getCardByIndexPosition = (cardIndex: number = 0) => {
const card = renderResult.getAllByTestId('policyTrustedAppsGrid-card')[cardIndex];
if (!card) {
throw new Error(`Card at index [${cardIndex}] not found`);
}
return card;
};
const toggleCardExpandCollapse = (cardIndex: number = 0) => {
act(() => {
fireEvent.click(
within(getCardByIndexPosition(cardIndex)).getByTestId(
'policyTrustedAppsGrid-card-header-expandCollapse'
)
);
});
};
const toggleCardActionMenu = async (cardIndex: number = 0) => {
act(() => {
fireEvent.click(
within(getCardByIndexPosition(cardIndex)).getByTestId(
'policyTrustedAppsGrid-card-header-actions-button'
)
);
});
await waitFor(() =>
expect(renderResult.getByTestId('policyTrustedAppsGrid-card-header-actions-contextMenuPanel'))
);
};
beforeEach(() => {
appTestContext = createAppRootMockRenderer();
mockedApis = policyDetailsPageAllApiHttpMocks(appTestContext.coreStart.http);
appTestContext.setExperimentalFlag({ trustedAppsByPolicyEnabled: true });
waitForAction = appTestContext.middlewareSpy.waitForAction;
render = async (waitForLoadedState: boolean = true) => {
appTestContext.history.push(
getPolicyDetailsArtifactsListPath('ddf6570b-9175-4a6d-b288-61a09771c647')
);
const trustedAppDataReceived = waitForLoadedState
? waitForAction('assignedTrustedAppsListStateChanged', {
validate({ payload }) {
return isLoadedResourceState(payload);
},
})
: Promise.resolve();
renderResult = appTestContext.render(<PolicyTrustedAppsList />);
await trustedAppDataReceived;
return renderResult;
};
});
// FIXME: implement this test once PR #113802 is merged
it.todo('should show loading spinner if checking to see if trusted apps exist');
it('should show total number of of items being displayed', async () => {
await render();
expect(renderResult.getByTestId('policyDetailsTrustedAppsCount').textContent).toBe(
'Showing 20 trusted applications'
);
});
it('should show card grid', async () => {
await render();
expect(renderResult.getByTestId('policyTrustedAppsGrid')).toBeTruthy();
await expect(renderResult.findAllByTestId('policyTrustedAppsGrid-card')).resolves.toHaveLength(
10
);
});
it('should expand cards', async () => {
await render();
// expand
toggleCardExpandCollapse();
toggleCardExpandCollapse(4);
await waitFor(() =>
expect(
renderResult.queryAllByTestId('policyTrustedAppsGrid-card-criteriaConditions')
).toHaveLength(2)
);
});
it('should collapse cards', async () => {
await render();
// expand
toggleCardExpandCollapse();
toggleCardExpandCollapse(4);
await waitFor(() =>
expect(
renderResult.queryAllByTestId('policyTrustedAppsGrid-card-criteriaConditions')
).toHaveLength(2)
);
// collapse
toggleCardExpandCollapse();
toggleCardExpandCollapse(4);
await waitFor(() =>
expect(
renderResult.queryAllByTestId('policyTrustedAppsGrid-card-criteriaConditions')
).toHaveLength(0)
);
});
it('should show action menu on card', async () => {
await render();
expect(
renderResult.getAllByTestId('policyTrustedAppsGrid-card-header-actions-button')
).toHaveLength(10);
});
it('should navigate to trusted apps page when view full details action is clicked', async () => {
await render();
await toggleCardActionMenu();
act(() => {
fireEvent.click(renderResult.getByTestId('policyTrustedAppsGrid-viewFullDetailsAction'));
});
expect(appTestContext.coreStart.application.navigateToApp).toHaveBeenCalledWith(
APP_ID,
expect.objectContaining({
path: '/administration/trusted_apps?show=edit&id=89f72d8a-05b5-4350-8cad-0dc3661d6e67',
})
);
});
it('should display policy names on assignment context menu', async () => {
const retrieveAllPolicies = waitForAction('policyDetailsListOfAllPoliciesStateChanged', {
validate({ payload }) {
return isLoadedResourceState(payload);
},
});
await render();
await retrieveAllPolicies;
act(() => {
fireEvent.click(
within(getCardByIndexPosition(2)).getByTestId(
'policyTrustedAppsGrid-card-header-effectScope-popupMenu-button'
)
);
});
await waitFor(() =>
expect(
renderResult.getByTestId(
'policyTrustedAppsGrid-card-header-effectScope-popupMenu-popoverPanel'
)
)
);
expect(
renderResult.getByTestId('policyTrustedAppsGrid-card-header-effectScope-popupMenu-item-0')
.textContent
).toEqual('Endpoint Policy 0');
expect(
renderResult.getByTestId('policyTrustedAppsGrid-card-header-effectScope-popupMenu-item-1')
.textContent
).toEqual('Endpoint Policy 1');
});
it.todo('should navigate to policy details when clicking policy on assignment context menu');
it('should handle pagination changes', async () => {
await render();
expect(appTestContext.history.location.search).not.toBeTruthy();
act(() => {
fireEvent.click(renderResult.getByTestId('pagination-button-next'));
});
expect(appTestContext.history.location.search).toMatch('?page_index=1');
});
it('should reset `pageIndex` when a new pageSize is selected', async () => {
await render();
// page ahead
act(() => {
fireEvent.click(renderResult.getByTestId('pagination-button-next'));
});
await waitFor(() => {
expect(appTestContext.history.location.search).toBeTruthy();
});
// now change the page size
await act(async () => {
fireEvent.click(renderResult.getByTestId('tablePaginationPopoverButton'));
await waitFor(() => expect(renderResult.getByTestId('tablePagination-50-rows')));
});
act(() => {
fireEvent.click(renderResult.getByTestId('tablePagination-50-rows'));
});
expect(appTestContext.history.location.search).toMatch('?page_size=50');
});
it('should show toast message if trusted app list api call fails', async () => {
const error = new Error('oh no');
// @ts-expect-error
mockedApis.responseProvider.policyTrustedAppsList.mockRejectedValue(error);
await render(false);
await act(async () => {
await waitForAction('assignedTrustedAppsListStateChanged', {
validate: ({ payload }) => isFailedResourceState(payload),
});
});
expect(appTestContext.startServices.notifications.toasts.addError).toHaveBeenCalledWith(
error,
expect.objectContaining({
title: expect.any(String),
})
);
});
});

View file

@ -19,6 +19,7 @@ import {
doesPolicyHaveTrustedApps,
getCurrentArtifactsLocation,
getPolicyTrustedAppList,
getPolicyTrustedAppListError,
getPolicyTrustedAppsListPagination,
getTrustedAppsAllPoliciesById,
isPolicyTrustedAppListLoading,
@ -31,12 +32,17 @@ import {
getTrustedAppsListPath,
} from '../../../../../common/routing';
import { Immutable, TrustedApp } from '../../../../../../../common/endpoint/types';
import { useAppUrl } from '../../../../../../common/lib/kibana';
import { useAppUrl, useToasts } from '../../../../../../common/lib/kibana';
import { APP_ID } from '../../../../../../../common/constants';
import { ContextMenuItemNavByRouterProps } from '../../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router';
import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/artifact_entry_card';
import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator';
const DATA_TEST_SUBJ = 'policyTrustedAppsGrid';
export const PolicyTrustedAppsList = memo(() => {
const getTestId = useTestIdGenerator(DATA_TEST_SUBJ);
const toasts = useToasts();
const history = useHistory();
const { getAppUrl } = useAppUrl();
const policyId = usePolicyDetailsSelector(policyIdFromParams);
@ -47,11 +53,10 @@ export const PolicyTrustedAppsList = memo(() => {
const pagination = usePolicyDetailsSelector(getPolicyTrustedAppsListPagination);
const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation);
const allPoliciesById = usePolicyDetailsSelector(getTrustedAppsAllPoliciesById);
const trustedAppsApiError = usePolicyDetailsSelector(getPolicyTrustedAppListError);
const [isCardExpanded, setCardExpanded] = useState<Record<string, boolean>>({});
// TODO:PT show load errors if any
const handlePageChange = useCallback<ArtifactCardGridProps['onPageChange']>(
({ pageIndex, pageSize }) => {
history.push(
@ -135,6 +140,7 @@ export const PolicyTrustedAppsList = memo(() => {
href: getAppUrl({ appId: APP_ID, path: viewUrlPath }),
navigateAppId: APP_ID,
navigateOptions: { path: viewUrlPath },
'data-test-subj': getTestId('viewFullDetailsAction'),
},
],
policies: assignedPoliciesMenuItems,
@ -144,7 +150,7 @@ export const PolicyTrustedAppsList = memo(() => {
}
return newCardProps;
}, [allPoliciesById, getAppUrl, isCardExpanded, trustedAppItems]);
}, [allPoliciesById, getAppUrl, getTestId, isCardExpanded, trustedAppItems]);
const provideCardProps = useCallback<Required<ArtifactCardGridProps>['cardComponentProps']>(
(item) => {
@ -153,6 +159,17 @@ export const PolicyTrustedAppsList = memo(() => {
[cardProps]
);
// if an error occurred while loading the data, show toast
useEffect(() => {
if (trustedAppsApiError) {
toasts.addError(trustedAppsApiError as unknown as Error, {
title: i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.list.apiError', {
defaultMessage: 'Error while retrieving list of trusted applications',
}),
});
}
}, [toasts, trustedAppsApiError]);
// Anytime a new set of data (trusted apps) is retrieved, reset the card expand state
useEffect(() => {
setCardExpanded({});
@ -161,7 +178,11 @@ export const PolicyTrustedAppsList = memo(() => {
if (hasTrustedApps.loading || isTrustedAppExistsCheckLoading) {
return (
<EuiPageTemplate template="centeredContent">
<EuiLoadingSpinner className="essentialAnimation" size="xl" />
<EuiLoadingSpinner
className="essentialAnimation"
size="xl"
data-test-subj={DATA_TEST_SUBJ}
/>
</EuiPageTemplate>
);
}
@ -180,8 +201,9 @@ export const PolicyTrustedAppsList = memo(() => {
onExpandCollapse={handleExpandCollapse}
cardComponentProps={provideCardProps}
loading={isLoading}
error={trustedAppsApiError?.message}
pagination={pagination as Pagination}
data-test-subj="policyTrustedAppsGrid"
data-test-subj={DATA_TEST_SUBJ}
/>
</>
);