diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx
index 72fd79805f970..6e3de061fd191 100644
--- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx
+++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx
@@ -5,13 +5,7 @@
* 2.0.
*/
-import {
- fireEvent,
- render,
- waitFor,
- waitForElementToBeRemoved,
- within,
-} from '@testing-library/react';
+import { render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import React from 'react';
@@ -22,60 +16,75 @@ import { Providers } from '../api_keys_management_app';
import { apiKeysAPIClientMock } from '../index.mock';
import { APIKeysGridPage } from './api_keys_grid_page';
-jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
- htmlIdGenerator: () => () => `id-${Math.random()}`,
-}));
+/*
+ * Note to engineers
+ * we moved these 4 tests below to "x-pack/test/functional/apps/api_keys/home_page.ts":
+ * 1-"creates API key when submitting form, redirects back and displays base64"
+ * 2-"creates API key with optional expiration, redirects back and displays base64"
+ * 3-"deletes multiple api keys using bulk select"
+ * 4-"deletes api key using cta button"
+ * to functional tests to avoid flakyness
+ */
-jest.setTimeout(15000);
+describe('APIKeysGridPage', () => {
+ // We are spying on the console.error to avoid react to throw error
+ // in our test "displays error when fetching API keys fails"
+ // since we are using EuiErrorBoundary and react will console.error any errors
+ const consoleWarnMock = jest.spyOn(console, 'error').mockImplementation();
-const coreStart = coreMock.createStart();
+ const coreStart = coreMock.createStart();
+ const apiClientMock = apiKeysAPIClientMock.create();
+ const { authc } = securityMock.createSetup();
-const apiClientMock = apiKeysAPIClientMock.create();
-apiClientMock.checkPrivileges.mockResolvedValue({
- areApiKeysEnabled: true,
- canManage: true,
- isAdmin: true,
-});
-apiClientMock.getApiKeys.mockResolvedValue({
- apiKeys: [
- {
- creation: 1571322182082,
- expiration: 1571408582082,
- id: '0QQZ2m0BO2XZwgJFuWTT',
- invalidated: false,
- name: 'first-api-key',
- realm: 'reserved',
- username: 'elastic',
- },
- {
- creation: 1571322182082,
- expiration: 1571408582082,
- id: 'BO2XZwgJFuWTT0QQZ2m0',
- invalidated: false,
- name: 'second-api-key',
- realm: 'reserved',
- username: 'elastic',
- },
- ],
-});
+ beforeEach(() => {
+ apiClientMock.checkPrivileges.mockClear();
+ apiClientMock.getApiKeys.mockClear();
+ coreStart.http.get.mockClear();
+ coreStart.http.post.mockClear();
+ authc.getCurrentUser.mockClear();
-const authc = securityMock.createSetup().authc;
-authc.getCurrentUser.mockResolvedValue(
- mockAuthenticatedUser({
- username: 'jdoe',
- full_name: '',
- email: '',
- enabled: true,
- roles: ['superuser'],
- })
-);
+ apiClientMock.checkPrivileges.mockResolvedValue({
+ areApiKeysEnabled: true,
+ canManage: true,
+ isAdmin: true,
+ });
+ apiClientMock.getApiKeys.mockResolvedValue({
+ apiKeys: [
+ {
+ creation: 1571322182082,
+ expiration: 1571408582082,
+ id: '0QQZ2m0BO2XZwgJFuWTT',
+ invalidated: false,
+ name: 'first-api-key',
+ realm: 'reserved',
+ username: 'elastic',
+ },
+ {
+ creation: 1571322182082,
+ expiration: 1571408582082,
+ id: 'BO2XZwgJFuWTT0QQZ2m0',
+ invalidated: false,
+ name: 'second-api-key',
+ realm: 'reserved',
+ username: 'elastic',
+ },
+ ],
+ });
-// FLAKY: https://github.com/elastic/kibana/issues/97085
-describe.skip('APIKeysGridPage', () => {
+ authc.getCurrentUser.mockResolvedValue(
+ mockAuthenticatedUser({
+ username: 'jdoe',
+ full_name: '',
+ email: '',
+ enabled: true,
+ roles: ['superuser'],
+ })
+ );
+ });
it('loads and displays API keys', async () => {
const history = createMemoryHistory({ initialEntries: ['/'] });
- const { getByText } = render(
+ const { findByText } = render(
{
);
- await waitForElementToBeRemoved(() => getByText(/Loading API keys/));
- getByText(/first-api-key/);
- getByText(/second-api-key/);
+ expect(await findByText(/Loading API keys/)).not.toBeInTheDocument();
+ await findByText(/first-api-key/);
+ await findByText(/second-api-key/);
+ });
+
+ afterAll(() => {
+ // Let's make sure we restore everything just in case
+ consoleWarnMock.mockRestore();
});
it('displays callout when API keys are disabled', async () => {
@@ -98,7 +112,7 @@ describe.skip('APIKeysGridPage', () => {
isAdmin: true,
});
- const { getByText } = render(
+ const { findByText } = render(
{
);
- await waitForElementToBeRemoved(() => getByText(/Loading API keys/));
- getByText(/API keys not enabled/);
+ expect(await findByText(/Loading API keys/)).not.toBeInTheDocument();
+ await findByText(/API keys not enabled/);
});
it('displays error when user does not have required permissions', async () => {
@@ -120,7 +134,7 @@ describe.skip('APIKeysGridPage', () => {
isAdmin: false,
});
- const { getByText } = render(
+ const { findByText } = render(
{
);
- await waitForElementToBeRemoved(() => getByText(/Loading API keys/));
- getByText(/You need permission to manage API keys/);
+ expect(await findByText(/Loading API keys/)).not.toBeInTheDocument();
+ await findByText(/You need permission to manage API keys/);
});
it('displays error when fetching API keys fails', async () => {
apiClientMock.getApiKeys.mockRejectedValueOnce({
- body: { error: 'Internal Server Error', message: '', statusCode: 500 },
- });
- const history = createMemoryHistory({ initialEntries: ['/'] });
-
- const { getByText } = render(
-
-
-
- );
-
- await waitForElementToBeRemoved(() => getByText(/Loading API keys/));
- getByText(/Could not load API keys/);
- });
-
- it('creates API key when submitting form, redirects back and displays base64', async () => {
- const history = createMemoryHistory({ initialEntries: ['/create'] });
- coreStart.http.get.mockResolvedValue([{ name: 'superuser' }]);
- coreStart.http.post.mockResolvedValue({ id: '1D', api_key: 'AP1_K3Y' });
-
- const { findByRole, findByDisplayValue } = render(
-
-
-
- );
- expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role');
-
- const dialog = await findByRole('dialog');
-
- fireEvent.click(await findByRole('button', { name: 'Create API key' }));
-
- const alert = await findByRole('alert');
- within(alert).getByText(/Enter a name/i);
-
- fireEvent.change(await within(dialog).findByLabelText('Name'), {
- target: { value: 'Test' },
- });
-
- fireEvent.click(await findByRole('button', { name: 'Create API key' }));
-
- await waitFor(() => {
- expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/api_key', {
- body: JSON.stringify({ name: 'Test' }),
- });
- expect(history.location.pathname).toBe('/');
- });
-
- await findByDisplayValue(btoa('1D:AP1_K3Y'));
- });
-
- it('creates API key with optional expiration, redirects back and displays base64', async () => {
- const history = createMemoryHistory({ initialEntries: ['/create'] });
- coreStart.http.get.mockResolvedValue([{ name: 'superuser' }]);
- coreStart.http.post.mockResolvedValue({ id: '1D', api_key: 'AP1_K3Y' });
-
- const { findByRole, findByDisplayValue } = render(
-
-
-
- );
- expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role');
-
- const dialog = await findByRole('dialog');
-
- fireEvent.change(await within(dialog).findByLabelText('Name'), {
- target: { value: 'Test' },
- });
-
- fireEvent.click(await within(dialog).findByLabelText('Expire after time'));
-
- fireEvent.click(await findByRole('button', { name: 'Create API key' }));
-
- const alert = await findByRole('alert');
- within(alert).getByText(/Enter a valid duration or disable this option\./i);
-
- fireEvent.change(await within(dialog).findByLabelText('Lifetime (days)'), {
- target: { value: '12' },
- });
-
- fireEvent.click(await findByRole('button', { name: 'Create API key' }));
-
- await waitFor(() => {
- expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/api_key', {
- body: JSON.stringify({ name: 'Test', expiration: '12d' }),
- });
- expect(history.location.pathname).toBe('/');
+ body: {
+ error: 'Internal Server Error',
+ message: 'Internal Server Error',
+ statusCode: 500,
+ },
});
-
- await findByDisplayValue(btoa('1D:AP1_K3Y'));
- });
-
- it('deletes api key using cta button', async () => {
- const history = createMemoryHistory({ initialEntries: ['/'] });
-
- const { findByRole, findAllByLabelText } = render(
-
-
-
- );
-
- const [deleteButton] = await findAllByLabelText(/Delete/i);
- fireEvent.click(deleteButton);
-
- const dialog = await findByRole('dialog');
- fireEvent.click(await within(dialog).findByRole('button', { name: 'Delete API key' }));
-
- await waitFor(() => {
- expect(apiClientMock.invalidateApiKeys).toHaveBeenLastCalledWith(
- [{ id: '0QQZ2m0BO2XZwgJFuWTT', name: 'first-api-key' }],
- true
- );
- });
- });
-
- it('deletes multiple api keys using bulk select', async () => {
const history = createMemoryHistory({ initialEntries: ['/'] });
- const { findByRole, findAllByRole } = render(
+ const { findByText } = render(
{
);
- const deleteCheckboxes = await findAllByRole('checkbox', { name: 'Select this row' });
- deleteCheckboxes.forEach((checkbox) => fireEvent.click(checkbox));
- fireEvent.click(await findByRole('button', { name: 'Delete API keys' }));
-
- const dialog = await findByRole('dialog');
- fireEvent.click(await within(dialog).findByRole('button', { name: 'Delete API keys' }));
-
- await waitFor(() => {
- expect(apiClientMock.invalidateApiKeys).toHaveBeenLastCalledWith(
- [
- { id: '0QQZ2m0BO2XZwgJFuWTT', name: 'first-api-key' },
- { id: 'BO2XZwgJFuWTT0QQZ2m0', name: 'second-api-key' },
- ],
- true
- );
- });
+ expect(await findByText(/Loading API keys/)).not.toBeInTheDocument();
+ await findByText(/Could not load API keys/);
});
});
diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx
index dcf2a7bfe5165..a4843e4637d8b 100644
--- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx
+++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx
@@ -164,6 +164,7 @@ export class APIKeysGridPage extends Component {
{...reactRouterNavigate(this.props.history, '/create')}
fill
iconType="plusInCircleFilled"
+ data-test-subj="apiKeysCreatePromptButton"
>
{
{...reactRouterNavigate(this.props.history, '/create')}
fill
iconType="plusInCircleFilled"
+ data-test-subj="apiKeysCreateTableButton"
>
{
color: 'danger',
onClick: (item) =>
invalidateApiKeyPrompt([{ id: item.id, name: item.name }], this.onApiKeysInvalidated),
+ 'data-test-subj': 'apiKeysTableDeleteAction',
},
],
},
diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx
index e1ffc3b4b3515..f2fa6f7de468e 100644
--- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx
+++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx
@@ -202,6 +202,7 @@ export const CreateApiKeyFlyout: FunctionComponent = ({
isInvalid={form.touched.name && !!form.errors.name}
inputRef={firstFieldRef}
fullWidth
+ data-test-subj="apiKeyNameInput"
/>
@@ -258,6 +259,7 @@ export const CreateApiKeyFlyout: FunctionComponent = ({
)}
checked={!!form.values.customExpiration}
onChange={(e) => form.setValue('customExpiration', e.target.checked)}
+ data-test-subj="apiKeyCustomExpirationSwitch"
/>
{form.values.customExpiration && (
<>
@@ -284,6 +286,7 @@ export const CreateApiKeyFlyout: FunctionComponent = ({
defaultValue={form.values.expiration}
isInvalid={form.touched.expiration && !!form.errors.expiration}
fullWidth
+ data-test-subj="apiKeyCustomExpirationInput"
/>
diff --git a/x-pack/test/functional/apps/api_keys/home_page.ts b/x-pack/test/functional/apps/api_keys/home_page.ts
index be8f128359345..5907247527585 100644
--- a/x-pack/test/functional/apps/api_keys/home_page.ts
+++ b/x-pack/test/functional/apps/api_keys/home_page.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
@@ -13,6 +14,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const security = getService('security');
const testSubjects = getService('testSubjects');
const find = getService('find');
+ const browser = getService('browser');
describe('Home page', function () {
before(async () => {
@@ -34,5 +36,90 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
log.debug('Checking for create API key call to action');
await find.existsByLinkText('Create API key');
});
+
+ describe('creates API key', function () {
+ before(async () => {
+ await security.testUser.setRoles(['kibana_admin']);
+ await security.testUser.setRoles(['test_api_keys']);
+ await pageObjects.common.navigateToApp('apiKeys');
+ });
+
+ afterEach(async () => {
+ await pageObjects.apiKeys.deleteAllApiKeyOneByOne();
+ });
+
+ it('when submitting form, close dialog and displays new api key', async () => {
+ const apiKeyName = 'Happy API Key';
+ await pageObjects.apiKeys.clickOnPromptCreateApiKey();
+ expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys/create');
+
+ await pageObjects.apiKeys.setApiKeyName(apiKeyName);
+ await pageObjects.apiKeys.submitOnCreateApiKey();
+ const newApiKeyCreation = await pageObjects.apiKeys.getNewApiKeyCreation();
+
+ expect(await browser.getCurrentUrl()).to.not.contain(
+ 'app/management/security/api_keys/create'
+ );
+ expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys');
+ expect(await pageObjects.apiKeys.isApiKeyModalExists()).to.be(false);
+ expect(newApiKeyCreation).to.be(`Created API key '${apiKeyName}'`);
+ });
+
+ it('with optional expiration, redirects back and displays base64', async () => {
+ const apiKeyName = 'Happy expiration API key';
+ await pageObjects.apiKeys.clickOnPromptCreateApiKey();
+ expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys/create');
+
+ await pageObjects.apiKeys.setApiKeyName(apiKeyName);
+ await pageObjects.apiKeys.toggleCustomExpiration();
+ await pageObjects.apiKeys.submitOnCreateApiKey();
+ expect(await pageObjects.apiKeys.getErrorCallOutText()).to.be(
+ 'Enter a valid duration or disable this option.'
+ );
+
+ await pageObjects.apiKeys.setApiKeyCustomExpiration('12');
+ await pageObjects.apiKeys.submitOnCreateApiKey();
+ const newApiKeyCreation = await pageObjects.apiKeys.getNewApiKeyCreation();
+
+ expect(await browser.getCurrentUrl()).to.not.contain(
+ 'app/management/security/api_keys/create'
+ );
+ expect(await browser.getCurrentUrl()).to.contain('app/management/security/api_keys');
+ expect(await pageObjects.apiKeys.isApiKeyModalExists()).to.be(false);
+ expect(newApiKeyCreation).to.be(`Created API key '${apiKeyName}'`);
+ });
+ });
+
+ describe('deletes API key(s)', function () {
+ before(async () => {
+ await security.testUser.setRoles(['kibana_admin']);
+ await security.testUser.setRoles(['test_api_keys']);
+ await pageObjects.common.navigateToApp('apiKeys');
+ });
+
+ beforeEach(async () => {
+ await pageObjects.apiKeys.clickOnPromptCreateApiKey();
+ await pageObjects.apiKeys.setApiKeyName('api key 1');
+ await pageObjects.apiKeys.submitOnCreateApiKey();
+ });
+
+ it('one by one', async () => {
+ await pageObjects.apiKeys.deleteAllApiKeyOneByOne();
+ expect(await pageObjects.apiKeys.getApiKeysFirstPromptTitle()).to.be(
+ 'Create your first API key'
+ );
+ });
+
+ it('by bulk', async () => {
+ await pageObjects.apiKeys.clickOnTableCreateApiKey();
+ await pageObjects.apiKeys.setApiKeyName('api key 2');
+ await pageObjects.apiKeys.submitOnCreateApiKey();
+
+ await pageObjects.apiKeys.bulkDeleteApiKeys();
+ expect(await pageObjects.apiKeys.getApiKeysFirstPromptTitle()).to.be(
+ 'Create your first API key'
+ );
+ });
+ });
});
};
diff --git a/x-pack/test/functional/page_objects/api_keys_page.ts b/x-pack/test/functional/page_objects/api_keys_page.ts
index 99c2aa01a4eda..9349eaa4bda0c 100644
--- a/x-pack/test/functional/page_objects/api_keys_page.ts
+++ b/x-pack/test/functional/page_objects/api_keys_page.ts
@@ -9,6 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context';
export function ApiKeysPageProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
+ const find = getService('find');
return {
async noAPIKeysHeading() {
@@ -26,5 +27,70 @@ export function ApiKeysPageProvider({ getService }: FtrProviderContext) {
async apiKeysPermissionDeniedMessage() {
return await testSubjects.getVisibleText('apiKeysPermissionDeniedMessage');
},
+
+ async clickOnPromptCreateApiKey() {
+ return await testSubjects.click('apiKeysCreatePromptButton');
+ },
+
+ async clickOnTableCreateApiKey() {
+ return await testSubjects.click('apiKeysCreateTableButton');
+ },
+
+ async setApiKeyName(apiKeyName: string) {
+ return await testSubjects.setValue('apiKeyNameInput', apiKeyName);
+ },
+
+ async setApiKeyCustomExpiration(expirationTime: string) {
+ return await testSubjects.setValue('apiKeyCustomExpirationInput', expirationTime);
+ },
+
+ async submitOnCreateApiKey() {
+ return await testSubjects.click('formFlyoutSubmitButton');
+ },
+
+ async isApiKeyModalExists() {
+ return await find.existsByCssSelector('[role="dialog"]');
+ },
+
+ async getNewApiKeyCreation() {
+ const euiCallOutHeader = await find.byCssSelector('.euiCallOutHeader__title');
+ return euiCallOutHeader.getVisibleText();
+ },
+
+ async toggleCustomExpiration() {
+ return await testSubjects.click('apiKeyCustomExpirationSwitch');
+ },
+
+ async getErrorCallOutText() {
+ const alertElem = await find.byCssSelector('[role="dialog"] [role="alert"] .euiText');
+ return await alertElem.getVisibleText();
+ },
+
+ async getApiKeysFirstPromptTitle() {
+ const titlePromptElem = await find.byCssSelector('.euiEmptyPrompt .euiTitle');
+ return await titlePromptElem.getVisibleText();
+ },
+
+ async deleteAllApiKeyOneByOne() {
+ const hasApiKeysToDelete = await testSubjects.exists('apiKeysTableDeleteAction');
+ if (hasApiKeysToDelete) {
+ const apiKeysToDelete = await testSubjects.findAll('apiKeysTableDeleteAction');
+ for (const element of apiKeysToDelete) {
+ await element.click();
+ await testSubjects.click('confirmModalConfirmButton');
+ }
+ }
+ },
+
+ async bulkDeleteApiKeys() {
+ const hasApiKeysToDelete = await testSubjects.exists('checkboxSelectAll', {
+ allowHidden: true,
+ });
+ if (hasApiKeysToDelete) {
+ await testSubjects.click('checkboxSelectAll');
+ await testSubjects.click('bulkInvalidateActionButton');
+ await testSubjects.click('confirmModalConfirmButton');
+ }
+ },
};
}