[SECURITY] Remove flakiness around edit user (#117558) (#117897)

* wip

* convert flaky jest test to functional test

* improvement from review

* fix

* fix i18n

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2021-11-08 16:40:01 -05:00 committed by GitHub
parent 4e592db5db
commit cb0625ed88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 395 additions and 352 deletions

View file

@ -0,0 +1,137 @@
/*
* 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 { ChangePasswordFormValues } from './change_password_flyout';
import { validateChangePasswordForm } from './change_password_flyout';
describe('ChangePasswordFlyout', () => {
describe('#validateChangePasswordForm', () => {
describe('for current user', () => {
it('should show an error when it is current user with no current password', () => {
expect(
validateChangePasswordForm({ password: 'changeme', confirm_password: 'changeme' }, true)
).toMatchInlineSnapshot(`
Object {
"current_password": "Enter your current password.",
}
`);
});
it('should show errors when there is no new password', () => {
expect(
validateChangePasswordForm(
{
password: undefined,
confirm_password: 'changeme',
} as unknown as ChangePasswordFormValues,
true
)
).toMatchInlineSnapshot(`
Object {
"current_password": "Enter your current password.",
"password": "Enter a new password.",
}
`);
});
it('should show errors when the new password is not at least 6 characters', () => {
expect(validateChangePasswordForm({ password: '12345', confirm_password: '12345' }, true))
.toMatchInlineSnapshot(`
Object {
"current_password": "Enter your current password.",
"password": "Password must be at least 6 characters.",
}
`);
});
it('should show errors when new password does not match confirmation password', () => {
expect(
validateChangePasswordForm(
{
password: 'changeme',
confirm_password: 'notTheSame',
},
true
)
).toMatchInlineSnapshot(`
Object {
"confirm_password": "Passwords do not match.",
"current_password": "Enter your current password.",
}
`);
});
it('should show NO errors', () => {
expect(
validateChangePasswordForm(
{
current_password: 'oldpassword',
password: 'changeme',
confirm_password: 'changeme',
},
true
)
).toMatchInlineSnapshot(`Object {}`);
});
});
describe('for another user', () => {
it('should show errors when there is no new password', () => {
expect(
validateChangePasswordForm(
{
password: undefined,
confirm_password: 'changeme',
} as unknown as ChangePasswordFormValues,
false
)
).toMatchInlineSnapshot(`
Object {
"password": "Enter a new password.",
}
`);
});
it('should show errors when the new password is not at least 6 characters', () => {
expect(validateChangePasswordForm({ password: '1234', confirm_password: '1234' }, false))
.toMatchInlineSnapshot(`
Object {
"password": "Password must be at least 6 characters.",
}
`);
});
it('should show errors when new password does not match confirmation password', () => {
expect(
validateChangePasswordForm(
{
password: 'changeme',
confirm_password: 'notTheSame',
},
false
)
).toMatchInlineSnapshot(`
Object {
"confirm_password": "Passwords do not match.",
}
`);
});
it('should show NO errors', () => {
expect(
validateChangePasswordForm(
{
password: 'changeme',
confirm_password: 'changeme',
},
false
)
).toMatchInlineSnapshot(`Object {}`);
});
});
});
});

View file

@ -44,6 +44,49 @@ export interface ChangePasswordFlyoutProps {
onSuccess?(): void;
}
export const validateChangePasswordForm = (
values: ChangePasswordFormValues,
isCurrentUser: boolean
) => {
const errors: ValidationErrors<typeof values> = {};
if (isCurrentUser) {
if (!values.current_password) {
errors.current_password = i18n.translate(
'xpack.security.management.users.changePasswordFlyout.currentPasswordRequiredError',
{
defaultMessage: 'Enter your current password.',
}
);
}
}
if (!values.password) {
errors.password = i18n.translate(
'xpack.security.management.users.changePasswordFlyout.passwordRequiredError',
{
defaultMessage: 'Enter a new password.',
}
);
} else if (values.password.length < 6) {
errors.password = i18n.translate(
'xpack.security.management.users.changePasswordFlyout.passwordInvalidError',
{
defaultMessage: 'Password must be at least 6 characters.',
}
);
} else if (values.password !== values.confirm_password) {
errors.confirm_password = i18n.translate(
'xpack.security.management.users.changePasswordFlyout.confirmPasswordInvalidError',
{
defaultMessage: 'Passwords do not match.',
}
);
}
return errors;
};
export const ChangePasswordFlyout: FunctionComponent<ChangePasswordFlyoutProps> = ({
username,
defaultValues = {
@ -99,52 +142,7 @@ export const ChangePasswordFlyout: FunctionComponent<ChangePasswordFlyoutProps>
}
}
},
validate: async (values) => {
const errors: ValidationErrors<typeof values> = {};
if (isCurrentUser) {
if (!values.current_password) {
errors.current_password = i18n.translate(
'xpack.security.management.users.changePasswordFlyout.currentPasswordRequiredError',
{
defaultMessage: 'Enter your current password.',
}
);
}
}
if (!values.password) {
errors.password = i18n.translate(
'xpack.security.management.users.changePasswordFlyout.passwordRequiredError',
{
defaultMessage: 'Enter a new password.',
}
);
} else if (values.password.length < 6) {
errors.password = i18n.translate(
'xpack.security.management.users.changePasswordFlyout.passwordInvalidError',
{
defaultMessage: 'Password must be at least 6 characters.',
}
);
} else if (!values.confirm_password) {
errors.confirm_password = i18n.translate(
'xpack.security.management.users.changePasswordFlyout.confirmPasswordRequiredError',
{
defaultMessage: 'Passwords do not match.',
}
);
} else if (values.password !== values.confirm_password) {
errors.confirm_password = i18n.translate(
'xpack.security.management.users.changePasswordFlyout.confirmPasswordInvalidError',
{
defaultMessage: 'Passwords do not match.',
}
);
}
return errors;
},
validate: async (values) => validateChangePasswordForm(values, isCurrentUser),
defaultValues,
});
@ -246,6 +244,7 @@ export const ChangePasswordFlyout: FunctionComponent<ChangePasswordFlyoutProps>
isInvalid={form.touched.current_password && !!form.errors.current_password}
autoComplete="current-password"
inputRef={firstFieldRef}
data-test-subj="editUserChangePasswordCurrentPasswordInput"
/>
</EuiFormRow>
) : null}
@ -268,6 +267,7 @@ export const ChangePasswordFlyout: FunctionComponent<ChangePasswordFlyoutProps>
isInvalid={form.touched.password && !!form.errors.password}
autoComplete="new-password"
inputRef={isCurrentUser ? undefined : firstFieldRef}
data-test-subj="editUserChangePasswordNewPasswordInput"
/>
</EuiFormRow>
<EuiFormRow
@ -284,6 +284,7 @@ export const ChangePasswordFlyout: FunctionComponent<ChangePasswordFlyoutProps>
defaultValue={form.values.confirm_password}
isInvalid={form.touched.confirm_password && !!form.errors.confirm_password}
autoComplete="new-password"
data-test-subj="editUserChangePasswordConfirmPasswordInput"
/>
</EuiFormRow>

View file

@ -5,21 +5,16 @@
* 2.0.
*/
import { fireEvent, render, waitFor, within } from '@testing-library/react';
import { fireEvent, render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import React from 'react';
import { coreMock } from 'src/core/public/mocks';
import { mockAuthenticatedUser } from '../../../../common/model/authenticated_user.mock';
import { securityMock } from '../../../mocks';
import { Providers } from '../users_management_app';
import { EditUserPage } from './edit_user_page';
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => `id-${Math.random()}`,
}));
const userMock = {
username: 'jdoe',
full_name: '',
@ -28,13 +23,7 @@ const userMock = {
roles: ['superuser'],
};
// FLAKY: https://github.com/elastic/kibana/issues/115473
// FLAKY: https://github.com/elastic/kibana/issues/115474
// FLAKY: https://github.com/elastic/kibana/issues/116889
// FLAKY: https://github.com/elastic/kibana/issues/117081
// FLAKY: https://github.com/elastic/kibana/issues/116891
// FLAKY: https://github.com/elastic/kibana/issues/116890
describe.skip('EditUserPage', () => {
describe('EditUserPage', () => {
const coreStart = coreMock.createStart();
let history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] });
const authc = securityMock.createSetup().authc;
@ -135,263 +124,4 @@ describe.skip('EditUserPage', () => {
await findByText(/Role .deprecated_role. is deprecated. Use .new_role. instead/i);
});
it('updates user when submitting form and redirects back', async () => {
coreStart.http.get.mockResolvedValueOnce(userMock);
coreStart.http.get.mockResolvedValueOnce([]);
coreStart.http.post.mockResolvedValueOnce({});
const { findByRole, findByLabelText } = render(
<Providers services={coreStart} authc={authc} history={history}>
<EditUserPage username={userMock.username} />
</Providers>
);
fireEvent.change(await findByLabelText('Full name'), { target: { value: 'John Doe' } });
fireEvent.change(await findByLabelText('Email address'), {
target: { value: 'jdoe@elastic.co' },
});
fireEvent.click(await findByRole('button', { name: 'Update user' }));
await waitFor(() => {
expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security/users/jdoe');
expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role');
expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', {
body: JSON.stringify({
...userMock,
full_name: 'John Doe',
email: 'jdoe@elastic.co',
}),
});
expect(history.location.pathname).toBe('/');
});
});
it('warns when user form submission fails', async () => {
coreStart.http.get.mockResolvedValueOnce(userMock);
coreStart.http.get.mockResolvedValueOnce([]);
coreStart.http.post.mockRejectedValueOnce(new Error('Error message'));
const { findByRole, findByLabelText } = render(
<Providers services={coreStart} authc={authc} history={history}>
<EditUserPage username={userMock.username} />
</Providers>
);
fireEvent.change(await findByLabelText('Full name'), { target: { value: 'John Doe' } });
fireEvent.change(await findByLabelText('Email address'), {
target: { value: 'jdoe@elastic.co' },
});
fireEvent.click(await findByRole('button', { name: 'Update user' }));
await waitFor(() => {
expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security/users/jdoe');
expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role');
expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', {
body: JSON.stringify({
...userMock,
full_name: 'John Doe',
email: 'jdoe@elastic.co',
}),
});
expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({
text: 'Error message',
title: "Could not update user 'jdoe'",
});
expect(history.location.pathname).toBe('/edit/jdoe');
});
});
it('changes password of other user when submitting form and closes dialog', async () => {
coreStart.http.get.mockResolvedValueOnce(userMock);
coreStart.http.get.mockResolvedValueOnce([]);
authc.getCurrentUser.mockResolvedValueOnce(
mockAuthenticatedUser({ ...userMock, username: 'elastic' })
);
coreStart.http.post.mockResolvedValueOnce({});
const { findByRole } = render(
<Providers services={coreStart} authc={authc} history={history}>
<EditUserPage username={userMock.username} />
</Providers>
);
fireEvent.click(await findByRole('button', { name: 'Change password' }));
const dialog = await findByRole('dialog');
fireEvent.change(await within(dialog).findByLabelText('New password'), {
target: { value: 'changeme' },
});
fireEvent.change(await within(dialog).findByLabelText('Confirm password'), {
target: { value: 'changeme' },
});
fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' }));
expect(await findByRole('dialog')).not.toBeInTheDocument();
expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/password', {
body: JSON.stringify({
newPassword: 'changeme',
}),
});
});
it('changes password of current user when submitting form and closes dialog', async () => {
coreStart.http.get.mockResolvedValueOnce(userMock);
coreStart.http.get.mockResolvedValueOnce([]);
authc.getCurrentUser.mockResolvedValueOnce(mockAuthenticatedUser(userMock));
coreStart.http.post.mockResolvedValueOnce({});
const { findByRole } = render(
<Providers services={coreStart} authc={authc} history={history}>
<EditUserPage username={userMock.username} />
</Providers>
);
fireEvent.click(await findByRole('button', { name: 'Change password' }));
const dialog = await findByRole('dialog');
fireEvent.change(await within(dialog).findByLabelText('Current password'), {
target: { value: '123456' },
});
fireEvent.change(await within(dialog).findByLabelText('New password'), {
target: { value: 'changeme' },
});
fireEvent.change(await within(dialog).findByLabelText('Confirm password'), {
target: { value: 'changeme' },
});
fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' }));
expect(await findByRole('dialog')).not.toBeInTheDocument();
expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/password', {
body: JSON.stringify({
newPassword: 'changeme',
password: '123456',
}),
});
});
it('warns when change password form submission fails', async () => {
coreStart.http.get.mockResolvedValueOnce(userMock);
coreStart.http.get.mockResolvedValueOnce([]);
authc.getCurrentUser.mockResolvedValueOnce(
mockAuthenticatedUser({ ...userMock, username: 'elastic' })
);
coreStart.http.post.mockRejectedValueOnce(new Error('Error message'));
const { findByRole } = render(
<Providers services={coreStart} authc={authc} history={history}>
<EditUserPage username={userMock.username} />
</Providers>
);
fireEvent.click(await findByRole('button', { name: 'Change password' }));
const dialog = await findByRole('dialog');
fireEvent.change(await within(dialog).findByLabelText('New password'), {
target: { value: 'changeme' },
});
fireEvent.change(await within(dialog).findByLabelText('Confirm password'), {
target: { value: 'changeme' },
});
fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' }));
await waitFor(() => {
expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({
text: 'Error message',
title: 'Could not change password',
});
});
});
it('validates change password form', async () => {
coreStart.http.get.mockResolvedValueOnce(userMock);
coreStart.http.get.mockResolvedValueOnce([]);
authc.getCurrentUser.mockResolvedValueOnce(mockAuthenticatedUser(userMock));
coreStart.http.post.mockResolvedValueOnce({});
const { findByRole } = render(
<Providers services={coreStart} authc={authc} history={history}>
<EditUserPage username={userMock.username} />
</Providers>
);
fireEvent.click(await findByRole('button', { name: 'Change password' }));
const dialog = await findByRole('dialog');
fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' }));
await within(dialog).findByText(/Enter your current password/i);
await within(dialog).findByText(/Enter a new password/i);
fireEvent.change(await within(dialog).findByLabelText('Current password'), {
target: { value: 'changeme' },
});
fireEvent.change(await within(dialog).findByLabelText('New password'), {
target: { value: '111' },
});
await within(dialog).findAllByText(/Password must be at least 6 characters/i);
fireEvent.change(await within(dialog).findByLabelText('New password'), {
target: { value: '123456' },
});
fireEvent.change(await within(dialog).findByLabelText('Confirm password'), {
target: { value: '111' },
});
await within(dialog).findAllByText(/Passwords do not match/i);
});
it('deactivates user when confirming and closes dialog', async () => {
coreStart.http.get.mockResolvedValueOnce(userMock);
coreStart.http.get.mockResolvedValueOnce([]);
coreStart.http.post.mockResolvedValueOnce({});
const { findByRole } = render(
<Providers services={coreStart} authc={authc} history={history}>
<EditUserPage username={userMock.username} />
</Providers>
);
fireEvent.click(await findByRole('button', { name: 'Deactivate user' }));
const dialog = await findByRole('dialog');
fireEvent.click(await within(dialog).findByRole('button', { name: 'Deactivate user' }));
expect(await findByRole('dialog')).not.toBeInTheDocument();
expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_disable');
});
it('activates user when confirming and closes dialog', async () => {
coreStart.http.get.mockResolvedValueOnce({ ...userMock, enabled: false });
coreStart.http.get.mockResolvedValueOnce([]);
coreStart.http.post.mockResolvedValueOnce({});
const { findByRole, findAllByRole } = render(
<Providers services={coreStart} authc={authc} history={history}>
<EditUserPage username={userMock.username} />
</Providers>
);
const [enableButton] = await findAllByRole('button', { name: 'Activate user' });
fireEvent.click(enableButton);
const dialog = await findByRole('dialog');
fireEvent.click(await within(dialog).findByRole('button', { name: 'Activate user' }));
expect(await findByRole('dialog')).not.toBeInTheDocument();
expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_enable');
});
it('deletes user when confirming and redirects back', async () => {
coreStart.http.get.mockResolvedValueOnce(userMock);
coreStart.http.get.mockResolvedValueOnce([]);
coreStart.http.delete.mockResolvedValueOnce({});
const { findByRole } = render(
<Providers services={coreStart} authc={authc} history={history}>
<EditUserPage username={userMock.username} />
</Providers>
);
fireEvent.click(await findByRole('button', { name: 'Delete user' }));
const dialog = await findByRole('dialog');
fireEvent.click(await within(dialog).findByRole('button', { name: 'Delete user' }));
await waitFor(() => {
expect(coreStart.http.delete).toHaveBeenLastCalledWith('/internal/security/users/jdoe');
expect(history.location.pathname).toBe('/');
});
});
});

