Skip to content

Commit

Permalink
[Security Solutions] Fix User flyout loading forever when Managed use…
Browse files Browse the repository at this point in the history
…r flag is enabled (elastic#175764)

## Summary

* Fix the bug that made the user modal load forever
* Add unit tests
* Add cypress test for managed data
* Flaky test runner
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/4975

### How to test it?
* Enable `newUserDetailsFlyoutManagedUser` and `newUserDetailsFlyout`
flags
* Open a kibana instance with alerts
* Go to Alerts page and click on a username
* It should load the new user details flyout


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
  • Loading branch information
machadoum authored Jan 30, 2024
1 parent e7e6afb commit 851df69
Show file tree
Hide file tree
Showing 12 changed files with 1,746 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 { TestProviders } from '../../../common/mock';
import { render } from '@testing-library/react';
import React from 'react';
import { UserDetailsPanel } from '.';
import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header';

describe('LeftPanel', () => {
it('renders', () => {
const { queryByText } = render(
<UserDetailsPanel
path={{
tab: EntityDetailsLeftPanelTab.RISK_INPUTS,
}}
isRiskScoreExist
user={{ name: 'test user', email: [] }}
/>,
{
wrapper: TestProviders,
}
);

const tabElement = queryByText('Risk Inputs');

expect(tabElement).toBeInTheDocument();
});

it('does not render the tab if tab is not found', () => {
const { queryByText } = render(
<UserDetailsPanel
path={{
tab: EntityDetailsLeftPanelTab.RISK_INPUTS,
}}
isRiskScoreExist={false}
user={{ name: 'test user', email: [] }}
/>,
{
wrapper: TestProviders,
}
);

const tabElement = queryByText('Risk Inputs');

expect(tabElement).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export const UserDetailsPanel = ({ isRiskScoreExist, user, path }: UserDetailsPa

if (managedUser.isLoading) return <FlyoutLoading />;

if (!selectedTabId) {
return null;
}

return (
<>
<LeftPanelHeader
Expand All @@ -62,7 +66,7 @@ const useSelectedTab = (
const { openLeftPanel } = useExpandableFlyoutApi();

const selectedTabId = useMemo(() => {
const defaultTab = tabs[0].id;
const defaultTab = tabs.length > 0 ? tabs[0].id : undefined;
if (!path) return defaultTab;

return tabs.find((tab) => tab.id === path.tab)?.id ?? defaultTab;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,28 @@ jest.mock('../../../../../common/hooks/use_space_id', () => ({
useSpaceId: () => 'test-space-id',
}));

const mockUseIsExperimentalFeatureEnabled = jest.fn().mockReturnValue(true);

jest.mock('../../../../../common/hooks/use_experimental_features', () => ({
useIsExperimentalFeatureEnabled: () => mockUseIsExperimentalFeatureEnabled(),
}));

const mockSearch = jest.fn().mockReturnValue({
data: [],
});

const useSearchStrategyDefaultResponse = {
loading: false,
result: { users: [] },
search: (...params: unknown[]) => mockSearch(...params),
refetch: () => {},
inspect: {},
};

const mockUseSearchStrategy = jest.fn().mockReturnValue(useSearchStrategyDefaultResponse);

jest.mock('../../../../../common/containers/use_search_strategy', () => ({
useSearchStrategy: () => ({
loading: false,
result: { users: [] },
search: (...params: unknown[]) => mockSearch(...params),
refetch: () => {},
inspect: {},
}),
useSearchStrategy: () => mockUseSearchStrategy(),
}));

describe('useManagedUser', () => {
Expand Down Expand Up @@ -108,4 +118,28 @@ describe('useManagedUser', () => {
})
);
});

it('should not search if the feature is disabled', () => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
renderHook(() => useManagedUser('test-userName', undefined, false), {
wrapper: TestProviders,
});

expect(mockSearch).not.toHaveBeenCalled();
});

it('should return loading false when the feature is disabled', () => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
mockUseInstalledIntegrations.mockReturnValue({
data: [],
isLoading: true,
});
mockUseSearchStrategy.mockReturnValue({ ...useSearchStrategyDefaultResponse, loading: true });

const { result } = renderHook(() => useManagedUser('test-userName', undefined, false), {
wrapper: TestProviders,
});

expect(result.current.isLoading).toBeFalsy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const useManagedUser = (
email: string[] | undefined,
isLoading?: boolean
): ManagedUserData => {
const skip = useIsExperimentalFeatureEnabled('newUserDetailsFlyoutManagedUser');
const skip = !useIsExperimentalFeatureEnabled('newUserDetailsFlyoutManagedUser');
const { to, from, isInitializing, deleteQuery, setQuery } = useGlobalTime();
const spaceId = useSpaceId();
const {
Expand Down Expand Up @@ -95,9 +95,9 @@ export const useManagedUser = (
return useMemo(
() => ({
data: managedUserData,
isLoading: loadingManagedUser || loadingIntegrations,
isLoading: skip ? false : loadingManagedUser || loadingIntegrations,
isIntegrationEnabled,
}),
[isIntegrationEnabled, loadingIntegrations, loadingManagedUser, managedUserData]
[isIntegrationEnabled, loadingIntegrations, loadingManagedUser, managedUserData, skip]
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const ManagedUserAccordion: React.FC<ManagedUserAccordionProps> = ({

return (
<ExpandablePanel
data-test-subj={`managed-user-accordion-${tableType}`}
header={{
title,
iconType: 'arrowStart',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
* 2.0.
*/

import {
ENTRA_ID_PACKAGE_NAME,
OKTA_PACKAGE_NAME,
} from '@kbn/security-solution-plugin/public/timelines/components/side_panel/new_user_detail/constants';
import {
expandFirstAlertHostFlyout,
expandFirstAlertUserFlyout,
Expand All @@ -14,7 +18,7 @@ import {
import { login } from '../../tasks/login';
import { visitWithTimeRange } from '../../tasks/navigation';
import { ALERTS_URL } from '../../urls/navigation';
import { USER_PANEL_HEADER } from '../../screens/hosts/flyout_user_panel';
import { USER_PANEL_HEADER } from '../../screens/users/flyout_user_panel';
import { waitForAlerts } from '../../tasks/alerts';
import { HOST_PANEL_HEADER } from '../../screens/hosts/flyout_host_panel';
import { RISK_INPUT_PANEL_HEADER, ASSET_CRITICALITY_BADGE } from '../../screens/flyout_risk_panel';
Expand All @@ -28,6 +32,16 @@ import {
ENTITY_DETAILS_FLYOUT_ASSET_CRITICALITY_SELECTOR,
} from '../../screens/asset_criticality/flyouts';
import { deleteCriticality } from '../../tasks/api_calls/entity_analytics';
import { mockFleetInstalledIntegrations } from '../../tasks/fleet_integrations';
import {
expandManagedDataEntraPanel,
expandManagedDataOktaPanel,
} from '../../tasks/users/flyout_user_panel';
import {
ASSET_TYPE_FIELD,
ENTRA_DOCUMENT_TAB,
OKTA_DOCUMENT_TAB,
} from '../../screens/users/flyout_asset_panel';

const USER_NAME = 'user1';
const SIEM_KIBANA_HOST_NAME = 'Host-fwarau82er';
Expand All @@ -43,6 +57,7 @@ describe(
'newUserDetailsFlyout',
'newHostDetailsFlyout',
'entityAnalyticsAssetCriticalityEnabled',
'newUserDetailsFlyoutManagedUser',
])}`,
],
},
Expand All @@ -52,10 +67,12 @@ describe(
before(() => {
cy.task('esArchiverLoad', { archiveName: 'risk_scores_new_complete_data' });
cy.task('esArchiverLoad', { archiveName: 'query_alert', useCreate: true, docsOnly: true });
cy.task('esArchiverLoad', { archiveName: 'user_managed_data' });
});

after(() => {
cy.task('esArchiverUnload', 'risk_scores_new_complete_data');
cy.task('esArchiverUnload', 'user_managed_data');
deleteAlertsAndRules(); // esArchiverUnload doesn't work properly when using with `useCreate` and `docsOnly` flags
deleteCriticality({ idField: 'host.name', idValue: SIEM_KIBANA_HOST_NAME });
deleteCriticality({ idField: 'user.name', idValue: USER_NAME });
Expand All @@ -80,39 +97,73 @@ describe(
cy.get(RISK_INPUT_PANEL_HEADER).should('exist');
});

it('should show asset criticality in the risk input panel', () => {
expandFirstAlertUserFlyout();
expandRiskInputsFlyoutPanel();
cy.get(ASSET_CRITICALITY_BADGE).should('contain.text', 'Very important');
});

it('should display asset criticality accordion', () => {
cy.log('asset criticality');
expandFirstAlertUserFlyout();
cy.get(ENTITY_DETAILS_FLYOUT_ASSET_CRITICALITY_SELECTOR).should(
'contain.text',
'Asset Criticality'
);

cy.get(ENTITY_DETAILS_FLYOUT_ASSET_CRITICALITY_BUTTON).should('have.text', 'Create');
});
it('should display asset criticality modal', () => {
cy.log('asset criticality modal');
expandFirstAlertUserFlyout();
toggleAssetCriticalityModal();
cy.get(ENTITY_DETAILS_FLYOUT_ASSET_CRITICALITY_MODAL_TITLE).should(
'have.text',
'Pick asset criticality level'
);
describe('Asset criticality', () => {
it('should show asset criticality in the risk input panel', () => {
expandFirstAlertUserFlyout();
expandRiskInputsFlyoutPanel();
cy.get(ASSET_CRITICALITY_BADGE).should('contain.text', 'Very important');
});

it('should display asset criticality accordion', () => {
cy.log('asset criticality');
expandFirstAlertUserFlyout();
cy.get(ENTITY_DETAILS_FLYOUT_ASSET_CRITICALITY_SELECTOR).should(
'contain.text',
'Asset Criticality'
);

cy.get(ENTITY_DETAILS_FLYOUT_ASSET_CRITICALITY_BUTTON).should('have.text', 'Create');
});

it('should display asset criticality modal', () => {
cy.log('asset criticality modal');
expandFirstAlertUserFlyout();
toggleAssetCriticalityModal();
cy.get(ENTITY_DETAILS_FLYOUT_ASSET_CRITICALITY_MODAL_TITLE).should(
'have.text',
'Pick asset criticality level'
);
});

it('should update asset criticality state', () => {
cy.log('asset criticality update');
expandFirstAlertUserFlyout();
selectAssetCriticalityLevel('Important');
cy.get(ENTITY_DETAILS_FLYOUT_ASSET_CRITICALITY_LEVEL)
.contains('Important')
.should('be.visible');
});
});

it('should update asset criticality state', () => {
cy.log('asset criticality update');
expandFirstAlertUserFlyout();
selectAssetCriticalityLevel('Important');
cy.get(ENTITY_DETAILS_FLYOUT_ASSET_CRITICALITY_LEVEL)
.contains('Important')
.should('be.visible');
describe('Managed data section', () => {
beforeEach(() => {
mockFleetInstalledIntegrations([
{
package_name: ENTRA_ID_PACKAGE_NAME,
is_enabled: true,
package_title: 'azure entra',
package_version: 'test_package_version',
},
{
package_name: OKTA_PACKAGE_NAME,
is_enabled: true,
package_title: 'okta',
package_version: 'test_package_version',
},
]);
});

it('should show okta and azure managed data sections and expand panel', () => {
expandFirstAlertUserFlyout();

expandManagedDataEntraPanel();
cy.get(ENTRA_DOCUMENT_TAB).should('have.attr', 'aria-selected');
cy.get(ASSET_TYPE_FIELD).should('contain.text', 'microsoft_entra_id_user');

expandManagedDataOktaPanel();
cy.get(OKTA_DOCUMENT_TAB).should('have.attr', 'aria-selected');
cy.get(ASSET_TYPE_FIELD).should('contain.text', 'okta_user');
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@

import { getDataTestSubjectSelector } from '../../helpers/common';

export const USER_PANEL_HEADER = getDataTestSubjectSelector('user-panel-header');
export const ASSET_TYPE_FIELD = getDataTestSubjectSelector('event-field-asset.type');

export const OKTA_DOCUMENT_TAB = getDataTestSubjectSelector('securitySolutionFlyoutOktaTab');

export const ENTRA_DOCUMENT_TAB = getDataTestSubjectSelector('securitySolutionFlyoutEntraTab');
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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 { getDataTestSubjectSelector } from '../../helpers/common';

export const USER_PANEL_HEADER = getDataTestSubjectSelector('user-panel-header');

const MANAGED_DATA_SECTION = getDataTestSubjectSelector('managedUser-data');

export const OKTA_MANAGED_DATA_TITLE = `${MANAGED_DATA_SECTION} ${getDataTestSubjectSelector(
'managed-user-accordion-userAssetOktaLeftSection'
)}`;

export const ENTRA_MANAGED_DATA_TITLE = `${MANAGED_DATA_SECTION} ${getDataTestSubjectSelector(
'managed-user-accordion-userAssetEntraLeftSection'
)}`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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 {
GET_INSTALLED_INTEGRATIONS_URL,
InstalledIntegration,
} from '@kbn/security-solution-plugin/common/api/detection_engine';

export const mockFleetInstalledIntegrations = (integrations: InstalledIntegration[] = []) => {
cy.intercept('GET', `${GET_INSTALLED_INTEGRATIONS_URL}*`, {
statusCode: 200,
body: {
installed_integrations: integrations,
},
}).as('installedIntegrations');
};
Loading

0 comments on commit 851df69

Please sign in to comment.