[Defend workflows] Add Cypress tests for Endpoint reassignment functi… (#151887)

This commit is contained in:
Patryk Kopyciński 2023-02-24 16:19:30 +01:00 committed by GitHub
parent b907c51e0e
commit e7e66a8944
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 330 additions and 124 deletions

View file

@ -51,6 +51,8 @@ export function defineCypressConfig(options?: Cypress.ConfigOptions<any>) {
on(event, task); on(event, task);
}, config); }, config);
return config;
} }
}, },
}, },

View file

@ -5,15 +5,91 @@
* 2.0. * 2.0.
*/ */
import type { Agent } from '@kbn/fleet-plugin/common';
import { ENDPOINT_VM_NAME } from '../../tasks/common';
import {
getAgentByHostName,
getEndpointIntegrationVersion,
reassignAgentPolicy,
} from '../../tasks/fleet';
import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import { getEndpointListPath } from '../../../common/routing';
import { login } from '../../tasks/login'; import { login } from '../../tasks/login';
import {
AGENT_HOSTNAME_CELL,
TABLE_ROW_ACTIONS,
TABLE_ROW_ACTIONS_MENU,
AGENT_POLICY_CELL,
} from '../../screens/endpoints';
import {
FLEET_REASSIGN_POLICY_MODAL,
FLEET_REASSIGN_POLICY_MODAL_CONFIRM_BUTTON,
} from '../../screens/fleet';
describe('Endpoints page', () => { describe('Endpoints page', () => {
const endpointHostname = Cypress.env(ENDPOINT_VM_NAME);
beforeEach(() => { beforeEach(() => {
login(); login();
}); });
it('Loads the endpoints page', () => { it('Shows endpoint on the list', () => {
cy.visit('/app/security/administration/endpoints'); cy.visit(getEndpointListPath({ name: 'endpointList' }));
cy.contains('Hosts running Elastic Defend').should('exist'); cy.contains('Hosts running Elastic Defend').should('exist');
cy.getByTestSubj(AGENT_HOSTNAME_CELL).should('have.text', endpointHostname);
});
describe('Endpoint reassignment', () => {
let response: IndexedFleetEndpointPolicyResponse;
let initialAgentData: Agent;
before(() => {
getAgentByHostName(endpointHostname).then((agentData) => {
initialAgentData = agentData;
});
getEndpointIntegrationVersion().then((version) => {
cy.task<IndexedFleetEndpointPolicyResponse>('indexFleetEndpointPolicy', {
policyName: `Reassign ${Math.random().toString(36).substr(2, 5)}`,
endpointPackageVersion: version,
}).then((data) => {
response = data;
});
});
});
beforeEach(() => {
login();
});
after(() => {
if (initialAgentData?.policy_id) {
reassignAgentPolicy(initialAgentData.id, initialAgentData.policy_id);
}
if (response) {
cy.task('deleteIndexedFleetEndpointPolicies', response);
}
});
it('User can reassign a single endpoint to a different Agent Configuration', () => {
cy.visit(getEndpointListPath({ name: 'endpointList' }));
const hostname = cy
.getByTestSubj(AGENT_HOSTNAME_CELL)
.filter(`:contains("${endpointHostname}")`);
const tableRow = hostname.parents('tr');
tableRow.getByTestSubj(TABLE_ROW_ACTIONS).click();
cy.getByTestSubj(TABLE_ROW_ACTIONS_MENU).contains('Reassign agent policy').click();
cy.getByTestSubj(FLEET_REASSIGN_POLICY_MODAL)
.find('select')
.select(response.agentPolicies[0].name);
cy.getByTestSubj(FLEET_REASSIGN_POLICY_MODAL_CONFIRM_BUTTON).click();
cy.getByTestSubj(AGENT_HOSTNAME_CELL)
.filter(`:contains("${endpointHostname}")`)
.should('exist');
cy.getByTestSubj(AGENT_HOSTNAME_CELL)
.filter(`:contains("${endpointHostname}")`)
.parents('tr')
.getByTestSubj(AGENT_POLICY_CELL)
.should('have.text', response.agentPolicies[0].name);
});
}); });
}); });

View file