View file

@ -211,7 +211,11 @@ export const EditUserPage: FunctionComponent<EditUserPageProps> = ({ username })
</EuiDescriptionList>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={() => setAction('changePassword')} size="s">
<EuiButton
onClick={() => setAction('changePassword')}
size="s"
data-test-subj="editUserChangePasswordButton"
>
<FormattedMessage
id="xpack.security.management.users.editUserPage.changePasswordButton"
defaultMessage="Change password"
@ -242,7 +246,11 @@ export const EditUserPage: FunctionComponent<EditUserPageProps> = ({ username })
</EuiDescriptionList>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={() => setAction('enableUser')} size="s">
<EuiButton
onClick={() => setAction('enableUser')}
size="s"
data-test-subj="editUserEnableUserButton"
>
<FormattedMessage
id="xpack.security.management.users.editUserPage.enableUserButton"
defaultMessage="Activate user"
@ -271,7 +279,11 @@ export const EditUserPage: FunctionComponent<EditUserPageProps> = ({ username })
</EuiDescriptionList>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={() => setAction('disableUser')} size="s">
<EuiButton
onClick={() => setAction('disableUser')}
size="s"
data-test-subj="editUserDisableUserButton"
>
<FormattedMessage
id="xpack.security.management.users.editUserPage.disableUserButton"
defaultMessage="Deactivate user"
@ -304,7 +316,12 @@ export const EditUserPage: FunctionComponent<EditUserPageProps> = ({ username })
</EuiDescriptionList>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={() => setAction('deleteUser')} size="s" color="danger">
<EuiButton
onClick={() => setAction('deleteUser')}
size="s"
color="danger"
data-test-subj="editUserDeleteUserButton"
>
<FormattedMessage
id="xpack.security.management.users.editUserPage.deleteUserButton"
defaultMessage="Delete user"

