= ({
{
+ it('should return index 1 when no policies', () => {
+ const name = getMaxPackageName('apache', []);
+ expect(name).toEqual('apache-1');
+ });
+
+ it('should return index 1 when policies with other name', () => {
+ const name = getMaxPackageName('apache', [{ name: 'package' } as any]);
+ expect(name).toEqual('apache-1');
+ });
+
+ it('should return index 2 when policies 1 exists', () => {
+ const name = getMaxPackageName('apache', [{ name: 'apache-1' } as any]);
+ expect(name).toEqual('apache-2');
+ });
+
+ it('should return index 11 when policy 10 is max', () => {
+ const name = getMaxPackageName('apache', [
+ { name: 'apache-10' } as any,
+ { name: 'apache-9' } as any,
+ { name: 'package' } as any,
+ ]);
+ expect(name).toEqual('apache-11');
+ });
+
+ it('should return index 1 when policies undefined', () => {
+ const name = getMaxPackageName('apache');
+ expect(name).toEqual('apache-1');
+ });
+});
diff --git a/x-pack/plugins/fleet/common/services/max_package_name.ts b/x-pack/plugins/fleet/common/services/max_package_name.ts
new file mode 100644
index 0000000000000..6078d4e6b7bbf
--- /dev/null
+++ b/x-pack/plugins/fleet/common/services/max_package_name.ts
@@ -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.
+ */
+
+export function getMaxPackageName(packageName: string, packagePolicies?: Array<{ name: string }>) {
+ // Retrieve highest number appended to package policy name and increment it by one
+ const pkgPoliciesNamePattern = new RegExp(`${packageName}-(\\d+)`);
+
+ const maxPkgPolicyName = Math.max(
+ ...(packagePolicies ?? [])
+ .filter((ds) => Boolean(ds.name.match(pkgPoliciesNamePattern)))
+ .map((ds) => parseInt(ds.name.match(pkgPoliciesNamePattern)![1], 10)),
+ 0
+ );
+
+ return `${packageName}-${maxPkgPolicyName + 1}`;
+}
diff --git a/x-pack/plugins/fleet/cypress/integration/fleet_startup.spec.ts b/x-pack/plugins/fleet/cypress/integration/fleet_startup.spec.ts
index 83423d62e2a43..5c14ee1df6d4e 100644
--- a/x-pack/plugins/fleet/cypress/integration/fleet_startup.spec.ts
+++ b/x-pack/plugins/fleet/cypress/integration/fleet_startup.spec.ts
@@ -5,47 +5,17 @@
* 2.0.
*/
-import { AGENTS_TAB, AGENT_POLICIES_TAB, ENROLLMENT_TOKENS_TAB } from '../screens/fleet';
+import {
+ AGENTS_TAB,
+ ADD_AGENT_BUTTON_TOP,
+ AGENT_FLYOUT_CLOSE_BUTTON,
+ STANDALONE_TAB,
+} from '../screens/fleet';
import { cleanupAgentPolicies, unenrollAgent } from '../tasks/cleanup';
+import { verifyPolicy, verifyAgentPackage, navigateToTab } from '../tasks/fleet';
import { FLEET, navigateTo } from '../tasks/navigation';
describe('Fleet startup', () => {
- function navigateToTab(tab: string) {
- cy.getBySel(tab).click();
- cy.get('.euiBasicTable-loading').should('not.exist');
- }
-
- function navigateToAgentPolicy(name: string) {
- cy.get('.euiLink').contains(name).click();
- cy.get('.euiLoadingSpinner').should('not.exist');
- }
-
- function navigateToEnrollmentTokens() {
- cy.getBySel(ENROLLMENT_TOKENS_TAB).click();
- cy.get('.euiBasicTable-loading').should('not.exist');
- cy.get('.euiButtonIcon--danger'); // wait for trash icon
- }
-
- function verifyPolicy(name: string, integrations: string[]) {
- navigateToTab(AGENT_POLICIES_TAB);
-
- navigateToAgentPolicy(name);
- integrations.forEach((integration) => {
- cy.get('.euiLink').contains(integration);
- });
-
- cy.get('.euiButtonEmpty').contains('View all agent policies').click();
-
- navigateToEnrollmentTokens();
-
- cy.get('.euiTableCellContent').contains(name);
- }
-
- function verifyAgentPackage() {
- cy.visit('/app/integrations/installed');
- cy.getBySel('integration-card:epr:elastic_agent');
- }
-
// skipping Fleet Server enroll, to enable, comment out runner.ts line 23
describe.skip('Fleet Server', () => {
it('should display Add agent button and Healthy agent once Fleet Agent page loaded', () => {
@@ -77,8 +47,8 @@ describe('Fleet startup', () => {
});
it('should create agent policy', () => {
- cy.getBySel('addAgentBtnTop').click();
- cy.getBySel('standaloneTab').click();
+ cy.getBySel(ADD_AGENT_BUTTON_TOP).click();
+ cy.getBySel(STANDALONE_TAB).click();
cy.intercept('POST', '/api/fleet/agent_policies?sys_monitoring=true').as('createAgentPolicy');
@@ -97,7 +67,7 @@ describe('Fleet startup', () => {
// verify agent.yml code block has new policy id
cy.get('.euiCodeBlock__code').contains(`id: ${agentPolicyId}`);
- cy.getBySel('euiFlyoutCloseButton').click();
+ cy.getBySel(AGENT_FLYOUT_CLOSE_BUTTON).click();
// verify policy is created and has system package
verifyPolicy('Agent policy 1', ['System']);
diff --git a/x-pack/plugins/fleet/cypress/integration/integrations_mock.spec.ts b/x-pack/plugins/fleet/cypress/integration/integrations_mock.spec.ts
index 080b01458e18f..1b969e1a8ca2e 100644
--- a/x-pack/plugins/fleet/cypress/integration/integrations_mock.spec.ts
+++ b/x-pack/plugins/fleet/cypress/integration/integrations_mock.spec.ts
@@ -7,6 +7,7 @@
import { navigateTo } from '../tasks/navigation';
import { UPDATE_PACKAGE_BTN } from '../screens/integrations';
+import { AGENT_POLICY_SAVE_INTEGRATION } from '../screens/fleet';
describe('Add Integration - Mock API', () => {
describe('upgrade package and upgrade package policy', () => {
@@ -141,7 +142,7 @@ describe('Add Integration - Mock API', () => {
);
cy.getBySel('toastCloseButton').click();
- cy.getBySel('saveIntegration').click();
+ cy.getBySel(AGENT_POLICY_SAVE_INTEGRATION).click();
cy.wait('@updateApachePolicy').then((interception) => {
expect(interception.request.body.package.version).to.equal(newVersion);
diff --git a/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts b/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts
index ffb0f14c97a7f..e06b3d3ed5670 100644
--- a/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts
+++ b/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts
@@ -24,6 +24,7 @@ import {
SETTINGS_TAB,
UPDATE_PACKAGE_BTN,
} from '../screens/integrations';
+import { ADD_PACKAGE_POLICY_BTN } from '../screens/fleet';
import { cleanupAgentPolicies } from '../tasks/cleanup';
describe('Add Integration - Real API', () => {
@@ -75,7 +76,7 @@ describe('Add Integration - Real API', () => {
cy.visit(`/app/fleet/policies/${agentPolicyId}`);
cy.intercept('GET', '/api/fleet/epm/packages?*').as('packages');
- cy.getBySel('addPackagePolicyButton').click();
+ cy.getBySel(ADD_PACKAGE_POLICY_BTN).click();
cy.wait('@packages');
cy.get('.euiLoadingSpinner').should('not.exist');
cy.get('input[placeholder="Search for integrations"]').type('Apache');
diff --git a/x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_none.spec.ts b/x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_none.spec.ts
new file mode 100644
index 0000000000000..f9ae802d3b426
--- /dev/null
+++ b/x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_none.spec.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { FLEET } from '../tasks/navigation';
+import {
+ createUsersAndRoles,
+ FleetAllIntegrNoneRole,
+ FleetAllIntegrNoneUser,
+ deleteUsersAndRoles,
+} from '../tasks/privileges';
+import { loginWithUserAndWaitForPage, logout } from '../tasks/login';
+
+import { MISSING_PRIVILEGES_TITLE, MISSING_PRIVILEGES_MESSAGE } from '../screens/fleet';
+const rolesToCreate = [FleetAllIntegrNoneRole];
+const usersToCreate = [FleetAllIntegrNoneUser];
+
+describe('When the user has All privilege for Fleet but None for integrations', () => {
+ before(() => {
+ createUsersAndRoles(usersToCreate, rolesToCreate);
+ });
+
+ afterEach(() => {
+ logout();
+ });
+
+ after(() => {
+ deleteUsersAndRoles(usersToCreate, rolesToCreate);
+ });
+
+ it('Fleet access is blocked with a callout', () => {
+ loginWithUserAndWaitForPage(FLEET, FleetAllIntegrNoneUser);
+ cy.getBySel(MISSING_PRIVILEGES_TITLE).should('have.text', 'Permission denied');
+ cy.getBySel(MISSING_PRIVILEGES_MESSAGE).should(
+ 'contain',
+ 'You are not authorized to access Fleet.'
+ );
+ });
+});
diff --git a/x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_read.spec.ts b/x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_read.spec.ts
new file mode 100644
index 0000000000000..327ba39e65377
--- /dev/null
+++ b/x-pack/plugins/fleet/cypress/integration/privileges_fleet_all_integrations_read.spec.ts
@@ -0,0 +1,88 @@
+/*
+ * 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 { FLEET, INTEGRATIONS, navigateTo } from '../tasks/navigation';
+import {
+ createUsersAndRoles,
+ FleetAllIntegrReadRole,
+ FleetAllIntegrReadUser,
+ deleteUsersAndRoles,
+} from '../tasks/privileges';
+import { loginWithUserAndWaitForPage, logout } from '../tasks/login';
+import { navigateToTab, createAgentPolicy } from '../tasks/fleet';
+import { cleanupAgentPolicies, unenrollAgent } from '../tasks/cleanup';
+
+import {
+ FLEET_SERVER_MISSING_PRIVILEGES_TITLE,
+ FLEET_SERVER_MISSING_PRIVILEGES_MESSAGE,
+ ADD_AGENT_BUTTON_TOP,
+ AGENT_POLICIES_TAB,
+ AGENT_POLICY_SAVE_INTEGRATION,
+ ADD_PACKAGE_POLICY_BTN,
+} from '../screens/fleet';
+import { ADD_POLICY_BTN, AGENT_POLICY_NAME_LINK } from '../screens/integrations';
+
+const rolesToCreate = [FleetAllIntegrReadRole];
+const usersToCreate = [FleetAllIntegrReadUser];
+
+describe('When the user has All privilege for Fleet but Read for integrations', () => {
+ before(() => {
+ createUsersAndRoles(usersToCreate, rolesToCreate);
+ });
+
+ after(() => {
+ deleteUsersAndRoles(usersToCreate, rolesToCreate);
+ });
+
+ afterEach(() => {
+ logout();
+ });
+
+ describe('When there are agent policies', () => {
+ before(() => {
+ navigateTo(FLEET);
+ createAgentPolicy();
+ });
+
+ it('Some elements in the UI are not enabled', () => {
+ logout();
+ loginWithUserAndWaitForPage(FLEET, FleetAllIntegrReadUser);
+ navigateToTab(AGENT_POLICIES_TAB);
+
+ cy.getBySel(AGENT_POLICY_NAME_LINK).click();
+ cy.getBySel(ADD_PACKAGE_POLICY_BTN).should('be.disabled');
+
+ cy.get('a[title="system-1"]').click();
+ cy.getBySel(AGENT_POLICY_SAVE_INTEGRATION).should('be.disabled');
+ });
+
+ after(() => {
+ unenrollAgent();
+ cleanupAgentPolicies();
+ });
+ });
+
+ describe('When there are no agent policies', () => {
+ it('If fleet server is not set up, Fleet shows a callout', () => {
+ loginWithUserAndWaitForPage(FLEET, FleetAllIntegrReadUser);
+ cy.getBySel(FLEET_SERVER_MISSING_PRIVILEGES_TITLE).should('have.text', 'Permission denied');
+ cy.getBySel(FLEET_SERVER_MISSING_PRIVILEGES_MESSAGE).should(
+ 'contain',
+ 'Fleet Server needs to be set up.'
+ );
+ cy.getBySel(ADD_AGENT_BUTTON_TOP).should('not.be.disabled');
+ });
+ });
+
+ describe('Integrations', () => {
+ it('are visible but cannot be added', () => {
+ loginWithUserAndWaitForPage(INTEGRATIONS, FleetAllIntegrReadUser);
+ cy.getBySel('integration-card:epr:apache').click();
+ cy.getBySel(ADD_POLICY_BTN).should('be.disabled');
+ });
+ });
+});
diff --git a/x-pack/plugins/fleet/cypress/integration/privileges_fleet_none_integrations_all.spec.ts b/x-pack/plugins/fleet/cypress/integration/privileges_fleet_none_integrations_all.spec.ts
new file mode 100644
index 0000000000000..68fcecb76de21
--- /dev/null
+++ b/x-pack/plugins/fleet/cypress/integration/privileges_fleet_none_integrations_all.spec.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { INTEGRATIONS } from '../tasks/navigation';
+import {
+ createUsersAndRoles,
+ FleetNoneIntegrAllRole,
+ FleetNoneIntegrAllUser,
+ deleteUsersAndRoles,
+} from '../tasks/privileges';
+import { loginWithUserAndWaitForPage, logout } from '../tasks/login';
+
+import { ADD_POLICY_BTN } from '../screens/integrations';
+
+const rolesToCreate = [FleetNoneIntegrAllRole];
+const usersToCreate = [FleetNoneIntegrAllUser];
+
+describe('When the user has All privileges for Integrations but None for for Fleet', () => {
+ before(() => {
+ createUsersAndRoles(usersToCreate, rolesToCreate);
+ });
+
+ afterEach(() => {
+ logout();
+ });
+
+ after(() => {
+ deleteUsersAndRoles(usersToCreate, rolesToCreate);
+ });
+
+ it('Integrations are visible but cannot be added', () => {
+ loginWithUserAndWaitForPage(INTEGRATIONS, FleetNoneIntegrAllUser);
+ cy.getBySel('integration-card:epr:apache').click();
+ cy.getBySel(ADD_POLICY_BTN).should('be.disabled');
+ });
+});
diff --git a/x-pack/plugins/fleet/cypress/screens/fleet.ts b/x-pack/plugins/fleet/cypress/screens/fleet.ts
index 4c0bb7cea161e..32ecdc4f5da71 100644
--- a/x-pack/plugins/fleet/cypress/screens/fleet.ts
+++ b/x-pack/plugins/fleet/cypress/screens/fleet.ts
@@ -6,8 +6,19 @@
*/
export const ADD_AGENT_BUTTON = 'addAgentButton';
+export const ADD_AGENT_BUTTON_TOP = 'addAgentBtnTop';
+export const CREATE_POLICY_BUTTON = 'createPolicyBtn';
+export const AGENT_FLYOUT_CLOSE_BUTTON = 'euiFlyoutCloseButton';
export const AGENTS_TAB = 'fleet-agents-tab';
export const AGENT_POLICIES_TAB = 'fleet-agent-policies-tab';
export const ENROLLMENT_TOKENS_TAB = 'fleet-enrollment-tokens-tab';
export const SETTINGS_TAB = 'fleet-settings-tab';
+export const STANDALONE_TAB = 'standaloneTab';
+export const MISSING_PRIVILEGES_TITLE = 'missingPrivilegesPromptTitle';
+export const MISSING_PRIVILEGES_MESSAGE = 'missingPrivilegesPromptMessage';
+export const FLEET_SERVER_MISSING_PRIVILEGES_MESSAGE = 'fleetServerMissingPrivilegesMessage';
+export const FLEET_SERVER_MISSING_PRIVILEGES_TITLE = 'fleetServerMissingPrivilegesTitle';
+export const AGENT_POLICY_SAVE_INTEGRATION = 'saveIntegration';
+export const PACKAGE_POLICY_TABLE_LINK = 'PackagePoliciesTableLink';
+export const ADD_PACKAGE_POLICY_BTN = 'addPackagePolicyButton';
diff --git a/x-pack/plugins/fleet/cypress/screens/integrations.ts b/x-pack/plugins/fleet/cypress/screens/integrations.ts
index 3c980723cc4df..dddede9e77f8d 100644
--- a/x-pack/plugins/fleet/cypress/screens/integrations.ts
+++ b/x-pack/plugins/fleet/cypress/screens/integrations.ts
@@ -11,6 +11,7 @@ export const INTEGRATIONS_CARD = '.euiCard__titleAnchor';
export const INTEGRATION_NAME_LINK = 'integrationNameLink';
export const AGENT_POLICY_NAME_LINK = 'agentPolicyNameLink';
+export const AGENT_ACTIONS_BTN = 'agentActionsBtn';
export const CONFIRM_MODAL_BTN = 'confirmModalConfirmButton';
export const CONFIRM_MODAL_BTN_SEL = `[data-test-subj=${CONFIRM_MODAL_BTN}]`;
@@ -19,6 +20,7 @@ export const FLYOUT_CLOSE_BTN_SEL = '[data-test-subj="euiFlyoutCloseButton"]';
export const SETTINGS_TAB = 'tab-settings';
export const POLICIES_TAB = 'tab-policies';
+export const ADVANCED_TAB = 'tab-custom';
export const UPDATE_PACKAGE_BTN = 'updatePackageBtn';
export const LATEST_VERSION = 'latestVersion';
diff --git a/x-pack/plugins/fleet/cypress/screens/navigation.ts b/x-pack/plugins/fleet/cypress/screens/navigation.ts
index fee38161b6b2b..76b73711db495 100644
--- a/x-pack/plugins/fleet/cypress/screens/navigation.ts
+++ b/x-pack/plugins/fleet/cypress/screens/navigation.ts
@@ -5,4 +5,5 @@
* 2.0.
*/
-export const TOGGLE_NAVIGATION_BTN = '[data-test-subj="toggleNavButton"]';
+export const TOGGLE_NAVIGATION_BTN = 'toggleNavButton';
+export const NAV_APP_LINK = 'collapsibleNavAppLink';
diff --git a/x-pack/plugins/fleet/cypress/tasks/fleet.ts b/x-pack/plugins/fleet/cypress/tasks/fleet.ts
new file mode 100644
index 0000000000000..304ab7445d4e4
--- /dev/null
+++ b/x-pack/plugins/fleet/cypress/tasks/fleet.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 {
+ AGENT_POLICIES_TAB,
+ ENROLLMENT_TOKENS_TAB,
+ ADD_AGENT_BUTTON_TOP,
+ CREATE_POLICY_BUTTON,
+ AGENT_FLYOUT_CLOSE_BUTTON,
+ STANDALONE_TAB,
+} from '../screens/fleet';
+
+export function createAgentPolicy() {
+ cy.getBySel(ADD_AGENT_BUTTON_TOP).click();
+ cy.getBySel(STANDALONE_TAB).click();
+ cy.getBySel(CREATE_POLICY_BUTTON).click();
+ cy.getBySel('agentPolicyCreateStatusCallOut').contains('Agent policy created');
+ cy.getBySel(AGENT_FLYOUT_CLOSE_BUTTON).click();
+}
+
+export function navigateToTab(tab: string) {
+ cy.getBySel(tab).click();
+ cy.get('.euiBasicTable-loading').should('not.exist');
+}
+
+export function navigateToAgentPolicy(name: string) {
+ cy.get('.euiLink').contains(name).click();
+ cy.get('.euiLoadingSpinner').should('not.exist');
+}
+
+export function navigateToEnrollmentTokens() {
+ cy.getBySel(ENROLLMENT_TOKENS_TAB).click();
+ cy.get('.euiBasicTable-loading').should('not.exist');
+ cy.get('.euiButtonIcon--danger'); // wait for trash icon
+}
+
+export function verifyPolicy(name: string, integrations: string[]) {
+ navigateToTab(AGENT_POLICIES_TAB);
+
+ navigateToAgentPolicy(name);
+ integrations.forEach((integration) => {
+ cy.get('.euiLink').contains(integration);
+ });
+
+ cy.get('.euiButtonEmpty').contains('View all agent policies').click();
+
+ navigateToEnrollmentTokens();
+
+ cy.get('.euiTableCellContent').contains(name);
+}
+
+export function verifyAgentPackage() {
+ cy.visit('/app/integrations/installed');
+ cy.getBySel('integration-card:epr:elastic_agent');
+}
diff --git a/x-pack/plugins/fleet/cypress/tasks/login.ts b/x-pack/plugins/fleet/cypress/tasks/login.ts
new file mode 100644
index 0000000000000..2df7b88f1607b
--- /dev/null
+++ b/x-pack/plugins/fleet/cypress/tasks/login.ts
@@ -0,0 +1,341 @@
+/*
+ * 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 Url from 'url';
+import type { UrlObject } from 'url';
+
+import * as yaml from 'js-yaml';
+
+import type { ROLES } from './privileges';
+import { hostDetailsUrl, LOGOUT_URL } from './navigation';
+
+/**
+ * Credentials in the `kibana.dev.yml` config file will be used to authenticate
+ * with Kibana when credentials are not provided via environment variables
+ */
+const KIBANA_DEV_YML_PATH = '../../../config/kibana.dev.yml';
+
+/**
+ * The configuration path in `kibana.dev.yml` to the username to be used when
+ * authenticating with Kibana.
+ */
+const ELASTICSEARCH_USERNAME_CONFIG_PATH = 'config.elasticsearch.username';
+
+/**
+ * The configuration path in `kibana.dev.yml` to the password to be used when
+ * authenticating with Kibana.
+ */
+const ELASTICSEARCH_PASSWORD_CONFIG_PATH = 'config.elasticsearch.password';
+
+/**
+ * The `CYPRESS_ELASTICSEARCH_USERNAME` environment variable specifies the
+ * username to be used when authenticating with Kibana
+ */
+const ELASTICSEARCH_USERNAME = 'ELASTICSEARCH_USERNAME';
+
+/**
+ * The `CYPRESS_ELASTICSEARCH_PASSWORD` environment variable specifies the
+ * username to be used when authenticating with Kibana
+ */
+const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD';
+
+/**
+ * The Kibana server endpoint used for authentication
+ */
+const LOGIN_API_ENDPOINT = '/internal/security/login';
+
+/**
+ * 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
+ * directly from the environment variables set up in x-pack/test/security_solution_cypress/runner.ts
+ *
+ * @param role string role/user to log in with
+ * @param route string route to visit
+ */
+export const getUrlWithRoute = (role: ROLES, route: string) => {
+ const url = Cypress.config().baseUrl;
+ const kibana = new URL(String(url));
+ const theUrl = `${Url.format({
+ auth: `${role}:changeme`,
+ username: role,
+ password: 'changeme',
+ protocol: kibana.protocol.replace(':', ''),
+ hostname: kibana.hostname,
+ port: kibana.port,
+ } as UrlObject)}${route.startsWith('/') ? '' : '/'}${route}`;
+ cy.log(`origin: ${theUrl}`);
+ return theUrl;
+};
+
+interface User {
+ username: string;
+ password: string;
+}
+
+/**
+ * Builds a URL with basic auth using the passed in user.
+ *
+ * @param user the user information to build the basic auth with
+ * @param route string route to visit
+ */
+export const constructUrlWithUser = (user: User, route: string) => {
+ const url = Cypress.config().baseUrl;
+ const kibana = new URL(String(url));
+ const hostname = kibana.hostname;
+ const username = user.username;
+ const password = user.password;
+ const protocol = kibana.protocol.replace(':', '');
+ const port = kibana.port;
+
+ const path = `${route.startsWith('/') ? '' : '/'}${route}`;
+ const strUrl = `${protocol}://${username}:${password}@${hostname}:${port}${path}`;
+ const builtUrl = new URL(strUrl);
+
+ cy.log(`origin: ${builtUrl.href}`);
+ return builtUrl.href;
+};
+
+export const getCurlScriptEnvVars = () => ({
+ ELASTICSEARCH_URL: Cypress.env('ELASTICSEARCH_URL'),
+ ELASTICSEARCH_USERNAME: Cypress.env('ELASTICSEARCH_USERNAME'),
+ ELASTICSEARCH_PASSWORD: Cypress.env('ELASTICSEARCH_PASSWORD'),
+ KIBANA_URL: Cypress.config().baseUrl,
+});
+
+export const postRoleAndUser = (role: ROLES) => {
+ const env = getCurlScriptEnvVars();
+ const detectionsRoleScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/post_detections_role.sh`;
+ const detectionsRoleJsonPath = `./server/lib/detection_engine/scripts/roles_users/${role}/detections_role.json`;
+ const detectionsUserScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/post_detections_user.sh`;
+ const detectionsUserJsonPath = `./server/lib/detection_engine/scripts/roles_users/${role}/detections_user.json`;
+
+ // post the role
+ cy.exec(`bash ${detectionsRoleScriptPath} ${detectionsRoleJsonPath}`, {
+ env,
+ });
+
+ // post the user associated with the role to elasticsearch
+ cy.exec(`bash ${detectionsUserScriptPath} ${detectionsUserJsonPath}`, {
+ env,
+ });
+};
+
+export const deleteRoleAndUser = (role: ROLES) => {
+ const env = getCurlScriptEnvVars();
+ const detectionsUserDeleteScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/delete_detections_user.sh`;
+
+ // delete the role
+ cy.exec(`bash ${detectionsUserDeleteScriptPath}`, {
+ env,
+ });
+};
+
+export const loginWithUser = (user: User) => {
+ cy.request({
+ body: {
+ providerType: 'basic',
+ providerName: 'basic',
+ currentURL: '/',
+ params: {
+ username: user.username,
+ password: user.password,
+ },
+ },
+ headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
+ method: 'POST',
+ url: constructUrlWithUser(user, LOGIN_API_ENDPOINT),
+ });
+};
+
+export const loginWithRole = async (role: ROLES) => {
+ postRoleAndUser(role);
+ const theUrl = Url.format({
+ auth: `${role}:changeme`,
+ username: role,
+ password: 'changeme',
+ protocol: Cypress.env('protocol'),
+ hostname: Cypress.env('hostname'),
+ port: Cypress.env('configport'),
+ } as UrlObject);
+ cy.log(`origin: ${theUrl}`);
+ cy.request({
+ body: {
+ providerType: 'basic',
+ providerName: 'basic',
+ currentURL: '/',
+ params: {
+ username: role,
+ password: 'changeme',
+ },
+ },
+ headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
+ method: 'POST',
+ url: getUrlWithRoute(role, LOGIN_API_ENDPOINT),
+ });
+};
+
+/**
+ * Authenticates with Kibana using, if specified, credentials specified by
+ * environment variables. The credentials in `kibana.dev.yml` will be used
+ * for authentication when the environment variables are unset.
+ *
+ * To speed the execution of tests, prefer this non-interactive authentication,
+ * which is faster than authentication via Kibana's interactive login page.
+ */
+export const login = (role?: ROLES) => {
+ if (role != null) {
+ loginWithRole(role);
+ } else if (credentialsProvidedByEnvironment()) {
+ loginViaEnvironmentCredentials();
+ } else {
+ loginViaConfig();
+ }
+};
+
+/**
+ * Returns `true` if the credentials used to login to Kibana are provided
+ * via environment variables
+ */
+const credentialsProvidedByEnvironment = (): boolean =>
+ Cypress.env(ELASTICSEARCH_USERNAME) != null && Cypress.env(ELASTICSEARCH_PASSWORD) != null;
+
+/**
+ * Authenticates with Kibana by reading credentials from the
+ * `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD`
+ * environment variables, and POSTing the username and password directly to
+ * Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed).
+ */
+const loginViaEnvironmentCredentials = () => {
+ cy.log(
+ `Authenticating via environment credentials from the \`CYPRESS_${ELASTICSEARCH_USERNAME}\` and \`CYPRESS_${ELASTICSEARCH_PASSWORD}\` environment variables`
+ );
+
+ // programmatically authenticate without interacting with the Kibana login page
+ cy.request({
+ body: {
+ providerType: 'basic',
+ providerName: 'basic',
+ currentURL: '/',
+ params: {
+ username: Cypress.env(ELASTICSEARCH_USERNAME),
+ password: Cypress.env(ELASTICSEARCH_PASSWORD),
+ },
+ },
+ headers: { 'kbn-xsrf': 'cypress-creds-via-env' },
+ method: 'POST',
+ url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`,
+ });
+};
+
+/**
+ * Authenticates with Kibana by reading credentials from the
+ * `kibana.dev.yml` file and POSTing the username and password directly to
+ * Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed).
+ */
+const loginViaConfig = () => {
+ cy.log(
+ `Authenticating via config credentials \`${ELASTICSEARCH_USERNAME_CONFIG_PATH}\` and \`${ELASTICSEARCH_PASSWORD_CONFIG_PATH}\` from \`${KIBANA_DEV_YML_PATH}\``
+ );
+
+ // read the login details from `kibana.dev.yaml`
+ cy.readFile(KIBANA_DEV_YML_PATH).then((kibanaDevYml) => {
+ const config = yaml.safeLoad(kibanaDevYml);
+
+ // programmatically authenticate without interacting with the Kibana login page
+ cy.request({
+ body: {
+ providerType: 'basic',
+ providerName: 'basic',
+ currentURL: '/',
+ params: {
+ username: config.elasticsearch.username,
+ password: config.elasticsearch.password,
+ },
+ },
+ headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
+ method: 'POST',
+ url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`,
+ });
+ });
+};
+
+/**
+ * Get the configured auth details that were used to spawn cypress
+ *
+ * @returns the default Elasticsearch username and password for this environment
+ */
+export const getEnvAuth = (): User => {
+ if (credentialsProvidedByEnvironment()) {
+ return {
+ username: Cypress.env(ELASTICSEARCH_USERNAME),
+ password: Cypress.env(ELASTICSEARCH_PASSWORD),
+ };
+ } else {
+ let user: User = { username: '', password: '' };
+ cy.readFile(KIBANA_DEV_YML_PATH).then((devYml) => {
+ const config = yaml.safeLoad(devYml);
+ user = { username: config.elasticsearch.username, password: config.elasticsearch.password };
+ });
+
+ return user;
+ }
+};
+
+/**
+ * Authenticates with Kibana, visits the specified `url`, and waits for the
+ * Kibana global nav to be displayed before continuing
+ */
+export const loginAndWaitForPage = (
+ url: string,
+ role?: ROLES,
+ onBeforeLoadCallback?: (win: Cypress.AUTWindow) => void
+) => {
+ login(role);
+ cy.visit(
+ `${url}?timerange=(global:(linkTo:!(timeline),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)))`,
+ {
+ onBeforeLoad(win) {
+ if (onBeforeLoadCallback) {
+ onBeforeLoadCallback(win);
+ }
+ },
+ }
+ );
+ cy.get('[data-test-subj="headerGlobalNav"]');
+};
+export const waitForPage = (url: string) => {
+ cy.visit(
+ `${url}?timerange=(global:(linkTo:!(timeline),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)))`
+ );
+ cy.get('[data-test-subj="headerGlobalNav"]');
+};
+
+export const loginAndWaitForPageWithoutDateRange = (url: string, role?: ROLES) => {
+ login(role);
+ cy.visit(role ? getUrlWithRoute(role, url) : url);
+ cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
+};
+
+export const loginWithUserAndWaitForPage = (url: string, user: User) => {
+ loginWithUser(user);
+ cy.visit(constructUrlWithUser(user, url));
+ cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
+};
+
+export const loginAndWaitForHostDetailsPage = (hostName = 'suricata-iowa') => {
+ loginAndWaitForPage(hostDetailsUrl(hostName));
+ cy.get('[data-test-subj="loading-spinner"]', { timeout: 12000 }).should('not.exist');
+};
+
+export const waitForPageWithoutDateRange = (url: string, role?: ROLES) => {
+ cy.visit(role ? getUrlWithRoute(role, url) : url);
+ cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
+};
+
+export const logout = () => {
+ cy.visit(LOGOUT_URL);
+};
diff --git a/x-pack/plugins/fleet/cypress/tasks/navigation.ts b/x-pack/plugins/fleet/cypress/tasks/navigation.ts
index a2dd131b647a6..741a2cf761e8c 100644
--- a/x-pack/plugins/fleet/cypress/tasks/navigation.ts
+++ b/x-pack/plugins/fleet/cypress/tasks/navigation.ts
@@ -5,15 +5,16 @@
* 2.0.
*/
-import { TOGGLE_NAVIGATION_BTN } from '../screens/navigation';
-
export const INTEGRATIONS = 'app/integrations#/';
export const FLEET = 'app/fleet/';
+export const LOGIN_API_ENDPOINT = '/internal/security/login';
+export const LOGOUT_API_ENDPOINT = '/api/security/logout';
+export const LOGIN_URL = '/login';
+export const LOGOUT_URL = '/logout';
+
+export const hostDetailsUrl = (hostName: string) =>
+ `/app/security/hosts/${hostName}/authentications`;
export const navigateTo = (page: string) => {
cy.visit(page);
};
-
-export const openNavigationFlyout = () => {
- cy.get(TOGGLE_NAVIGATION_BTN).click();
-};
diff --git a/x-pack/plugins/fleet/cypress/tasks/privileges.ts b/x-pack/plugins/fleet/cypress/tasks/privileges.ts
new file mode 100644
index 0000000000000..edcc8e3749689
--- /dev/null
+++ b/x-pack/plugins/fleet/cypress/tasks/privileges.ts
@@ -0,0 +1,232 @@
+/*
+ * 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 { constructUrlWithUser, getEnvAuth } from './login';
+
+interface User {
+ username: string;
+ password: string;
+ description?: string;
+ roles: string[];
+}
+
+interface UserInfo {
+ username: string;
+ full_name: string;
+ email: string;
+}
+
+interface FeaturesPrivileges {
+ [featureId: string]: string[];
+}
+
+interface ElasticsearchIndices {
+ names: string[];
+ privileges: string[];
+}
+
+interface ElasticSearchPrivilege {
+ cluster?: string[];
+ indices?: ElasticsearchIndices[];
+}
+
+interface KibanaPrivilege {
+ spaces: string[];
+ base?: string[];
+ feature?: FeaturesPrivileges;
+}
+
+interface Role {
+ name: string;
+ privileges: {
+ elasticsearch?: ElasticSearchPrivilege;
+ kibana?: KibanaPrivilege[];
+ };
+}
+
+// Create roles with allowed combinations of Fleet and Integrations
+export const FleetAllIntegrAllRole: Role = {
+ name: 'fleet_all_int_all_role',
+ privileges: {
+ elasticsearch: {
+ indices: [
+ {
+ names: ['*'],
+ privileges: ['all'],
+ },
+ ],
+ },
+ kibana: [
+ {
+ feature: {
+ fleetv2: ['all'],
+ fleet: ['all'],
+ },
+ spaces: ['*'],
+ },
+ ],
+ },
+};
+
+export const FleetAllIntegrAllUser: User = {
+ username: 'fleet_all_int_all_user',
+ password: 'password',
+ roles: [FleetAllIntegrAllRole.name],
+};
+
+export const FleetAllIntegrReadRole: Role = {
+ name: 'fleet_all_int_read_user',
+ privileges: {
+ elasticsearch: {
+ indices: [
+ {
+ names: ['*'],
+ privileges: ['all'],
+ },
+ ],
+ },
+ kibana: [
+ {
+ feature: {
+ fleetv2: ['all'],
+ fleet: ['read'],
+ },
+ spaces: ['*'],
+ },
+ ],
+ },
+};
+export const FleetAllIntegrReadUser: User = {
+ username: 'fleet_all_int_read_user',
+ password: 'password',
+ roles: [FleetAllIntegrReadRole.name],
+};
+export const FleetAllIntegrNoneRole: Role = {
+ name: 'fleet_all_int_none_role',
+ privileges: {
+ elasticsearch: {
+ indices: [
+ {
+ names: ['*'],
+ privileges: ['all'],
+ },
+ ],
+ },
+ kibana: [
+ {
+ feature: {
+ fleetv2: ['all'],
+ fleet: ['none'],
+ },
+ spaces: ['*'],
+ },
+ ],
+ },
+};
+export const FleetAllIntegrNoneUser: User = {
+ username: 'fleet_all_int_none_user',
+ password: 'password',
+ roles: [FleetAllIntegrNoneRole.name],
+};
+export const FleetNoneIntegrAllRole: Role = {
+ name: 'fleet_none_int_all_role',
+ privileges: {
+ elasticsearch: {
+ indices: [
+ {
+ names: ['*'],
+ privileges: ['all'],
+ },
+ ],
+ },
+ kibana: [
+ {
+ feature: {
+ fleetv2: ['none'],
+ fleet: ['all'],
+ },
+ spaces: ['*'],
+ },
+ ],
+ },
+};
+export const FleetNoneIntegrAllUser: User = {
+ username: 'fleet_none_int_all_user',
+ password: 'password',
+ roles: [FleetNoneIntegrAllRole.name],
+};
+
+const getUserInfo = (user: User): UserInfo => ({
+ username: user.username,
+ full_name: user.username.replace('_', ' '),
+ email: `${user.username}@elastic.co`,
+});
+
+export enum ROLES {
+ elastic = 'elastic',
+}
+
+export const createUsersAndRoles = (users: User[], roles: Role[]) => {
+ const envUser = getEnvAuth();
+ for (const role of roles) {
+ cy.log(`Creating role: ${JSON.stringify(role)}`);
+ cy.request({
+ body: role.privileges,
+ headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
+ method: 'PUT',
+ url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`),
+ })
+ .its('status')
+ .should('eql', 204);
+ }
+
+ for (const user of users) {
+ const userInfo = getUserInfo(user);
+ cy.log(`Creating user: ${JSON.stringify(user)}`);
+ cy.request({
+ body: {
+ username: user.username,
+ password: user.password,
+ roles: user.roles,
+ full_name: userInfo.full_name,
+ email: userInfo.email,
+ },
+ headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
+ method: 'POST',
+ url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`),
+ })
+ .its('status')
+ .should('eql', 200);
+ }
+};
+
+export const deleteUsersAndRoles = (users: User[], roles: Role[]) => {
+ const envUser = getEnvAuth();
+ for (const user of users) {
+ cy.log(`Deleting user: ${JSON.stringify(user)}`);
+ cy.request({
+ headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
+ method: 'DELETE',
+ url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`),
+ failOnStatusCode: false,
+ })
+ .its('status')
+ .should('oneOf', [204, 404]);
+ }
+
+ for (const role of roles) {
+ cy.log(`Deleting role: ${JSON.stringify(role)}`);
+ cy.request({
+ headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
+ method: 'DELETE',
+ url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`),
+ failOnStatusCode: false,
+ })
+ .its('status')
+ .should('oneOf', [204, 404]);
+ }
+};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx
index 9799561970e48..29a491fe0c932 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx
@@ -88,7 +88,7 @@ const PermissionsError: React.FunctionComponent<{ error: string }> = memo(({ err
+
= memo(({ err
}
body={
-
+
{
+ return {
+ ...jest.requireActual('../../../hooks'),
+ useFleetStatus: jest.fn().mockReturnValue({ isReady: true } as any),
+ sendGetStatus: jest
+ .fn()
+ .mockResolvedValue({ data: { isReady: true, missing_requirements: [] } }),
+ sendGetAgentStatus: jest.fn().mockResolvedValue({ data: { results: { total: 0 } } }),
+ useGetAgentPolicies: jest.fn().mockReturnValue({
+ data: {
+ items: [{ id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }],
+ },
+ error: undefined,
+ isLoading: false,
+ resendRequest: jest.fn(),
+ } as any),
+ sendGetOneAgentPolicy: jest.fn().mockResolvedValue({
+ data: { item: { id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' } },
+ }),
+ useGetPackageInfoByKey: jest.fn().mockReturnValue({
+ data: {
+ item: {
+ name: 'nginx',
+ title: 'Nginx',
+ version: '1.3.0',
+ release: 'ga',
+ description: 'Collect logs and metrics from Nginx HTTP servers with Elastic Agent.',
+ policy_templates: [
+ {
+ name: 'nginx',
+ title: 'Nginx logs and metrics',
+ description: 'Collect logs and metrics from Nginx instances',
+ inputs: [
+ {
+ type: 'logfile',
+ title: 'Collect logs from Nginx instances',
+ description: 'Collecting Nginx access and error logs',
+ },
+ ],
+ multiple: true,
+ },
+ ],
+ data_streams: [
+ {
+ type: 'logs',
+ dataset: 'nginx.access',
+ title: 'Nginx access logs',
+ release: 'experimental',
+ ingest_pipeline: 'default',
+ streams: [
+ {
+ input: 'logfile',
+ vars: [
+ {
+ name: 'paths',
+ type: 'text',
+ title: 'Paths',
+ multi: true,
+ required: true,
+ show_user: true,
+ default: ['/var/log/nginx/access.log*'],
+ },
+ ],
+ template_path: 'stream.yml.hbs',
+ title: 'Nginx access logs',
+ description: 'Collect Nginx access logs',
+ enabled: true,
+ },
+ ],
+ package: 'nginx',
+ path: 'access',
+ },
+ ],
+ latestVersion: '1.3.0',
+ removable: true,
+ keepPoliciesUpToDate: false,
+ status: 'not_installed',
+ },
+ },
+ isLoading: false,
+ }),
+ sendCreatePackagePolicy: jest
+ .fn()
+ .mockResolvedValue({ data: { item: { id: 'policy-1', inputs: [] } } }),
+ sendCreateAgentPolicy: jest.fn().mockResolvedValue({
+ data: { item: { id: 'agent-policy-2', name: 'Agent policy 2', namespace: 'default' } },
+ }),
+ useIntraAppState: jest.fn().mockReturnValue({}),
+ useStartServices: jest.fn().mockReturnValue({
+ application: { navigateToApp: jest.fn() },
+ notifications: {
+ toasts: {
+ addError: jest.fn(),
+ addSuccess: jest.fn(),
+ },
+ },
+ docLinks: {
+ links: {
+ fleet: {},
+ },
+ },
+ http: {
+ basePath: {
+ get: () => 'http://localhost:5620',
+ prepend: (url: string) => 'http://localhost:5620' + url,
+ },
+ },
+ chrome: {
+ docTitle: {
+ change: jest.fn(),
+ },
+ setBreadcrumbs: jest.fn(),
+ },
+ }),
+ };
+});
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useLocation: jest.fn().mockReturnValue({ search: '' }),
+ useHistory: jest.fn().mockReturnValue({
+ push: jest.fn(),
+ }),
+}));
+
describe('when on the package policy create page', () => {
- const createPageUrlPath = pagePathGetters.add_integration_to_policy({ pkgkey: 'nginx-0.3.7' })[1];
+ const createPageUrlPath = pagePathGetters.add_integration_to_policy({ pkgkey: 'nginx-1.3.0' })[1];
let testRenderer: TestRenderer;
let renderResult: ReturnType;
@@ -47,6 +180,8 @@ describe('when on the package policy create page', () => {
pathname: createPageUrlPath,
state: expectedRouteState,
});
+
+ (useIntraAppState as jest.MockedFunction).mockReturnValue(expectedRouteState);
});
describe('and the cancel Link or Button is clicked', () => {
@@ -67,16 +202,325 @@ describe('when on the package policy create page', () => {
});
});
- it('should use custom "cancel" URL', () => {
+ test('should use custom "cancel" URL', () => {
expect(cancelLink.href).toBe(expectedRouteState.onCancelUrl);
expect(cancelButton.href).toBe(expectedRouteState.onCancelUrl);
});
});
});
+
+ describe('submit page', () => {
+ const newPackagePolicy = {
+ description: '',
+ enabled: true,
+ inputs: [
+ {
+ enabled: true,
+ policy_template: 'nginx',
+ streams: [
+ {
+ data_stream: {
+ dataset: 'nginx.access',
+ type: 'logs',
+ },
+ enabled: true,
+ vars: {
+ paths: {
+ type: 'text',
+ value: ['/var/log/nginx/access.log*'],
+ },
+ },
+ },
+ ],
+ type: 'logfile',
+ },
+ ],
+ name: 'nginx-1',
+ namespace: 'default',
+ output_id: '',
+ package: {
+ name: 'nginx',
+ title: 'Nginx',
+ version: '1.3.0',
+ },
+ policy_id: 'agent-policy-1',
+ vars: undefined,
+ };
+
+ test('should create package policy on submit when query param agent policy id is set', async () => {
+ (useLocation as jest.MockedFunction).mockImplementationOnce(() => ({
+ search: 'policyId=agent-policy-1',
+ }));
+
+ render();
+
+ let saveBtn: HTMLElement;
+
+ await waitFor(() => {
+ saveBtn = renderResult.getByText(/Save and continue/).closest('button')!;
+ expect(saveBtn).not.toBeDisabled();
+ });
+
+ await act(async () => {
+ fireEvent.click(saveBtn);
+ });
+
+ expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalledWith({
+ ...newPackagePolicy,
+ policy_id: 'agent-policy-1',
+ });
+ expect(sendCreateAgentPolicy as jest.MockedFunction).not.toHaveBeenCalled();
+
+ await waitFor(() => {
+ expect(renderResult.getByText('Nginx integration added')).toBeInTheDocument();
+ });
+ });
+
+ describe('on save navigate', () => {
+ async function setupSaveNavigate(routeState: any) {
+ (useIntraAppState as jest.MockedFunction).mockReturnValue(routeState);
+ render();
+
+ await act(async () => {
+ fireEvent.click(renderResult.getByText('Existing hosts')!);
+ });
+
+ await act(async () => {
+ fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!);
+ });
+
+ await act(async () => {
+ fireEvent.click(
+ renderResult.getByText(/Add Elastic Agent to your hosts/).closest('button')!
+ );
+ });
+ }
+
+ test('should navigate to save navigate path if set', async () => {
+ const routeState = {
+ onSaveNavigateTo: [PLUGIN_ID, { path: '/save/url/here' }],
+ };
+
+ await setupSaveNavigate(routeState);
+
+ expect(useStartServices().application.navigateToApp).toHaveBeenCalledWith(PLUGIN_ID, {
+ path: '/save/url/here',
+ });
+ });
+
+ test('should navigate to save navigate path with query param if set', async () => {
+ const mockUseLocation = useLocation as jest.MockedFunction;
+ mockUseLocation.mockReturnValue({
+ search: 'policyId=agent-policy-1',
+ });
+
+ const routeState = {
+ onSaveNavigateTo: [PLUGIN_ID, { path: '/save/url/here' }],
+ };
+
+ await setupSaveNavigate(routeState);
+
+ expect(useStartServices().application.navigateToApp).toHaveBeenCalledWith(PLUGIN_ID, {
+ path: '/policies/agent-policy-1',
+ });
+
+ mockUseLocation.mockReturnValue({
+ search: '',
+ });
+ });
+
+ test('should navigate to save navigate app if set', async () => {
+ const routeState = {
+ onSaveNavigateTo: [PLUGIN_ID],
+ };
+ await setupSaveNavigate(routeState);
+
+ expect(useStartServices().application.navigateToApp).toHaveBeenCalledWith(PLUGIN_ID);
+ });
+
+ test('should set history if no routeState', async () => {
+ await setupSaveNavigate({});
+
+ expect(useHistory().push).toHaveBeenCalledWith('/policies/agent-policy-1');
+ });
+ });
+
+ describe('without query param', () => {
+ beforeEach(() => {
+ render();
+
+ (sendCreateAgentPolicy as jest.MockedFunction).mockClear();
+ (sendCreatePackagePolicy as jest.MockedFunction).mockClear();
+ });
+
+ test('should create agent policy before creating package policy on submit when new hosts is selected', async () => {
+ await waitFor(() => {
+ renderResult.getByDisplayValue('Agent policy 2');
+ });
+
+ await act(async () => {
+ fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!);
+ });
+
+ expect(sendCreateAgentPolicy as jest.MockedFunction).toHaveBeenCalledWith(
+ {
+ description: '',
+ monitoring_enabled: ['logs', 'metrics'],
+ name: 'Agent policy 2',
+ namespace: 'default',
+ },
+ { withSysMonitoring: true }
+ );
+ expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalledWith({
+ ...newPackagePolicy,
+ policy_id: 'agent-policy-2',
+ });
+
+ await waitFor(() => {
+ expect(renderResult.getByText('Nginx integration added')).toBeInTheDocument();
+ });
+ });
+
+ test('should disable submit button on invalid form with empty agent policy name', async () => {
+ await act(async () => {
+ fireEvent.change(renderResult.getByLabelText('New agent policy name'), {
+ target: { value: '' },
+ });
+ });
+
+ renderResult.getByText(
+ 'Your integration policy has errors. Please fix them before saving.'
+ );
+ expect(renderResult.getByText(/Save and continue/).closest('button')!).toBeDisabled();
+ });
+
+ test('should not show modal if agent policy has agents', async () => {
+ (sendGetAgentStatus as jest.MockedFunction).mockResolvedValueOnce({
+ data: { results: { total: 1 } },
+ });
+
+ await act(async () => {
+ fireEvent.click(renderResult.getByText('Existing hosts')!);
+ });
+
+ await act(async () => {
+ fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!);
+ });
+
+ await waitFor(() => {
+ expect(renderResult.getByText('This action will update 1 agent')).toBeInTheDocument();
+ });
+
+ await act(async () => {
+ fireEvent.click(
+ renderResult.getAllByText(/Save and deploy changes/)[1].closest('button')!
+ );
+ });
+
+ expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalled();
+ });
+
+ describe('create package policy with existing agent policy', () => {
+ beforeEach(async () => {
+ await act(async () => {
+ fireEvent.click(renderResult.getByText('Existing hosts')!);
+ });
+ });
+
+ test('should creating package policy with existing host', async () => {
+ await act(async () => {
+ fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!);
+ });
+
+ expect(sendCreateAgentPolicy as jest.MockedFunction