@ -64,8 +64,8 @@ const visitArtifactTab = (tabId: string) => {
const visitPolicyDetailsPage = () => { const visitPolicyDetailsPage = () => {
cy.visit('/app/security/administration/policy'); cy.visit('/app/security/administration/policy');
cy.getBySel('policyNameCellLink').eq(0).click({ force: true }); cy.getByTestSubj('policyNameCellLink').eq(0).click({ force: true });
cy.getBySel('policyDetailsPage').should('exist'); cy.getByTestSubj('policyDetailsPage').should('exist');
cy.get('#settings').should('exist'); // waiting for Policy Settings tab cy.get('#settings').should('exist'); // waiting for Policy Settings tab
}; };
@ -99,18 +99,18 @@ describe('Artifact tabs in Policy Details page', () => {
loginWithPrivilegeRead(testData.privilegePrefix); loginWithPrivilegeRead(testData.privilegePrefix);
visitArtifactTab(testData.tabId); visitArtifactTab(testData.tabId);
cy.getBySel('policy-artifacts-empty-unexisting').should('exist'); cy.getByTestSubj('policy-artifacts-empty-unexisting').should('exist');
cy.getBySel('unexisting-manage-artifacts-button').should('not.exist'); cy.getByTestSubj('unexisting-manage-artifacts-button').should('not.exist');
}); });
it(`[ALL] User can add ${testData.title} artifact`, () => { it(`[ALL] User can add ${testData.title} artifact`, () => {
loginWithPrivilegeAll(); loginWithPrivilegeAll();
visitArtifactTab(testData.tabId); visitArtifactTab(testData.tabId);
cy.getBySel('policy-artifacts-empty-unexisting').should('exist'); cy.getByTestSubj('policy-artifacts-empty-unexisting').should('exist');
cy.getBySel('unexisting-manage-artifacts-button').should('exist').click(); cy.getByTestSubj('unexisting-manage-artifacts-button').should('exist').click();
const { formActions, checkResults } = testData.create; const { formActions, checkResults } = testData.create;
@ -118,18 +118,18 @@ describe('Artifact tabs in Policy Details page', () => {
// Add a per policy artifact - but not assign it to any policy // Add a per policy artifact - but not assign it to any policy
cy.get('[data-test-subj$="-perPolicy"]').click(); // test-subjects are generated in different formats, but all ends with -perPolicy cy.get('[data-test-subj$="-perPolicy"]').click(); // test-subjects are generated in different formats, but all ends with -perPolicy
cy.getBySel(`${testData.pagePrefix}-flyout-submitButton`).click(); cy.getByTestSubj(`${testData.pagePrefix}-flyout-submitButton`).click();
// Check new artifact is in the list // Check new artifact is in the list
for (const checkResult of checkResults) { for (const checkResult of checkResults) {
cy.getBySel(checkResult.selector).should('have.text', checkResult.value); cy.getByTestSubj(checkResult.selector).should('have.text', checkResult.value);
} }
cy.getBySel('policyDetailsPage').should('not.exist'); cy.getByTestSubj('policyDetailsPage').should('not.exist');
cy.getBySel('backToOrigin').contains(/^Back to .+ policy$/); cy.getByTestSubj('backToOrigin').contains(/^Back to .+ policy$/);
cy.getBySel('backToOrigin').click(); cy.getByTestSubj('backToOrigin').click();
cy.getBySel('policyDetailsPage').should('exist'); cy.getByTestSubj('policyDetailsPage').should('exist');
}); });
}); });
@ -144,34 +144,34 @@ describe('Artifact tabs in Policy Details page', () => {
loginWithPrivilegeRead(testData.privilegePrefix); loginWithPrivilegeRead(testData.privilegePrefix);
visitArtifactTab(testData.tabId); visitArtifactTab(testData.tabId);
cy.getBySel('policy-artifacts-empty-unassigned').should('exist'); cy.getByTestSubj('policy-artifacts-empty-unassigned').should('exist');
cy.getBySel('unassigned-manage-artifacts-button').should('not.exist'); cy.getByTestSubj('unassigned-manage-artifacts-button').should('not.exist');
cy.getBySel('unassigned-assign-artifacts-button').should('not.exist'); cy.getByTestSubj('unassigned-assign-artifacts-button').should('not.exist');
}); });
it(`[ALL] User can Manage and Assign ${testData.title} artifacts`, () => { it(`[ALL] User can Manage and Assign ${testData.title} artifacts`, () => {
loginWithPrivilegeAll(); loginWithPrivilegeAll();
visitArtifactTab(testData.tabId); visitArtifactTab(testData.tabId);
cy.getBySel('policy-artifacts-empty-unassigned').should('exist'); cy.getByTestSubj('policy-artifacts-empty-unassigned').should('exist');
// Manage artifacts // Manage artifacts
cy.getBySel('unassigned-manage-artifacts-button').should('exist').click(); cy.getByTestSubj('unassigned-manage-artifacts-button').should('exist').click();
cy.location('pathname').should( cy.location('pathname').should(
'equal', 'equal',
`/app/security/administration/${testData.urlPath}` `/app/security/administration/${testData.urlPath}`
); );
cy.getBySel('backToOrigin').click(); cy.getByTestSubj('backToOrigin').click();
// Assign artifacts // Assign artifacts
cy.getBySel('unassigned-assign-artifacts-button').should('exist').click(); cy.getByTestSubj('unassigned-assign-artifacts-button').should('exist').click();
cy.getBySel('artifacts-assign-flyout').should('exist'); cy.getByTestSubj('artifacts-assign-flyout').should('exist');
cy.getBySel('artifacts-assign-confirm-button').should('be.disabled'); cy.getByTestSubj('artifacts-assign-confirm-button').should('be.disabled');
cy.getBySel(`${testData.artifactName}_checkbox`).click(); cy.getByTestSubj(`${testData.artifactName}_checkbox`).click();
cy.getBySel('artifacts-assign-confirm-button').click(); cy.getByTestSubj('artifacts-assign-confirm-button').click();
}); });
}); });
@ -189,17 +189,17 @@ describe('Artifact tabs in Policy Details page', () => {
visitArtifactTab(testData.tabId); visitArtifactTab(testData.tabId);
// List of artifacts // List of artifacts
cy.getBySel('artifacts-collapsed-list-card').should('have.length', 1); cy.getByTestSubj('artifacts-collapsed-list-card').should('have.length', 1);
cy.getBySel('artifacts-collapsed-list-card-header-titleHolder').contains( cy.getByTestSubj('artifacts-collapsed-list-card-header-titleHolder').contains(
testData.artifactName testData.artifactName
); );
// Cannot assign artifacts // Cannot assign artifacts
cy.getBySel('artifacts-assign-button').should('not.exist'); cy.getByTestSubj('artifacts-assign-button').should('not.exist');
// Cannot remove from policy // Cannot remove from policy
cy.getBySel('artifacts-collapsed-list-card-header-actions-button').click(); cy.getByTestSubj('artifacts-collapsed-list-card-header-actions-button').click();
cy.getBySel('remove-from-policy-action').should('not.exist'); cy.getByTestSubj('remove-from-policy-action').should('not.exist');
}); });
it(`[ALL] User can see ${testData.title} artifacts and can assign or remove artifacts from policy`, () => { it(`[ALL] User can see ${testData.title} artifacts and can assign or remove artifacts from policy`, () => {
@ -207,20 +207,20 @@ describe('Artifact tabs in Policy Details page', () => {
visitArtifactTab(testData.tabId); visitArtifactTab(testData.tabId);
// List of artifacts // List of artifacts
cy.getBySel('artifacts-collapsed-list-card').should('have.length', 1); cy.getByTestSubj('artifacts-collapsed-list-card').should('have.length', 1);
cy.getBySel('artifacts-collapsed-list-card-header-titleHolder').contains( cy.getByTestSubj('artifacts-collapsed-list-card-header-titleHolder').contains(
testData.artifactName testData.artifactName
); );
// Assign artifacts // Assign artifacts
cy.getBySel('artifacts-assign-button').should('exist').click(); cy.getByTestSubj('artifacts-assign-button').should('exist').click();
cy.getBySel('artifacts-assign-flyout').should('exist'); cy.getByTestSubj('artifacts-assign-flyout').should('exist');
cy.getBySel('artifacts-assign-cancel-button').click(); cy.getByTestSubj('artifacts-assign-cancel-button').click();
// Remove from policy // Remove from policy
cy.getBySel('artifacts-collapsed-list-card-header-actions-button').click(); cy.getByTestSubj('artifacts-collapsed-list-card-header-actions-button').click();
cy.getBySel('remove-from-policy-action').click(); cy.getByTestSubj('remove-from-policy-action').click();
cy.getBySel('confirmModalConfirmButton').click(); cy.getByTestSubj('confirmModalConfirmButton').click();
cy.contains('Successfully removed'); cy.contains('Successfully removed');
}); });

View file

@ -51,10 +51,10 @@ describe('Artifacts pages', () => {
describe(`When on the ${testData.title} entries list`, () => { describe(`When on the ${testData.title} entries list`, () => {
it(`no access - should show no privileges callout`, () => { it(`no access - should show no privileges callout`, () => {
loginWithoutAccess(`/app/security/administration/${testData.urlPath}`); loginWithoutAccess(`/app/security/administration/${testData.urlPath}`);
cy.getBySel('noPrivilegesPage').should('exist'); cy.getByTestSubj('noPrivilegesPage').should('exist');
cy.getBySel('empty-page-feature-action').should('exist'); cy.getByTestSubj('empty-page-feature-action').should('exist');
cy.getBySel(testData.emptyState).should('not.exist'); cy.getByTestSubj(testData.emptyState).should('not.exist');
cy.getBySel(`${testData.pagePrefix}-emptyState-addButton`).should('not.exist'); cy.getByTestSubj(`${testData.pagePrefix}-emptyState-addButton`).should('not.exist');
}); });
it(`read - should show empty state page if there is no ${testData.title} entry and the add button does not exist`, () => { it(`read - should show empty state page if there is no ${testData.title} entry and the add button does not exist`, () => {
@ -62,33 +62,33 @@ describe('Artifacts pages', () => {
testData.privilegePrefix, testData.privilegePrefix,
`/app/security/administration/${testData.urlPath}` `/app/security/administration/${testData.urlPath}`
); );
cy.getBySel(testData.emptyState).should('exist'); cy.getByTestSubj(testData.emptyState).should('exist');
cy.getBySel(`${testData.pagePrefix}-emptyState-addButton`).should('not.exist'); cy.getByTestSubj(`${testData.pagePrefix}-emptyState-addButton`).should('not.exist');
}); });
it(`write - should show empty state page if there is no ${testData.title} entry and the add button exists`, () => { it(`write - should show empty state page if there is no ${testData.title} entry and the add button exists`, () => {
loginWithWriteAccess(`/app/security/administration/${testData.urlPath}`); loginWithWriteAccess(`/app/security/administration/${testData.urlPath}`);
cy.getBySel(testData.emptyState).should('exist'); cy.getByTestSubj(testData.emptyState).should('exist');
cy.getBySel(`${testData.pagePrefix}-emptyState-addButton`).should('exist'); cy.getByTestSubj(`${testData.pagePrefix}-emptyState-addButton`).should('exist');
}); });
it(`write - should create new ${testData.title} entry`, () => { it(`write - should create new ${testData.title} entry`, () => {
loginWithWriteAccess(`/app/security/administration/${testData.urlPath}`); loginWithWriteAccess(`/app/security/administration/${testData.urlPath}`);
// Opens add flyout // Opens add flyout
cy.getBySel(`${testData.pagePrefix}-emptyState-addButton`).click(); cy.getByTestSubj(`${testData.pagePrefix}-emptyState-addButton`).click();
performUserActions(testData.create.formActions); performUserActions(testData.create.formActions);
// Submit create artifact form // Submit create artifact form
cy.getBySel(`${testData.pagePrefix}-flyout-submitButton`).click(); cy.getByTestSubj(`${testData.pagePrefix}-flyout-submitButton`).click();
// Check new artifact is in the list // Check new artifact is in the list
for (const checkResult of testData.create.checkResults) { for (const checkResult of testData.create.checkResults) {
cy.getBySel(checkResult.selector).should('have.text', checkResult.value); cy.getByTestSubj(checkResult.selector).should('have.text', checkResult.value);
} }
// Title is shown after adding an item // Title is shown after adding an item
cy.getBySel('header-page-title').contains(testData.title); cy.getByTestSubj('header-page-title').contains(testData.title);
}); });
it(`read - should not be able to update/delete an existing ${testData.title} entry`, () => { it(`read - should not be able to update/delete an existing ${testData.title} entry`, () => {
@ -96,10 +96,10 @@ describe('Artifacts pages', () => {
testData.privilegePrefix, testData.privilegePrefix,
`/app/security/administration/${testData.urlPath}` `/app/security/administration/${testData.urlPath}`
); );
cy.getBySel('header-page-title').contains(testData.title); cy.getByTestSubj('header-page-title').contains(testData.title);
cy.getBySel(`${testData.pagePrefix}-card-header-actions-button`).should('not.exist'); cy.getByTestSubj(`${testData.pagePrefix}-card-header-actions-button`).should('not.exist');
cy.getBySel(`${testData.pagePrefix}-card-cardEditAction`).should('not.exist'); cy.getByTestSubj(`${testData.pagePrefix}-card-cardEditAction`).should('not.exist');
cy.getBySel(`${testData.pagePrefix}-card-cardDeleteAction`).should('not.exist'); cy.getByTestSubj(`${testData.pagePrefix}-card-cardDeleteAction`).should('not.exist');
}); });
it(`read - should not be able to create a new ${testData.title} entry`, () => { it(`read - should not be able to create a new ${testData.title} entry`, () => {
@ -107,39 +107,39 @@ describe('Artifacts pages', () => {
testData.privilegePrefix, testData.privilegePrefix,
`/app/security/administration/${testData.urlPath}` `/app/security/administration/${testData.urlPath}`
); );
cy.getBySel('header-page-title').contains(testData.title); cy.getByTestSubj('header-page-title').contains(testData.title);
cy.getBySel(`${testData.pagePrefix}-pageAddButton`).should('not.exist'); cy.getByTestSubj(`${testData.pagePrefix}-pageAddButton`).should('not.exist');
}); });
it(`write - should be able to update an existing ${testData.title} entry`, () => { it(`write - should be able to update an existing ${testData.title} entry`, () => {
loginWithWriteAccess(`/app/security/administration/${testData.urlPath}`); loginWithWriteAccess(`/app/security/administration/${testData.urlPath}`);
// Opens edit flyout // Opens edit flyout
cy.getBySel(`${testData.pagePrefix}-card-header-actions-button`).click(); cy.getByTestSubj(`${testData.pagePrefix}-card-header-actions-button`).click();
cy.getBySel(`${testData.pagePrefix}-card-cardEditAction`).click(); cy.getByTestSubj(`${testData.pagePrefix}-card-cardEditAction`).click();
performUserActions(testData.update.formActions); performUserActions(testData.update.formActions);
// Submit edit artifact form // Submit edit artifact form
cy.getBySel(`${testData.pagePrefix}-flyout-submitButton`).click(); cy.getByTestSubj(`${testData.pagePrefix}-flyout-submitButton`).click();
for (const checkResult of testData.create.checkResults) { for (const checkResult of testData.create.checkResults) {
cy.getBySel(checkResult.selector).should('have.text', checkResult.value); cy.getByTestSubj(checkResult.selector).should('have.text', checkResult.value);
} }
// Title still shown after editing an item // Title still shown after editing an item
cy.getBySel('header-page-title').contains(testData.title); cy.getByTestSubj('header-page-title').contains(testData.title);
}); });
it(`write - should be able to delete the existing ${testData.title} entry`, () => { it(`write - should be able to delete the existing ${testData.title} entry`, () => {
loginWithWriteAccess(`/app/security/administration/${testData.urlPath}`); loginWithWriteAccess(`/app/security/administration/${testData.urlPath}`);
// Remove it // Remove it
cy.getBySel(`${testData.pagePrefix}-card-header-actions-button`).click(); cy.getByTestSubj(`${testData.pagePrefix}-card-header-actions-button`).click();
cy.getBySel(`${testData.pagePrefix}-card-cardDeleteAction`).click(); cy.getByTestSubj(`${testData.pagePrefix}-card-cardDeleteAction`).click();
cy.getBySel(`${testData.pagePrefix}-deleteModal-submitButton`).click(); cy.getByTestSubj(`${testData.pagePrefix}-deleteModal-submitButton`).click();
// No card visible after removing it // No card visible after removing it
cy.getBySel(testData.delete.card).should('not.exist'); cy.getByTestSubj(testData.delete.card).should('not.exist');
// Empty state is displayed after removing last item // Empty state is displayed after removing last item
cy.getBySel(testData.emptyState).should('exist'); cy.getByTestSubj(testData.emptyState).should('exist');
}); });
}); });
} }

View file

@ -0,0 +1,11 @@
/*
* 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 const AGENT_HOSTNAME_CELL = 'hostnameCellLink';
export const AGENT_POLICY_CELL = 'policyNameCellLink';
export const TABLE_ROW_ACTIONS = 'endpointTableRowActions';
export const TABLE_ROW_ACTIONS_MENU = 'tableRowActionsMenuPanel';

View file

@ -0,0 +1,9 @@
/*
* 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 const FLEET_REASSIGN_POLICY_MODAL = 'agentReassignPolicyModal';
export const FLEET_REASSIGN_POLICY_MODAL_CONFIRM_BUTTON = 'confirmModalConfirmButton';

View file

@ -0,0 +1,38 @@
/*
* 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.
*/
// / <reference types="cypress" />
import { createKbnClient } from '../../../../scripts/endpoint/common/stack_services';
import type { IndexedFleetEndpointPolicyResponse } from '../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import {
indexFleetEndpointPolicy,
deleteIndexedFleetEndpointPolicies,
} from '../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
export const dataLoaders = (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) => {
const kbnClient = createKbnClient({
url: config.env.KIBANA_URL,
username: config.env.ELASTICSEARCH_USERNAME,
password: config.env.ELASTICSEARCH_PASSWORD,
});
on('task', {
indexFleetEndpointPolicy: async ({
policyName,
endpointPackageVersion,
}: {
policyName: string;
endpointPackageVersion: string;
}) => {
return indexFleetEndpointPolicy(kbnClient, policyName, endpointPackageVersion);
},
deleteIndexedFleetEndpointPolicies: async (indexData: IndexedFleetEndpointPolicyResponse) => {
return deleteIndexedFleetEndpointPolicies(kbnClient, indexData);
},
});
};

View file

@ -31,13 +31,17 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace // eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress { namespace Cypress {
interface Chainable { interface Chainable {
getBySel(...args: Parameters<Cypress.Chainable['get']>): Chainable<JQuery<HTMLElement>>; getByTestSubj(...args: Parameters<Cypress.Chainable['get']>): Chainable<JQuery<HTMLElement>>;
} }
} }
} }
Cypress.Commands.add('getBySel', (selector, ...args) => Cypress.Commands.addQuery('getByTestSubj', function getByTestSubj(selector, options) {
cy.get(`[data-test-subj="${selector}"]`, ...args) const getFn = cy.now('get', `[data-test-subj="${selector}"]`, options) as (
); subject: Cypress.Chainable<JQuery<HTMLElement>>
) => Cypress.Chainable<JQuery<HTMLElement>>;
return (subject) => getFn(subject);
});
Cypress.on('uncaught:exception', () => false); Cypress.on('uncaught:exception', () => false);