View file

@ -43,6 +43,7 @@ export interface UserFormValues {
username?: string;
full_name?: string;
email?: string;
current_password?: string;
password?: string;
confirm_password?: string;
roles: readonly string[];

View file

@ -19974,7 +19974,6 @@
"xpack.security.management.users.changePasswordFlyout.confirmButton": "{isSubmitting, select, true{パスワードを変更しています…} other{パスワードの変更}}",
"xpack.security.management.users.changePasswordFlyout.confirmPasswordInvalidError": "パスワードが一致していません。",
"xpack.security.management.users.changePasswordFlyout.confirmPasswordLabel": "パスワードの確認",
"xpack.security.management.users.changePasswordFlyout.confirmPasswordRequiredError": "パスワードが一致していません。",
"xpack.security.management.users.changePasswordFlyout.confirmSystemPasswordButton": "{isSubmitting, select, true{パスワードを変更しています…} other{パスワードの変更}}",
"xpack.security.management.users.changePasswordFlyout.currentPasswordInvalidError": "無効なパスワードです。",
"xpack.security.management.users.changePasswordFlyout.currentPasswordLabel": "現在のパスワード",

View file

@ -20269,7 +20269,6 @@
"xpack.security.management.users.changePasswordFlyout.confirmButton": "{isSubmitting, select, true{正在更改密码……} other{更改密码}}",
"xpack.security.management.users.changePasswordFlyout.confirmPasswordInvalidError": "密码不匹配。",
"xpack.security.management.users.changePasswordFlyout.confirmPasswordLabel": "确认密码",
"xpack.security.management.users.changePasswordFlyout.confirmPasswordRequiredError": "密码不匹配。",
"xpack.security.management.users.changePasswordFlyout.confirmSystemPasswordButton": "{isSubmitting, select, true{正在更改密码……} other{更改密码}}",
"xpack.security.management.users.changePasswordFlyout.currentPasswordInvalidError": "密码无效。",
"xpack.security.management.users.changePasswordFlyout.currentPasswordLabel": "当前密码",

View file

@ -9,12 +9,24 @@ import expect from '@kbn/expect';
import { keyBy } from 'lodash';
import { FtrProviderContext } from '../../ftr_provider_context';
import type { UserFormValues } from '../../../../plugins/security/public/management/users/edit_user/user_form';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['security', 'settings']);
const config = getService('config');
const log = getService('log');
const retry = getService('retry');
const toasts = getService('toasts');
const browser = getService('browser');
describe('users', function () {
const optionalUser: UserFormValues = {
username: 'OptionalUser',
password: 'OptionalUserPwd',
confirm_password: 'OptionalUserPwd',
roles: ['superuser'],
};
before(async () => {
log.debug('users');
await PageObjects.settings.navigateTo();
@ -44,32 +56,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should add new user', async function () {
await PageObjects.security.createUser({
const userLee: UserFormValues = {
username: 'Lee',
password: 'LeePwd',
confirm_password: 'LeePwd',
full_name: 'LeeFirst LeeLast',
email: 'lee@myEmail.com',
roles: ['kibana_admin'],
});
};
await PageObjects.security.createUser(userLee);
const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username');
log.debug('actualUsers = %j', users);
expect(users.Lee.roles).to.eql(['kibana_admin']);
expect(users.Lee.fullname).to.eql('LeeFirst LeeLast');
expect(users.Lee.email).to.eql('lee@myEmail.com');
expect(users.Lee.roles).to.eql(userLee.roles);
expect(users.Lee.fullname).to.eql(userLee.full_name);
expect(users.Lee.email).to.eql(userLee.email);
expect(users.Lee.reserved).to.be(false);
});
it('should add new user with optional fields left empty', async function () {
await PageObjects.security.createUser({
username: 'OptionalUser',
password: 'OptionalUserPwd',
confirm_password: 'OptionalUserPwd',
roles: [],
});
await PageObjects.security.createUser(optionalUser);
const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username');
log.debug('actualUsers = %j', users);
expect(users.OptionalUser.roles).to.eql(['']);
expect(users.OptionalUser.roles).to.eql(optionalUser.roles);
expect(users.OptionalUser.fullname).to.eql('');
expect(users.OptionalUser.email).to.eql('');
expect(users.OptionalUser.reserved).to.be(false);
@ -115,5 +123,101 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(roles.monitoring_user.reserved).to.be(true);
expect(roles.monitoring_user.deprecated).to.be(false);
});
describe('edit', function () {
before(async () => {
await PageObjects.security.clickElasticsearchUsers();
});
describe('update user profile', () => {
it('when submitting form and redirects back', async () => {
optionalUser.full_name = 'Optional User';
optionalUser.email = 'optionalUser@elastic.co';
await PageObjects.security.updateUserProfile(optionalUser);
const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username');
expect(users.OptionalUser.roles).to.eql(optionalUser.roles);
expect(users.OptionalUser.fullname).to.eql(optionalUser.full_name);
expect(users.OptionalUser.email).to.eql(optionalUser.email);
expect(users.OptionalUser.reserved).to.be(false);
expect(await browser.getCurrentUrl()).to.contain('app/management/security/users');
});
});
describe('change password', () => {
before(async () => {
await toasts.dismissAllToasts();
});
afterEach(async () => {
await PageObjects.security.submitUpdateUserForm();
await toasts.dismissAllToasts();
});
after(async () => {
await PageObjects.security.forceLogout();
await PageObjects.security.login();
await PageObjects.settings.navigateTo();
await PageObjects.security.clickElasticsearchUsers();
});
it('of other user when submitting form', async () => {
optionalUser.password = 'NewOptionalUserPwd';
optionalUser.confirm_password = 'NewOptionalUserPwd';
await PageObjects.security.updateUserPassword(optionalUser);
await retry.waitFor('', async () => {
const toastCount = await toasts.getToastCount();
return toastCount >= 1;
});
const successToast = await toasts.getToastElement(1);
expect(await successToast.getVisibleText()).to.be(
`Password changed for '${optionalUser.username}'.`
);
});
it('of current user when submitting form', async () => {
optionalUser.current_password = 'NewOptionalUserPwd';
optionalUser.password = 'NewOptionalUserPwd_2';
optionalUser.confirm_password = 'NewOptionalUserPwd_2';
await PageObjects.security.forceLogout();
await PageObjects.security.login(optionalUser.username, optionalUser.current_password);
await PageObjects.settings.navigateTo();
await PageObjects.security.clickElasticsearchUsers();
await PageObjects.security.updateUserPassword(optionalUser, true);
await retry.waitFor('', async () => {
const toastCount = await toasts.getToastCount();
return toastCount >= 1;
});
const successToast = await toasts.getToastElement(1);
expect(await successToast.getVisibleText()).to.be(
`Password changed for '${optionalUser.username}'.`
);
});
});
describe('Deactivate/Activate user', () => {
it('deactivates user when confirming', async () => {
await PageObjects.security.deactivatesUser(optionalUser);
const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username');
expect(users.OptionalUser.enabled).to.be(false);
});
it('activates user when confirming', async () => {
await PageObjects.security.activatesUser(optionalUser);
const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username');
expect(users.OptionalUser.enabled).to.be(true);
});
});
describe('Delete user', () => {
it('when confirming and closes dialog', async () => {
await PageObjects.security.deleteUser(optionalUser.username ?? '');
const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username');
expect(users).to.not.have.key(optionalUser.username ?? '');
expect(await browser.getCurrentUrl()).to.contain('app/management/security/users');
});
});
});
});
}

