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

[App Search] Credentials: Add final Logic and server routes #81519

Merged
merged 8 commits into from
Oct 22, 2020
Merged
Show file tree
Hide file tree
Changes from 6 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 Down Expand Up @@ -1081,10 +1091,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 +1106,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 +1124,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 +1137,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 +1155,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 +1167,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 +1177,147 @@ describe('CredentialsLogic', () => {
}
});
});

describe('onApiTokenChange', () => {
it('calls a POST API endpoint that creates a new token', async () => {
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
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 existing API tokens', async () => {
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
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('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
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
import { kea, MakeLogicType } from 'kea';

import { formatApiName } from '../../utils/format_api_name';
import { ApiTokenTypes, DELETE_MESSAGE } from './constants';
import { ApiTokenTypes, CREATE_MESSAGE, UPDATE_MESSAGE, DELETE_MESSAGE } from './constants';

import { HttpLogic } from '../../../shared/http';
import {
FlashMessagesLogic,
setSuccessMessage,
flashAPIErrors,
} from '../../../shared/flash_messages';
import { AppLogic } from '../../app_logic';

import { IMeta } from '../../../../../common/types';
import { IEngine } from '../../types';
Expand Down Expand Up @@ -49,6 +50,8 @@ interface ICredentialsLogicActions {
fetchCredentials(page?: number): number;
fetchDetails(): { value: boolean };
deleteApiKey(tokenName: string): string;
onApiTokenChange(): void;
onEngineSelect(engineName: string): string;
}

interface ICredentialsLogicValues {
Expand Down Expand Up @@ -92,6 +95,8 @@ export const CredentialsLogic = kea<
fetchCredentials: (page) => page,
fetchDetails: true,
deleteApiKey: (tokenName) => tokenName,
onApiTokenChange: () => null,
onEngineSelect: (engineName) => engineName,
}),
reducers: () => ({
apiTokens: [
Expand Down Expand Up @@ -204,7 +209,10 @@ export const CredentialsLogic = kea<
],
}),
selectors: ({ selectors }) => ({
// TODO fullEngineAccessChecked from ent-search
fullEngineAccessChecked: [
() => [AppLogic.selectors.myRole, selectors.activeApiToken],
(myRole, activeApiToken) => myRole.canAccessAllEngines && !!activeApiToken.access_all_engines,
],
dataLoading: [
() => [selectors.isCredentialsDetailsComplete, selectors.isCredentialsDataComplete],
(isCredentialsDetailsComplete, isCredentialsDataComplete) => {
Expand Down Expand Up @@ -255,7 +263,54 @@ export const CredentialsLogic = kea<
flashAPIErrors(e);
}
},
// TODO onApiTokenChange from ent-search
// TODO onEngineSelect from ent-search
onApiTokenChange: async () => {
const { myRole } = AppLogic.values;
const {
id,
name,
engines,
type,
read,
write,
access_all_engines: accessAllEngines,
} = values.activeApiToken;

const data: IApiToken = {
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
name,
type,
};
if (type === ApiTokenTypes.Private) {
data.read = read;
data.write = write;
}
if (type !== ApiTokenTypes.Admin) {
data.access_all_engines = !!(accessAllEngines && myRole.canAccessAllEngines);
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
data.engines = engines;
}

try {
const { http } = HttpLogic.values;
const body = JSON.stringify(data);

if (id) {
const response = await http.put(`/api/app_search/credentials/${name}`, { body });
actions.onApiTokenUpdateSuccess(response);
setSuccessMessage(UPDATE_MESSAGE);
} else {
const response = await http.post('/api/app_search/credentials', { body });
actions.onApiTokenCreateSuccess(response);
setSuccessMessage(CREATE_MESSAGE);
}
} catch (e) {
flashAPIErrors(e);
}
},
onEngineSelect: (engineName: string) => {
if (values.activeApiToken?.engines?.includes(engineName)) {
actions.removeEngineName(engineName);
} else {
actions.addEngineName(engineName);
}
},
}),
});
Loading