View file

@ -6,6 +6,10 @@
*/ */
import { PACKAGE_POLICY_API_ROOT } from '@kbn/fleet-plugin/common'; import { PACKAGE_POLICY_API_ROOT } from '@kbn/fleet-plugin/common';
import type {
ExceptionListItemSchema,
ExceptionListSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { import {
ENDPOINT_ARTIFACT_LISTS, ENDPOINT_ARTIFACT_LISTS,
@ -13,8 +17,7 @@ import {
EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_ITEM_URL,
EXCEPTION_LIST_URL, EXCEPTION_LIST_URL,
} from '@kbn/securitysolution-list-constants'; } from '@kbn/securitysolution-list-constants';
import { request } from './common';
const API_HEADER = { 'kbn-xsrf': 'kibana' };
export const removeAllArtifacts = () => { export const removeAllArtifacts = () => {
for (const listId of ENDPOINT_ARTIFACT_LIST_IDS) { for (const listId of ENDPOINT_ARTIFACT_LIST_IDS) {
@ -23,10 +26,9 @@ export const removeAllArtifacts = () => {
}; };
export const removeExceptionsList = (listId: string) => { export const removeExceptionsList = (listId: string) => {
cy.request({ request({
method: 'DELETE', method: 'DELETE',
url: `${EXCEPTION_LIST_URL}?list_id=${listId}&namespace_type=agnostic`, url: `${EXCEPTION_LIST_URL}?list_id=${listId}&namespace_type=agnostic`,
headers: API_HEADER,
failOnStatusCode: false, failOnStatusCode: false,
}).then(({ status }) => { }).then(({ status }) => {
expect(status).to.be.oneOf([200, 404]); // should either be success or not found expect(status).to.be.oneOf([200, 404]); // should either be success or not found
@ -42,10 +44,9 @@ const ENDPOINT_ARTIFACT_LIST_TYPES = {
}; };
export const createArtifactList = (listId: string) => { export const createArtifactList = (listId: string) => {
cy.request({ request<ExceptionListSchema>({
method: 'POST', method: 'POST',
url: EXCEPTION_LIST_URL, url: EXCEPTION_LIST_URL,
headers: API_HEADER,
body: { body: {
name: listId, name: listId,
description: 'This is a test list', description: 'This is a test list',
@ -61,11 +62,9 @@ export const createArtifactList = (listId: string) => {
}; };
export const createPerPolicyArtifact = (name: string, body: object, policyId?: 'all' | string) => { export const createPerPolicyArtifact = (name: string, body: object, policyId?: 'all' | string) => {
cy.request({ request<ExceptionListItemSchema>({
method: 'POST', method: 'POST',
url: EXCEPTION_LIST_ITEM_URL, url: EXCEPTION_LIST_ITEM_URL,
headers: API_HEADER,
body: { body: {
name, name,
description: '', description: '',

View file

@ -0,0 +1,24 @@
/*
* 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 const ENDPOINT_VM_NAME = 'ENDPOINT_VM_NAME';
export const API_AUTH = {
user: Cypress.env('ELASTICSEARCH_USERNAME'),
pass: Cypress.env('ELASTICSEARCH_PASSWORD'),
};
export const API_HEADERS = { 'kbn-xsrf': 'cypress' };
export const request = <T = unknown>(
options: Partial<Cypress.RequestOptions>
): Cypress.Chainable<Cypress.Response<T>> =>
cy.request<T>({
auth: API_AUTH,
headers: API_HEADERS,
...options,
});

View file

@ -0,0 +1,38 @@
/*
* 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 type { Agent, GetAgentsResponse, GetInfoResponse } from '@kbn/fleet-plugin/common';
import { agentRouteService, epmRouteService } from '@kbn/fleet-plugin/common';
import type { PutAgentReassignResponse } from '@kbn/fleet-plugin/common/types';
import { request } from './common';
export const getEndpointIntegrationVersion = (): Cypress.Chainable<string> =>
request<GetInfoResponse>({
url: epmRouteService.getInfoPath('endpoint'),
method: 'GET',
}).then((response) => response.body.item.version);
export const getAgentByHostName = (hostname: string): Cypress.Chainable<Agent> =>
request<GetAgentsResponse>({
url: agentRouteService.getListPath(),
method: 'GET',
qs: {
kuery: `local_metadata.host.hostname: "${hostname}"`,
},
}).then((response) => response.body.items[0]);
export const reassignAgentPolicy = (
agentId: string,
agentPolicyId: string
): Cypress.Chainable<Cypress.Response<PutAgentReassignResponse>> =>
request<PutAgentReassignResponse>({
url: agentRouteService.getReassignPath(agentId),
method: 'PUT',
body: {
policy_id: agentPolicyId,
},
});

View file

@ -8,17 +8,17 @@
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { BASE_ENDPOINT_ROUTE } from '../../../../common/endpoint/constants'; import { BASE_ENDPOINT_ROUTE } from '../../../../common/endpoint/constants';
import { runEndpointLoaderScript } from './run_endpoint_loader'; import { runEndpointLoaderScript } from './run_endpoint_loader';
import { request } from './common';
// Checks for Endpoint data and creates it if needed // Checks for Endpoint data and creates it if needed
export const loadEndpointDataForEventFiltersIfNeeded = () => { export const loadEndpointDataForEventFiltersIfNeeded = () => {
cy.request({ request({
method: 'POST', method: 'POST',
url: `${BASE_ENDPOINT_ROUTE}/suggestions/eventFilters`, url: `${BASE_ENDPOINT_ROUTE}/suggestions/eventFilters`,
body: { body: {
field: 'agent.type', field: 'agent.type',
query: '', query: '',
}, },
headers: { 'kbn-xsrf': 'kibana' },
failOnStatusCode: false, failOnStatusCode: false,
}).then(({ body }) => { }).then(({ body }) => {
if (isEmpty(body)) { if (isEmpty(body)) {

View file

@ -11,6 +11,7 @@ import * as yaml from 'js-yaml';
import type { UrlObject } from 'url'; import type { UrlObject } from 'url';
import Url from 'url'; import Url from 'url';
import type { Role } from '@kbn/security-plugin/common'; import type { Role } from '@kbn/security-plugin/common';
import { request } from './common';
import { getT1Analyst } from '../../../../scripts/endpoint/common/roles_users/t1_analyst'; import { getT1Analyst } from '../../../../scripts/endpoint/common/roles_users/t1_analyst';
import { getT2Analyst } from '../../../../scripts/endpoint/common/roles_users/t2_analyst'; import { getT2Analyst } from '../../../../scripts/endpoint/common/roles_users/t2_analyst';
import { getHunter } from '../../../../scripts/endpoint/common/roles_users/hunter'; import { getHunter } from '../../../../scripts/endpoint/common/roles_users/hunter';
@ -79,13 +80,6 @@ const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD';
*/ */
const LOGIN_API_ENDPOINT = '/internal/security/login'; const LOGIN_API_ENDPOINT = '/internal/security/login';
const API_AUTH = {
user: Cypress.env(ELASTICSEARCH_USERNAME),
pass: Cypress.env(ELASTICSEARCH_PASSWORD),
};
const API_HEADERS = { 'kbn-xsrf': 'cypress' };
/** /**
* cy.visit will default to the baseUrl which uses the default kibana test user * cy.visit will default to the baseUrl which uses the default kibana test user
* This function will override that functionality in cy.visit by building the baseUrl * This function will override that functionality in cy.visit by building the baseUrl
@ -151,52 +145,40 @@ export const createRoleAndUser = (role: ROLE) => {
}; };
export const createCustomRoleAndUser = (role: string, rolePrivileges: Omit<Role, 'name'>) => { export const createCustomRoleAndUser = (role: string, rolePrivileges: Omit<Role, 'name'>) => {
const env = getCurlScriptEnvVars();
// post the role // post the role
cy.request({ request({
method: 'PUT', method: 'PUT',
url: `${env.KIBANA_URL}/api/security/role/${role}`, url: `/api/security/role/${role}`,
body: rolePrivileges, body: rolePrivileges,
headers: API_HEADERS,
auth: API_AUTH,
}); });
// post the user associated with the role to elasticsearch // post the user associated with the role to elasticsearch
cy.request({ request({
method: 'POST', method: 'POST',
url: `${env.KIBANA_URL}/internal/security/users/${role}`, url: `/internal/security/users/${role}`,
headers: API_HEADERS,
body: { body: {
username: role, username: role,
password: Cypress.env(ELASTICSEARCH_PASSWORD), password: Cypress.env(ELASTICSEARCH_PASSWORD),
roles: [role], roles: [role],
}, },
auth: API_AUTH,
}); });
}; };
export const deleteRoleAndUser = (role: ROLE) => { export const deleteRoleAndUser = (role: ROLE) => {
const env = getCurlScriptEnvVars(); request({
cy.request({
method: 'DELETE', method: 'DELETE',
auth: API_AUTH, url: `/internal/security/users/${role}`,
headers: API_HEADERS,
url: `${env.KIBANA_URL}/internal/security/users/${role}`,
}); });
cy.request({ request({
method: 'DELETE', method: 'DELETE',
auth: API_AUTH, url: `/api/security/role/${role}`,
headers: API_HEADERS,
url: `${env.KIBANA_URL}/api/security/role/${role}`,
}); });
}; };
export const loginWithUser = (user: User) => { export const loginWithUser = (user: User) => {
const url = Cypress.config().baseUrl; const url = Cypress.config().baseUrl;
cy.request({ request({
body: { body: {
providerType: 'basic', providerType: 'basic',
providerName: url && !url.includes('localhost') ? 'cloud-basic' : 'basic', providerName: url && !url.includes('localhost') ? 'cloud-basic' : 'basic',
@ -227,7 +209,7 @@ export const loginWithCustomRole = async (role: string, rolePrivileges: Omit<Rol
port: Cypress.env('configport'), port: Cypress.env('configport'),
} as UrlObject); } as UrlObject);
cy.log(`origin: ${theUrl}`); cy.log(`origin: ${theUrl}`);
cy.request({ request({
body: { body: {
providerType: 'basic', providerType: 'basic',
providerName: 'basic', providerName: 'basic',
@ -282,7 +264,7 @@ const loginViaEnvironmentCredentials = () => {
); );
// programmatically authenticate without interacting with the Kibana login page // programmatically authenticate without interacting with the Kibana login page
cy.request({ request({
body: { body: {
providerType: 'basic', providerType: 'basic',
providerName: url && !url.includes('localhost') ? 'cloud-basic' : 'basic', providerName: url && !url.includes('localhost') ? 'cloud-basic' : 'basic',
@ -313,7 +295,7 @@ const loginViaConfig = () => {
const config = yaml.safeLoad(kibanaDevYml); const config = yaml.safeLoad(kibanaDevYml);
// programmatically authenticate without interacting with the Kibana login page // programmatically authenticate without interacting with the Kibana login page
cy.request({ request({
body: { body: {
providerType: 'basic', providerType: 'basic',
providerName: 'basic', providerName: 'basic',

View file

@ -25,7 +25,7 @@ const performAction = (action: FormAction) => {
if (action.customSelector) { if (action.customSelector) {
element = cy.get(action.customSelector); element = cy.get(action.customSelector);
} else { } else {
element = cy.getBySel(action.selector || ''); element = cy.getByTestSubj(action.selector || '');
} }
if (action.type === 'click') { if (action.type === 'click') {

View file

@ -1,7 +1,8 @@
{ {
"extends": "../../../../../../tsconfig.base.json", "extends": "../../../../../../tsconfig.base.json",
"include": [ "include": [
"**/*" "**/*",
"../cypress_endpoint.config.ts"
], ],
"exclude": [ "exclude": [
"target/**/*" "target/**/*"
@ -26,5 +27,6 @@
"@kbn/securitysolution-list-constants", "@kbn/securitysolution-list-constants",
"@kbn/fleet-plugin", "@kbn/fleet-plugin",
"@kbn/securitysolution-io-ts-list-types", "@kbn/securitysolution-io-ts-list-types",
"@kbn/cypress-config",
] ]
} }

View file

@ -6,6 +6,8 @@
*/ */
import { defineCypressConfig } from '@kbn/cypress-config'; import { defineCypressConfig } from '@kbn/cypress-config';
// eslint-disable-next-line @kbn/imports/no_boundary_crossing
import { dataLoaders } from './cypress/support/data_loaders';
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
export default defineCypressConfig({ export default defineCypressConfig({
@ -37,5 +39,8 @@ export default defineCypressConfig({
supportFile: 'public/management/cypress/support/e2e.ts', supportFile: 'public/management/cypress/support/e2e.ts',
specPattern: 'public/management/cypress/e2e/endpoint/*.cy.{js,jsx,ts,tsx}', specPattern: 'public/management/cypress/e2e/endpoint/*.cy.{js,jsx,ts,tsx}',
experimentalRunAllSpecs: true, experimentalRunAllSpecs: true,
setupNodeEvents(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) {
dataLoaders(on, config);
},
}, },
}); });

View file

@ -12,7 +12,7 @@ import { useUserPrivileges } from '../../../../../common/components/user_privile
import { useWithShowEndpointResponder } from '../../../../hooks'; import { useWithShowEndpointResponder } from '../../../../hooks';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { APP_UI_ID } from '../../../../../../common/constants'; import { APP_UI_ID } from '../../../../../../common/constants';
import { getEndpointDetailsPath } from '../../../../common/routing'; import { getEndpointDetailsPath, getEndpointListPath } from '../../../../common/routing';
import type { HostMetadata, MaybeImmutable } from '../../../../../../common/endpoint/types'; import type { HostMetadata, MaybeImmutable } from '../../../../../../common/endpoint/types';
import { useEndpointSelector } from './hooks'; import { useEndpointSelector } from './hooks';
import { agentPolicies, uiQueryParams } from '../../store/selectors'; import { agentPolicies, uiQueryParams } from '../../store/selectors';
@ -236,6 +236,12 @@ export const useEndpointActionItems = (
agentId: fleetAgentId, agentId: fleetAgentId,
})[1] })[1]
}?openReassignFlyout=true`, }?openReassignFlyout=true`,
state: {
onDoneNavigateTo: [
APP_UI_ID,
{ path: getEndpointListPath({ name: 'endpointList' }) },
],
},
}, },
href: `${getAppUrl({ appId: 'fleet' })}${ href: `${getAppUrl({ appId: 'fleet' })}${
pagePathGetters.agent_details({ pagePathGetters.agent_details({

View file

@ -17,6 +17,7 @@
"exclude": [ "exclude": [
"target/**/*", "target/**/*",
"**/cypress/**", "**/cypress/**",
"public/management/cypress_endpoint.config.ts",
], ],
"kbn_references": [ "kbn_references": [
"@kbn/core", "@kbn/core",

View file

@ -21,6 +21,8 @@ export class AgentManager extends Manager {
public async setup() { public async setup() {
this.vmName = await enrollEndpointHost(); this.vmName = await enrollEndpointHost();
return this.vmName;
} }
public cleanup() { public cleanup() {

View file

@ -14,9 +14,11 @@ import { AgentManager } from './agent';
import { FleetManager } from './fleet_server'; import { FleetManager } from './fleet_server';
import { getLatestAvailableAgentVersion } from './utils'; import { getLatestAvailableAgentVersion } from './utils';
type RunnerEnv = Record<string, string | undefined>;
async function withFleetAgent( async function withFleetAgent(
{ getService }: FtrProviderContext, { getService }: FtrProviderContext,
runner: (runnerEnv: Record<string, string>) => Promise<void> runner: (runnerEnv: RunnerEnv) => Promise<void>
) { ) {
const log = getService('log'); const log = getService('log');
const config = getService('config'); const config = getService('config');
@ -40,9 +42,9 @@ async function withFleetAgent(
const agentManager = new AgentManager(log); const agentManager = new AgentManager(log);
await fleetManager.setup(); await fleetManager.setup();
await agentManager.setup(); const agentVmName = await agentManager.setup();
try { try {
await runner({}); await runner({ agentVmName });
} finally { } finally {
agentManager.cleanup(); agentManager.cleanup();
fleetManager.cleanup(); fleetManager.cleanup();
@ -58,10 +60,16 @@ export async function DefendWorkflowsCypressVisualTestRunner(context: FtrProvide
} }
export async function DefendWorkflowsCypressEndpointTestRunner(context: FtrProviderContext) { export async function DefendWorkflowsCypressEndpointTestRunner(context: FtrProviderContext) {
await withFleetAgent(context, () => startDefendWorkflowsCypress(context, 'dw:endpoint:open')); await withFleetAgent(context, (runnerEnv) =>
startDefendWorkflowsCypress(context, 'dw:endpoint:open', runnerEnv)
);
} }
function startDefendWorkflowsCypress(context: FtrProviderContext, cypressCommand: string) { function startDefendWorkflowsCypress(
context: FtrProviderContext,
cypressCommand: 'dw:endpoint:open' | 'dw:open' | 'dw:run',
runnerEnv?: RunnerEnv
) {
const log = context.getService('log'); const log = context.getService('log');
const config = context.getService('config'); const config = context.getService('config');
return withProcRunner(log, async (procs) => { return withProcRunner(log, async (procs) => {
@ -91,6 +99,7 @@ function startDefendWorkflowsCypress(context: FtrProviderContext, cypressCommand
hostname: config.get('servers.kibana.hostname'), hostname: config.get('servers.kibana.hostname'),
port: config.get('servers.kibana.port'), port: config.get('servers.kibana.port'),
}), }),
CYPRESS_ENDPOINT_VM_NAME: runnerEnv?.agentVmName,
...process.env, ...process.env,
}, },
wait: true, wait: true,

View file

@ -7,7 +7,7 @@
import axios from 'axios'; import axios from 'axios';
import semver from 'semver'; import semver from 'semver';
import { filter } from 'lodash'; import { map } from 'lodash';
import { KbnClient } from '@kbn/test'; import { KbnClient } from '@kbn/test';
/** /**
@ -20,9 +20,7 @@ export const getLatestAvailableAgentVersion = async (kbnClient: KbnClient): Prom
const kbnStatus = await kbnClient.status.get(); const kbnStatus = await kbnClient.status.get();
const agentVersions = await axios const agentVersions = await axios
.get('https://artifacts-api.elastic.co/v1/versions') .get('https://artifacts-api.elastic.co/v1/versions')
.then((response) => .then((response) => map(response.data.versions, (version) => version.split('-SNAPSHOT')[0]));
filter(response.data.versions, (versionString) => !versionString.includes('SNAPSHOT'))
);
let version = let version =
semver.maxSatisfying(agentVersions, `<=${kbnStatus.version.number}`) ?? semver.maxSatisfying(agentVersions, `<=${kbnStatus.version.number}`) ??