diff --git a/public/apps/configuration/panels/permission-list/permission-list.tsx b/public/apps/configuration/panels/permission-list/permission-list.tsx index 96f0c425b..ec90dd7d5 100644 --- a/public/apps/configuration/panels/permission-list/permission-list.tsx +++ b/public/apps/configuration/panels/permission-list/permission-list.tsx @@ -34,7 +34,14 @@ import { Query, } from '@elastic/eui'; import { difference } from 'lodash'; -import React, { Dispatch, ReactNode, SetStateAction, useCallback, useState } from 'react'; +import React, { + Dispatch, + ReactNode, + SetStateAction, + useCallback, + useState, + useContext, +} from 'react'; import { AppDependencies } from '../../../types'; import { Action, DataObject, ActionGroupItem, ExpandedRowMapInterface } from '../../types'; import { @@ -54,6 +61,9 @@ import { useDeleteConfirmState } from '../../utils/delete-confirm-modal-utils'; import { useContextMenuState } from '../../utils/context-menu'; import { generateResourceName } from '../../utils/resource-utils'; import { DocLinks } from '../../constants'; +import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; +import { DataSourceContext } from '../../app-router'; +import { createDataSourceQuery } from '../../../../utils/datasource-utils'; export function renderBooleanToCheckMark(value: boolean): React.ReactNode { return value ? : ''; @@ -77,7 +87,7 @@ export function toggleRowDetails( }); } -export function renderRowExpanstionArrow( +export function renderRowExpansionArrow( itemIdToExpandedRowMap: ExpandedRowMapInterface, actionGroupDict: DataObject, setItemIdToExpandedRowMap: Dispatch> @@ -129,7 +139,7 @@ function getColumns( align: RIGHT_ALIGNMENT, width: '40px', isExpander: true, - render: renderRowExpanstionArrow( + render: renderRowExpansionArrow( itemIdToExpandedRowMap, actionGroupDict, setItemIdToExpandedRowMap @@ -182,6 +192,8 @@ export function PermissionList(props: AppDependencies) { const [selection, setSelection] = React.useState([]); const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState({}); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; + // Modal state const [editModal, setEditModal] = useState(null); @@ -194,7 +206,10 @@ export function PermissionList(props: AppDependencies) { const fetchData = useCallback(async () => { try { setLoading(true); - const actionGroups = await fetchActionGroups(props.coreStart.http); + const actionGroups = await fetchActionGroups( + props.coreStart.http, + createDataSourceQuery(dataSource.id) + ); setActionGroupDict(actionGroups); setPermissionList(await mergeAllPermissions(actionGroups)); } catch (e) { @@ -203,16 +218,20 @@ export function PermissionList(props: AppDependencies) { } finally { setLoading(false); } - }, [props.coreStart.http]); + }, [props.coreStart.http, dataSource.id]); React.useEffect(() => { fetchData(); - }, [props.coreStart.http, fetchData]); + }, [props.coreStart.http, fetchData, dataSource.id]); const handleDelete = async () => { const groupsToDelete: string[] = selection.map((r) => r.name); try { - await requestDeleteActionGroups(props.coreStart.http, groupsToDelete); + await requestDeleteActionGroups( + props.coreStart.http, + groupsToDelete, + createDataSourceQuery(dataSource.id) + ); setPermissionList(difference(permissionList, selection)); setSelection([]); } catch (e) { @@ -276,9 +295,12 @@ export function PermissionList(props: AppDependencies) { handleClose={() => setEditModal(null)} handleSave={async (groupName, allowedAction) => { try { - await updateActionGroup(props.coreStart.http, groupName, { - allowed_actions: allowedAction, - }); + await updateActionGroup( + props.coreStart.http, + groupName, + { allowed_actions: allowedAction }, + createDataSourceQuery(dataSource.id) + ); setEditModal(null); fetchData(); addToast({ @@ -301,7 +323,11 @@ export function PermissionList(props: AppDependencies) { }; const createActionGroupMenuItems = [ - showEditModal('', Action.create, [])}> + showEditModal('', Action.create, [])} + > Create from blank , +

Permissions

diff --git a/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx b/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx index 965ccea01..856a3e8fb 100644 --- a/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx +++ b/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx @@ -19,7 +19,7 @@ import { PermissionList, renderBooleanToCheckMark, toggleRowDetails, - renderRowExpanstionArrow, + renderRowExpansionArrow, } from '../permission-list'; import { EuiInMemoryTable, EuiButtonIcon } from '@elastic/eui'; import { @@ -49,6 +49,11 @@ jest.mock('../../../utils/toast-utils', () => ({ useToastState: jest.fn().mockReturnValue([[], jest.fn(), jest.fn()]), })); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn().mockReturnValue({ dataSource: { id: 'test' }, setDataSource: jest.fn() }), // Mock the useContext hook to return dummy datasource and setdatasource function +})); + describe('Permission list page ', () => { const sampleActionGroup: PermissionListingItem = { name: 'group', @@ -73,7 +78,7 @@ describe('Permission list page ', () => { describe('renderRowExpanstionArrow', () => { it('should render down arrow when collapsed', () => { - const renderFunc = renderRowExpanstionArrow({}, {}, jest.fn()); + const renderFunc = renderRowExpansionArrow({}, {}, jest.fn()); const Wrapper = () => <>{renderFunc(sampleActionGroup)}; const component = shallow(); @@ -81,7 +86,7 @@ describe('Permission list page ', () => { }); it('should render up arrow when expanded', () => { - const renderFunc = renderRowExpanstionArrow( + const renderFunc = renderRowExpansionArrow( { [sampleActionGroup.name]: sampleActionGroup }, {}, jest.fn() @@ -94,6 +99,12 @@ describe('Permission list page ', () => { }); describe('PermissionList', () => { + const mockCoreStart = { + http: 1, + }; + const dataSourceQuery = { + dataSourceId: 'test', + }; it('render empty', () => { const component = shallow( { jest.spyOn(React, 'useEffect').mockImplementationOnce((f) => f()); shallow( ); - expect(fetchActionGroups).toBeCalled(); + expect(fetchActionGroups).toBeCalledWith(mockCoreStart.http, dataSourceQuery); }); it('fetch data error', () => { @@ -147,7 +158,7 @@ describe('Permission list page ', () => { it('submit change', () => { const component = shallow( { const submitFunc = component.find(PermissionEditModal).prop('handleSave'); submitFunc('group1', []); - expect(updateActionGroup).toBeCalled(); + expect(updateActionGroup).toBeCalledWith( + mockCoreStart.http, + 'group1', + { allowed_actions: [] }, + dataSourceQuery + ); }); it('submit change error', () => { @@ -186,7 +202,7 @@ describe('Permission list page ', () => { it('delete action group', (done) => { shallow( { deleteFunc(); process.nextTick(() => { - expect(requestDeleteActionGroups).toBeCalled(); + expect(requestDeleteActionGroups).toBeCalledWith(mockCoreStart.http, [], dataSourceQuery); done(); }); }); diff --git a/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap b/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap index 9826ce5c9..a9cbacb15 100644 --- a/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap +++ b/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap @@ -17,6 +17,7 @@ exports[`Role list Render columns render Customization column 1`] = ` > Reserved diff --git a/public/apps/configuration/utils/action-groups-utils.tsx b/public/apps/configuration/utils/action-groups-utils.tsx index 048448ead..7852f86ba 100644 --- a/public/apps/configuration/utils/action-groups-utils.tsx +++ b/public/apps/configuration/utils/action-groups-utils.tsx @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import { HttpStart } from 'opensearch-dashboards/public'; +import { HttpStart, HttpFetchQuery } from 'opensearch-dashboards/public'; import { map } from 'lodash'; import { API_ENDPOINT_ACTIONGROUPS, CLUSTER_PERMISSIONS, INDEX_PERMISSIONS } from '../constants'; import { DataObject, ActionGroupItem, ActionGroupUpdate, ObjectsMessage } from '../types'; @@ -29,10 +29,14 @@ export interface PermissionListingItem { hasIndexPermission: boolean; } -export async function fetchActionGroups(http: HttpStart): Promise> { +export async function fetchActionGroups( + http: HttpStart, + query: HttpFetchQuery +): Promise> { const actiongroups = await httpGet>({ http, url: API_ENDPOINT_ACTIONGROUPS, + query, }); return actiongroups.data; } @@ -50,8 +54,11 @@ export function transformActionGroupsToListingFormat( })); } -export async function fetchActionGroupListing(http: HttpStart): Promise { - return transformActionGroupsToListingFormat(await fetchActionGroups(http)); +export async function fetchActionGroupListing( + http: HttpStart, + query: HttpFetchQuery +): Promise { + return transformActionGroupsToListingFormat(await fetchActionGroups(http, query)); } function getClusterSinglePermissions(): PermissionListingItem[] { @@ -76,8 +83,11 @@ function getIndexSinglePermissions(): PermissionListingItem[] { })); } -export async function getAllPermissionsListing(http: HttpStart): Promise { - return mergeAllPermissions(await fetchActionGroups(http)); +export async function getAllPermissionsListing( + http: HttpStart, + query: HttpFetchQuery +): Promise { + return mergeAllPermissions(await fetchActionGroups(http, query)); } export async function mergeAllPermissions( @@ -91,17 +101,23 @@ export async function mergeAllPermissions( export async function updateActionGroup( http: HttpStart, groupName: string, - updateObject: ActionGroupUpdate + updateObject: ActionGroupUpdate, + query: HttpFetchQuery ): Promise { return await httpPost({ http, url: getResourceUrl(API_ENDPOINT_ACTIONGROUPS, groupName), body: updateObject, + query, }); } -export async function requestDeleteActionGroups(http: HttpStart, groups: string[]) { +export async function requestDeleteActionGroups( + http: HttpStart, + groups: string[], + query: HttpFetchQuery +) { for (const group of groups) { - await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_ACTIONGROUPS, group) }); + await httpDelete({ http, url: getResourceUrl(API_ENDPOINT_ACTIONGROUPS, group), query }); } } diff --git a/public/apps/configuration/utils/display-utils.tsx b/public/apps/configuration/utils/display-utils.tsx index d4b33fb62..f11584ab4 100644 --- a/public/apps/configuration/utils/display-utils.tsx +++ b/public/apps/configuration/utils/display-utils.tsx @@ -70,7 +70,14 @@ export function renderCustomization(reserved: boolean, props: UIProps) { - {reserved ? 'Reserved' : 'Custom'} + + {reserved ? 'Reserved' : 'Custom'} + ); diff --git a/public/apps/configuration/utils/test/__snapshots__/display-utils.test.tsx.snap b/public/apps/configuration/utils/test/__snapshots__/display-utils.test.tsx.snap index f81deee72..f1795f2a5 100644 --- a/public/apps/configuration/utils/test/__snapshots__/display-utils.test.tsx.snap +++ b/public/apps/configuration/utils/test/__snapshots__/display-utils.test.tsx.snap @@ -16,6 +16,7 @@ exports[`Display utils Render Customization column when reserved = False 1`] = ` > Custom @@ -39,6 +40,7 @@ exports[`Display utils Render Customization column when reserved = True 1`] = ` > Reserved diff --git a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js index 93a810243..8026c3da7 100644 --- a/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js +++ b/test/cypress/e2e/multi-datasources/multi_datasources_enabled.spec.js @@ -22,8 +22,8 @@ const createDataSource = () => { }, body: { attributes: { - title: `9202`, - endpoint: `https://localhost:9202`, + title: '9202', + endpoint: 'https://localhost:9202', auth: { type: 'username_password', credentials: { @@ -36,6 +36,15 @@ const createDataSource = () => { }); }; +const closeToast = () => { + // remove browser incompatibiltiy toast causing flakyness (cause it has higher z-index than Create button making it invisible) + cy.get('body').then((body) => { + if (body.find('[data-test-subj="toastCloseButton"]').length > 0) { + cy.get('[data-test-subj="toastCloseButton"]').click(); + } + }); +}; + const deleteAllDataSources = () => { cy.request( 'GET', @@ -77,7 +86,7 @@ describe('Multi-datasources enabled', () => { cy.get('.euiToastHeader__title').should('contain', 'successful for Local cluster'); // Remote cluster purge cache cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.contains('li.euiSelectableListItem', '9202').click(); + cy.get('[title="9202"]').click(); cy.get('[data-test-subj="purge-cache"]').click(); cy.get('.euiToastHeader__title').should('contain', 'successful for 9202'); cy.visit('http://localhost:5601/app/security-dashboards-plugin#/auth'); @@ -91,21 +100,23 @@ describe('Multi-datasources enabled', () => { cy.get('.panel-header-count').first().invoke('text').should('contain', '(6)'); // Remote cluster auth cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.contains('li.euiSelectableListItem', '9202').click(); - cy.get('.panel-header-count').first().invoke('text').should('contain', '(2)'); - cy.visit('http://localhost:5601/app/security-dashboards-plugin#/users'); - // Data source persisted across tabs - cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').contains('9202'); + cy.get('[title="9202"]').click(); + cy.get('.panel-header-count').first().invoke('text').should('contain', '(6)'); }); - it.skip('Checks Users Tab', () => { + it('Checks Users Tab', () => { cy.visit('http://localhost:5601/app/security-dashboards-plugin#/users'); // Create an internal user in the remote cluster cy.contains('h3', 'Internal users'); cy.contains('a', 'admin'); - // TODO replace these with navigating to urls that get read to determine datasource, since these are flaky + + closeToast(); + + // select remote data source cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.contains('li.euiSelectableListItem', '9202').click(); + cy.get('[title="9202"]').click(); + + // create a user on remote data source cy.get('[data-test-subj="create-user"]').click(); cy.get('[data-test-subj="name-text"]').focus().type('9202-user'); cy.get('[data-test-subj="password"]').focus().type('myStrongPassword123!'); @@ -113,12 +124,59 @@ describe('Multi-datasources enabled', () => { cy.get('[data-test-subj="submit-save-user"]').click(); // Internal user exists on the remote - cy.visit('http://localhost:5601/app/security-dashboards-plugin#/users'); + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( + 'contain', + '9202' + ); cy.get('[data-test-subj="checkboxSelectRow-9202-user"]').should('exist'); // Internal user doesn't exist on local cluster cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); - cy.contains('li.euiSelectableListItem', 'Local cluster').click(); + cy.get('[title="Local cluster"]').click(); + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( + 'contain', + 'Local cluster' + ); cy.get('[data-test-subj="checkboxSelectRow-9202-user"]').should('not.exist'); }); + + it('Checks Permissions Tab', () => { + cy.visit('http://localhost:5601/app/security-dashboards-plugin#/permissions'); + // Create a permission in the remote cluster + cy.contains('h3', 'Permissions'); + + closeToast(); + + // Select remote cluster + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); + cy.get('[title="9202"]').click(); + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( + 'contain', + '9202' + ); + + // Create an action group + cy.get('[id="Create action group"]').click(); + cy.get('[id="create-from-blank"]').click(); + cy.get('[data-test-subj="name-text"]') + .focus() + .type('test_permission_ag', { force: true }) + .should('have.value', 'test_permission_ag'); + cy.get('[data-test-subj="comboBoxInput"]').focus().type('some_permission'); + cy.get('[id="submit"]').click(); + + // Permission exists on the remote data source + cy.get('[data-text="Customization"]').click(); + cy.get('[data-test-subj="filter-custom-action-groups"]').click(); + cy.get('[data-test-subj="checkboxSelectRow-test_permission_ag"]').should('exist'); + + // Permission doesn't exist on local cluster + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').click(); + cy.get('[title="Local cluster"]').click(); + cy.get('[data-test-subj="dataSourceSelectableContextMenuHeaderLink"]').should( + 'contain', + 'Local cluster' + ); + cy.get('[data-test-subj="checkboxSelectRow-test_permission_ag"]').should('not.exist'); + }); }); diff --git a/test/helper/entity_operation.ts b/test/helper/entity_operation.ts index aabdfbd8a..aad5da56f 100644 --- a/test/helper/entity_operation.ts +++ b/test/helper/entity_operation.ts @@ -57,3 +57,24 @@ export async function getEntityAsAdminWithDataSource( .get(root, `/api/v1/configuration/${entityType}/${entityId}?dataSourceId=${dataSourceId}`) .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); } + +export async function getAllEntitiesAsAdminWithDataSource( + root: Root, + entityType: string, + dataSourceId: string +) { + return await osdTestServer.request + .get(root, `/api/v1/configuration/${entityType}?dataSourceId=${dataSourceId}`) + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); +} + +export async function deleteEntityAsAdminWithDataSource( + root: Root, + entityType: string, + entityId: string, + dataSourceId: string +) { + return await osdTestServer.request + .delete(root, `/api/v1/configuration/${entityType}/${entityId}?dataSourceId=${dataSourceId}`) + .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); +} diff --git a/test/jest_integration/security_entity_api.test.ts b/test/jest_integration/security_entity_api.test.ts index c3ca21986..4f745c9de 100644 --- a/test/jest_integration/security_entity_api.test.ts +++ b/test/jest_integration/security_entity_api.test.ts @@ -29,6 +29,8 @@ import { extractAuthCookie, getAuthCookie } from '../helper/cookie'; import { createOrUpdateEntityAsAdmin, createOrUpdateEntityAsAdminWithDataSource, + deleteEntityAsAdminWithDataSource, + getAllEntitiesAsAdminWithDataSource, getEntityAsAdmin, getEntityAsAdminWithDataSource, } from '../helper/entity_operation'; @@ -478,6 +480,7 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { }, }, }); + expect(createDataSource.status).toEqual(200); dataSourceId = createDataSource.body.id; }); @@ -541,10 +544,11 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { it('create/get/update/list/delete internal user for external datasource', async () => { const testUsername = `test_user_${Date.now()}`; const testUserPassword = 'testUserPassword123'; + const entityType = 'internalusers'; const createUserResponse = await createOrUpdateEntityAsAdminWithDataSource( root, - 'internalusers', + entityType, testUsername, { description: 'test user description', @@ -557,7 +561,7 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { const getUserResponse = await getEntityAsAdminWithDataSource( root, - 'internalusers', + entityType, testUsername, dataSourceId ); @@ -565,16 +569,18 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { expect(getUserResponse.body.description).toEqual('test user description'); expect(getUserResponse.body.backend_roles).toContain('arbitrary_backend_role'); - const listUserResponse = await osdTestServer.request - .get(root, `/api/v1/configuration/internalusers?dataSourceId=${dataSourceId}`) - .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); + const listUserResponse = await getAllEntitiesAsAdminWithDataSource( + root, + entityType, + dataSourceId + ); expect(listUserResponse.status).toEqual(200); expect(listUserResponse.body.total).toBeGreaterThan(2); expect(listUserResponse.body.data[testUsername]).toBeTruthy(); const updateUserResponse = await createOrUpdateEntityAsAdminWithDataSource( root, - 'internalusers', + entityType, testUsername, { description: 'new description', @@ -587,27 +593,107 @@ describe('start OpenSearch Dashboards server multi datasources enabled', () => { const getUpdatedUserResponse = await getEntityAsAdminWithDataSource( root, - 'internalusers', + entityType, testUsername, dataSourceId ); expect(getUpdatedUserResponse.status).toEqual(200); expect(getUpdatedUserResponse.body.description).toEqual('new description'); - const deleteUserResponse = await osdTestServer.request - .delete( - root, - `/api/v1/configuration/internalusers/${testUsername}?dataSourceId=${dataSourceId}` - ) - .set(AUTHORIZATION_HEADER_NAME, ADMIN_CREDENTIALS); + const deleteUserResponse = await deleteEntityAsAdminWithDataSource( + root, + entityType, + testUsername, + dataSourceId + ); expect(deleteUserResponse.status).toEqual(200); const getDeletedUserResponse = await getEntityAsAdminWithDataSource( root, - 'internalusers', + entityType, testUsername, dataSourceId ); expect(getDeletedUserResponse.status).toEqual(404); }); + + it('CRUD Permissions for external datasource', async () => { + const entityType = 'actiongroups'; + const testActionGroupName = `test_action_group_${Date.now()}`; + + const createActionGroupResponse = await createOrUpdateEntityAsAdminWithDataSource( + root, + entityType, + testActionGroupName, + { + allowed_actions: ['some_allowed_action'], + }, + dataSourceId + ); + expect(createActionGroupResponse.status).toEqual(200); + + const getActionGroupsResponse = await getAllEntitiesAsAdminWithDataSource( + root, + entityType, + dataSourceId + ); + expect(getActionGroupsResponse.status).toEqual(200); + expect(getActionGroupsResponse.body.data?.hasOwnProperty(testActionGroupName)).toBe(true); + expect(getActionGroupsResponse.body.data[testActionGroupName].allowed_actions).toContain( + 'some_allowed_action' + ); + + // verify that this AG is not created in Local Cluster + const getActionGroupsResponseLocalCluster = await getAllEntitiesAsAdminWithDataSource( + root, + entityType, + '' + ); + expect(getActionGroupsResponseLocalCluster.status).toEqual(200); + expect(getActionGroupsResponseLocalCluster.body.data?.hasOwnProperty(testActionGroupName)).toBe( + false + ); + + const updatePermissionResponse = await createOrUpdateEntityAsAdminWithDataSource( + root, + entityType, + testActionGroupName, + { + allowed_actions: ['some_allowed_action', 'another_permission'], + }, + dataSourceId + ); + expect(updatePermissionResponse.status).toEqual(200); + + const getUpdatedActionGroupsResponse = await getAllEntitiesAsAdminWithDataSource( + root, + entityType, + dataSourceId + ); + expect(getUpdatedActionGroupsResponse.status).toEqual(200); + expect(getUpdatedActionGroupsResponse.body.data?.hasOwnProperty(testActionGroupName)).toBe( + true + ); + expect(getUpdatedActionGroupsResponse.body.data[testActionGroupName].allowed_actions).toContain( + 'another_permission' + ); + + const deleteActionGroupResponse = await deleteEntityAsAdminWithDataSource( + root, + entityType, + testActionGroupName, + dataSourceId + ); + expect(deleteActionGroupResponse.status).toEqual(200); + + const getDeletedActionGroupsResponse = await getAllEntitiesAsAdminWithDataSource( + root, + entityType, + dataSourceId + ); + expect(getDeletedActionGroupsResponse.status).toEqual(200); + expect(getDeletedActionGroupsResponse.body.data?.hasOwnProperty(testActionGroupName)).toBe( + false + ); + }); });