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.x] [App Search] Credentials: Add final Logic and server routes (#81519) #81540

Merged
merged 2 commits into from
Oct 26, 2020
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 @@ -18,6 +18,7 @@ describe('CredentialsFlyoutFooter', () => {
};
const actions = {
hideCredentialsForm: jest.fn(),
onApiTokenChange: jest.fn(),
};

beforeEach(() => {
Expand Down Expand Up @@ -59,6 +60,6 @@ describe('CredentialsFlyoutFooter', () => {
const button = wrapper.find('[data-test-subj="APIKeyActionButton"]');
button.simulate('click');

// TODO: Expect onApiTokenChange to have been called
expect(actions.onApiTokenChange).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { i18n } from '@kbn/i18n';
import { CredentialsLogic } from '../credentials_logic';

export const CredentialsFlyoutFooter: React.FC = () => {
const { hideCredentialsForm } = useActions(CredentialsLogic);
const { hideCredentialsForm, onApiTokenChange } = useActions(CredentialsLogic);
const { activeApiTokenExists } = useValues(CredentialsLogic);

return (
Expand All @@ -33,7 +33,7 @@ export const CredentialsFlyoutFooter: React.FC = () => {
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => window.alert('submit')}
onClick={onApiTokenChange}
fill={true}
color="secondary"
iconType="check"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@

import { resetContext } from 'kea';

import { CredentialsLogic } from './credentials_logic';
import { ApiTokenTypes } from './constants';

import { mockHttpValues } from '../../../__mocks__';
jest.mock('../../../shared/http', () => ({
HttpLogic: { values: { http: { get: jest.fn(), delete: jest.fn() } } },
HttpLogic: { values: mockHttpValues },
}));
import { HttpLogic } from '../../../shared/http';
const { http } = mockHttpValues;

jest.mock('../../../shared/flash_messages', () => ({
FlashMessagesLogic: { actions: { clearFlashMessages: jest.fn() } },
setSuccessMessage: jest.fn(),
Expand All @@ -24,6 +23,17 @@ import {
flashAPIErrors,
} from '../../../shared/flash_messages';

jest.mock('../../app_logic', () => ({
AppLogic: {
selectors: { myRole: jest.fn(() => ({})) },
values: { myRole: jest.fn(() => ({})) },
},
}));
import { AppLogic } from '../../app_logic';

import { ApiTokenTypes } from './constants';
import { CredentialsLogic } from './credentials_logic';

describe('CredentialsLogic', () => {
const DEFAULT_VALUES = {
activeApiToken: {
Expand All @@ -44,6 +54,7 @@ describe('CredentialsLogic', () => {
meta: {},
nameInputBlurred: false,
shouldShowCredentialsForm: false,
fullEngineAccessChecked: false,
};

const mount = (defaults?: object) => {
Expand Down Expand Up @@ -1081,10 +1092,10 @@ describe('CredentialsLogic', () => {
mount();
jest.spyOn(CredentialsLogic.actions, 'setCredentialsData').mockImplementationOnce(() => {});
const promise = Promise.resolve({ meta, results });
(HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise);
http.get.mockReturnValue(promise);

CredentialsLogic.actions.fetchCredentials(2);
expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/app_search/credentials', {
expect(http.get).toHaveBeenCalledWith('/api/app_search/credentials', {
query: {
'page[current]': 2,
},
Expand All @@ -1096,7 +1107,7 @@ describe('CredentialsLogic', () => {
it('handles errors', async () => {
mount();
const promise = Promise.reject('An error occured');
(HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise);
http.get.mockReturnValue(promise);

CredentialsLogic.actions.fetchCredentials();
try {
Expand All @@ -1114,12 +1125,10 @@ describe('CredentialsLogic', () => {
.spyOn(CredentialsLogic.actions, 'setCredentialsDetails')
.mockImplementationOnce(() => {});
const promise = Promise.resolve(credentialsDetails);
(HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise);
http.get.mockReturnValue(promise);

CredentialsLogic.actions.fetchDetails();
expect(HttpLogic.values.http.get).toHaveBeenCalledWith(
'/api/app_search/credentials/details'
);
expect(http.get).toHaveBeenCalledWith('/api/app_search/credentials/details');
await promise;
expect(CredentialsLogic.actions.setCredentialsDetails).toHaveBeenCalledWith(
credentialsDetails
Expand All @@ -1129,7 +1138,7 @@ describe('CredentialsLogic', () => {
it('handles errors', async () => {
mount();
const promise = Promise.reject('An error occured');
(HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise);
http.get.mockReturnValue(promise);

CredentialsLogic.actions.fetchDetails();
try {
Expand All @@ -1147,12 +1156,10 @@ describe('CredentialsLogic', () => {
mount();
jest.spyOn(CredentialsLogic.actions, 'onApiKeyDelete').mockImplementationOnce(() => {});
const promise = Promise.resolve();
(HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise);
http.delete.mockReturnValue(promise);

CredentialsLogic.actions.deleteApiKey(tokenName);
expect(HttpLogic.values.http.delete).toHaveBeenCalledWith(
`/api/app_search/credentials/${tokenName}`
);
expect(http.delete).toHaveBeenCalledWith(`/api/app_search/credentials/${tokenName}`);
await promise;
expect(CredentialsLogic.actions.onApiKeyDelete).toHaveBeenCalledWith(tokenName);
expect(setSuccessMessage).toHaveBeenCalled();
Expand All @@ -1161,7 +1168,7 @@ describe('CredentialsLogic', () => {
it('handles errors', async () => {
mount();
const promise = Promise.reject('An error occured');
(HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise);
http.delete.mockReturnValue(promise);

CredentialsLogic.actions.deleteApiKey(tokenName);
try {
Expand All @@ -1171,9 +1178,189 @@ describe('CredentialsLogic', () => {
}
});
});

describe('onApiTokenChange', () => {
it('calls a POST API endpoint that creates a new token if the active token does not exist yet', async () => {
const createdToken = {
name: 'new-key',
type: ApiTokenTypes.Admin,
};
mount({
activeApiToken: createdToken,
});
jest.spyOn(CredentialsLogic.actions, 'onApiTokenCreateSuccess');
const promise = Promise.resolve(createdToken);
http.post.mockReturnValue(promise);

CredentialsLogic.actions.onApiTokenChange();
expect(http.post).toHaveBeenCalledWith('/api/app_search/credentials', {
body: JSON.stringify(createdToken),
});
await promise;
expect(CredentialsLogic.actions.onApiTokenCreateSuccess).toHaveBeenCalledWith(createdToken);
expect(setSuccessMessage).toHaveBeenCalled();
});

it('calls a PUT endpoint that updates the active token if it already exists', async () => {
const updatedToken = {
name: 'test-key',
type: ApiTokenTypes.Private,
read: true,
write: false,
access_all_engines: false,
engines: ['engine1'],
};
mount({
activeApiToken: {
...updatedToken,
id: 'some-id',
},
});
jest.spyOn(CredentialsLogic.actions, 'onApiTokenUpdateSuccess');
const promise = Promise.resolve(updatedToken);
http.put.mockReturnValue(promise);

CredentialsLogic.actions.onApiTokenChange();
expect(http.put).toHaveBeenCalledWith('/api/app_search/credentials/test-key', {
body: JSON.stringify(updatedToken),
});
await promise;
expect(CredentialsLogic.actions.onApiTokenUpdateSuccess).toHaveBeenCalledWith(updatedToken);
expect(setSuccessMessage).toHaveBeenCalled();
});

it('handles errors', async () => {
mount();
const promise = Promise.reject('An error occured');
http.post.mockReturnValue(promise);

CredentialsLogic.actions.onApiTokenChange();
try {
await promise;
} catch {
expect(flashAPIErrors).toHaveBeenCalledWith('An error occured');
}
});

describe('token type data', () => {
it('does not send extra read/write/engine access data for admin tokens', () => {
const correctAdminToken = {
name: 'bogus-admin',
type: ApiTokenTypes.Admin,
};
const extraData = {
read: true,
write: true,
access_all_engines: true,
};
mount({ activeApiToken: { ...correctAdminToken, ...extraData } });

CredentialsLogic.actions.onApiTokenChange();
expect(http.post).toHaveBeenCalledWith('/api/app_search/credentials', {
body: JSON.stringify(correctAdminToken),
});
});

it('does not send extra read/write access data for search tokens', () => {
const correctSearchToken = {
name: 'bogus-search',
type: ApiTokenTypes.Search,
access_all_engines: false,
engines: ['some-engine'],
};
const extraData = {
read: true,
write: false,
};
mount({ activeApiToken: { ...correctSearchToken, ...extraData } });

CredentialsLogic.actions.onApiTokenChange();
expect(http.post).toHaveBeenCalledWith('/api/app_search/credentials', {
body: JSON.stringify(correctSearchToken),
});
});

// Private tokens send all data per the PUT test above.
// If that ever changes, we should capture that in another test here.
});
});

describe('onEngineSelect', () => {
it('calls addEngineName if the engine is not selected', () => {
mount({
activeApiToken: {
...DEFAULT_VALUES.activeApiToken,
engines: [],
},
});
jest.spyOn(CredentialsLogic.actions, 'addEngineName');

CredentialsLogic.actions.onEngineSelect('engine1');
expect(CredentialsLogic.actions.addEngineName).toHaveBeenCalledWith('engine1');
expect(CredentialsLogic.values.activeApiToken.engines).toEqual(['engine1']);
});

it('calls removeEngineName if the engine is already selected', () => {
mount({
activeApiToken: {
...DEFAULT_VALUES.activeApiToken,
engines: ['engine1', 'engine2'],
},
});
jest.spyOn(CredentialsLogic.actions, 'removeEngineName');

CredentialsLogic.actions.onEngineSelect('engine1');
expect(CredentialsLogic.actions.removeEngineName).toHaveBeenCalledWith('engine1');
expect(CredentialsLogic.values.activeApiToken.engines).toEqual(['engine2']);
});
});
});

describe('selectors', () => {
describe('fullEngineAccessChecked', () => {
it('should be true if active token is set to access all engines and the user can access all engines', () => {
(AppLogic.selectors.myRole as jest.Mock).mockReturnValueOnce({
canAccessAllEngines: true,
});
mount({
activeApiToken: {
...DEFAULT_VALUES.activeApiToken,
access_all_engines: true,
},
});

expect(CredentialsLogic.values.fullEngineAccessChecked).toEqual(true);
});

it('should be false if the token is not set to access all engines', () => {
(AppLogic.selectors.myRole as jest.Mock).mockReturnValueOnce({
canAccessAllEngines: true,
});
mount({
activeApiToken: {
...DEFAULT_VALUES.activeApiToken,
access_all_engines: false,
},
});

expect(CredentialsLogic.values.fullEngineAccessChecked).toEqual(false);
});

it('should be false if the user cannot acess all engines', () => {
(AppLogic.selectors.myRole as jest.Mock).mockReturnValueOnce({
canAccessAllEngines: false,
});
mount({
activeApiToken: {
...DEFAULT_VALUES.activeApiToken,
access_all_engines: true,
},
});

expect(CredentialsLogic.values.fullEngineAccessChecked).toEqual(false);
});
});

describe('activeApiTokenExists', () => {
it('should be false if the token has no id', () => {
mount({
Expand Down
Loading