View file

@ -389,7 +389,7 @@ export class SecurityPageObject extends FtrService {
// findAll is substantially faster than `find.descendantExistsByCssSelector for negative cases
const isUserReserved = (await user.findAllByTestSubject('userReserved', 1)).length > 0;
const isUserDeprecated = (await user.findAllByTestSubject('userDeprecated', 1)).length > 0;
const isEnabled = (await user.findAllByTestSubject('userDisabled', 1)).length === 0;
users.push({
username: await usernameElement.getVisibleText(),
fullname: await fullnameElement.getVisibleText(),
@ -397,6 +397,7 @@ export class SecurityPageObject extends FtrService {
roles: (await rolesElement.getVisibleText()).split('\n').map((role) => role.trim()),
reserved: isUserReserved,
deprecated: isUserDeprecated,
enabled: isEnabled,
});
}
@ -459,10 +460,23 @@ export class SecurityPageObject extends FtrService {
}
}
async updateUserProfileForm(user: UserFormValues) {
if (user.full_name) {
await this.find.setValue('[name=full_name]', user.full_name);
}
if (user.email) {
await this.find.setValue('[name=email]', user.email);
}
}
async submitCreateUserForm() {
await this.find.clickByButtonText('Create user');
}
async submitUpdateUserForm() {
await this.find.clickByButtonText('Update user');
}
async createUser(user: UserFormValues) {
await this.clickElasticsearchUsers();
await this.clickCreateNewUser();
@ -470,6 +484,62 @@ export class SecurityPageObject extends FtrService {
await this.submitCreateUserForm();
}
async clickUserByUserName(username: string) {
await this.find.clickByDisplayedLinkText(username);
await this.header.awaitGlobalLoadingIndicatorHidden();
}
async updateUserPassword(user: UserFormValues, isCurrentUser: boolean = false) {
await this.clickUserByUserName(user.username ?? '');
await this.testSubjects.click('editUserChangePasswordButton');
if (isCurrentUser) {
await this.testSubjects.setValue(
'editUserChangePasswordCurrentPasswordInput',
user.current_password ?? ''
);
}
await this.testSubjects.setValue('editUserChangePasswordNewPasswordInput', user.password ?? '');
await this.testSubjects.setValue(
'editUserChangePasswordConfirmPasswordInput',
user.confirm_password ?? ''
);
await this.testSubjects.click('formFlyoutSubmitButton');
}
async updateUserProfile(user: UserFormValues) {
await this.clickUserByUserName(user.username ?? '');
await this.updateUserProfileForm(user);
await this.submitUpdateUserForm();
}
async deactivatesUser(user: UserFormValues) {
await this.clickUserByUserName(user.username ?? '');
await this.testSubjects.click('editUserDisableUserButton');
await this.testSubjects.click('confirmModalConfirmButton');
await this.submitUpdateUserForm();
}
async activatesUser(user: UserFormValues) {
await this.clickUserByUserName(user.username ?? '');
await this.testSubjects.click('editUserEnableUserButton');
await this.testSubjects.click('confirmModalConfirmButton');
await this.submitUpdateUserForm();
}
async deleteUser(username: string) {
this.log.debug('Delete user ' + username);
await this.clickUserByUserName(username);
this.log.debug('Find delete button and click');
await this.find.clickByButtonText('Delete user');
await this.common.sleep(2000);
const confirmText = await this.testSubjects.getVisibleText('confirmModalBodyText');
this.log.debug('Delete user alert text = ' + confirmText);
await this.testSubjects.click('confirmModalConfirmButton');
return confirmText;
}
async addRole(roleName: string, roleObj: Role) {
const self = this;
@ -548,19 +618,4 @@ export class SecurityPageObject extends FtrService {
await this.find.clickByCssSelector(`[role=option][title="${role}"]`);
await this.testSubjects.click('comboBoxToggleListButton');
}
async deleteUser(username: string) {
this.log.debug('Delete user ' + username);
await this.find.clickByDisplayedLinkText(username);
await this.header.awaitGlobalLoadingIndicatorHidden();
this.log.debug('Find delete button and click');
await this.find.clickByButtonText('Delete user');
await this.common.sleep(2000);
const confirmText = await this.testSubjects.getVisibleText('confirmModalBodyText');
this.log.debug('Delete user alert text = ' + confirmText);
await this.testSubjects.click('confirmModalConfirmButton');
return confirmText;
}
}