Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.16] [SECURITY] Flaky api keys grid (#116991) #117561

Merged
merged 1 commit into from
Nov 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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(
<Providers services={coreStart} authc={authc} history={history}>
<APIKeysGridPage
apiKeysAPIClient={apiClientMock}
Expand All @@ -85,9 +94,14 @@ describe.skip('APIKeysGridPage', () => {
</Providers>
);

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 () => {
Expand All @@ -98,7 +112,7 @@ describe.skip('APIKeysGridPage', () => {
isAdmin: true,
});

const { getByText } = render(
const { findByText } = render(
<Providers services={coreStart} authc={authc} history={history}>
<APIKeysGridPage
apiKeysAPIClient={apiClientMock}
Expand All @@ -108,8 +122,8 @@ describe.skip('APIKeysGridPage', () => {
</Providers>
);

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 () => {
Expand All @@ -120,7 +134,7 @@ describe.skip('APIKeysGridPage', () => {
isAdmin: false,
});

const { getByText } = render(
const { findByText } = render(
<Providers services={coreStart} authc={authc} history={history}>
<APIKeysGridPage
apiKeysAPIClient={apiClientMock}
Expand All @@ -130,145 +144,21 @@ describe.skip('APIKeysGridPage', () => {
</Providers>
);

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(
<Providers services={coreStart} authc={authc} history={history}>
<APIKeysGridPage
apiKeysAPIClient={apiClientMock}
notifications={coreStart.notifications}
history={history}
/>
</Providers>
);

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(
<Providers services={coreStart} authc={authc} history={history}>
<APIKeysGridPage
apiKeysAPIClient={apiClientMock}
notifications={coreStart.notifications}
history={history}
/>
</Providers>
);
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(
<Providers services={coreStart} authc={authc} history={history}>
<APIKeysGridPage
apiKeysAPIClient={apiClientMock}
notifications={coreStart.notifications}
history={history}
/>
</Providers>
);
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(
<Providers services={coreStart} authc={authc} history={history}>
<APIKeysGridPage
apiKeysAPIClient={apiClientMock}
notifications={coreStart.notifications}
history={history}
/>
</Providers>
);

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(
<Providers services={coreStart} authc={authc} history={history}>
<APIKeysGridPage
apiKeysAPIClient={apiClientMock}
Expand All @@ -278,21 +168,7 @@ describe.skip('APIKeysGridPage', () => {
</Providers>
);

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/);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export class APIKeysGridPage extends Component<Props, State> {
{...reactRouterNavigate(this.props.history, '/create')}
fill
iconType="plusInCircleFilled"
data-test-subj="apiKeysCreatePromptButton"
>
<FormattedMessage
id="xpack.security.management.apiKeys.table.createButton"
Expand Down Expand Up @@ -207,6 +208,7 @@ export class APIKeysGridPage extends Component<Props, State> {
{...reactRouterNavigate(this.props.history, '/create')}
fill
iconType="plusInCircleFilled"
data-test-subj="apiKeysCreateTableButton"
>
<FormattedMessage
id="xpack.security.management.apiKeys.table.createButton"
Expand Down Expand Up @@ -597,6 +599,7 @@ export class APIKeysGridPage extends Component<Props, State> {
color: 'danger',
onClick: (item) =>
invalidateApiKeyPrompt([{ id: item.id, name: item.name }], this.onApiKeysInvalidated),
'data-test-subj': 'apiKeysTableDeleteAction',
},
],
},
Expand Down
Loading