[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:
parent
c3f1e0de54
commit
4436ed2f71
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -63,7 +63,7 @@ export const GridHeader = memo<GridHeaderProps>(({ 'data-test-subj': dataTestSub
|
|||
</strong>
|
||||
</EuiText>
|
||||
}
|
||||
actionMenu={false}
|
||||
actionMenu={true}
|
||||
/>
|
||||
</GridHeaderContainer>
|
||||
);
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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')}
|
||||
|
|
|
@ -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.*',
|
||||
|
|
|
@ -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 } : {})
|
||||
|
|
|
@ -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' })
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export * from './context_menu_with_router_support';
|
||||
export * from './context_menu_item_nav_by_router';
|
||||
|
|
|
@ -24,3 +24,5 @@ export const EndpointsContainer = memo(() => {
|
|||
});
|
||||
|
||||
EndpointsContainer.displayName = 'EndpointsContainer';
|
||||
export { endpointListFleetApisHttpMock } from './mocks';
|
||||
export { EndpointListFleetApisHttpMockInterface } from './mocks';
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
]);
|
|
@ -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';
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
]);
|
|
@ -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),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue