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

[Enterprise Search] Added an App Search route for listing Credentials #75487

Merged
merged 17 commits into from
Aug 27, 2020
Merged
Show file tree
Hide file tree
Changes from 16 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,7 +5,7 @@
*/
JasonStoltz marked this conversation as resolved.
Show resolved Hide resolved

import { loggingSystemMock } from 'src/core/server/mocks';
import { ConfigType } from '../../';
import { ConfigType } from '../';

export const mockLogger = loggingSystemMock.createLogger().get();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { mockLogger } from '../../routes/__mocks__';
import { mockLogger } from '../../__mocks__';

import { registerTelemetryUsageCollector } from './telemetry';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { mockLogger } from '../../routes/__mocks__';
import { mockLogger } from '../../__mocks__';

jest.mock('../../../../../../src/core/server', () => ({
SavedObjectsErrorHelpers: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { mockLogger } from '../../routes/__mocks__';
import { mockLogger } from '../../__mocks__';

import { registerTelemetryUsageCollector } from './telemetry';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { mockConfig, mockLogger } from '../__mocks__';

import { createEnterpriseSearchRequestHandler } from './enterprise_search_request_handler';

jest.mock('node-fetch');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fetchMock = require('node-fetch') as jest.Mock;
const { Response } = jest.requireActual('node-fetch');

const responseMock = {
ok: jest.fn(),
customError: jest.fn(),
};
const KibanaAuthHeader = 'Basic 123';

describe('createEnterpriseSearchRequestHandler', () => {
beforeEach(() => {
jest.clearAllMocks()
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
fetchMock.mockReset();
});

it('makes an API call and returns the response', async () => {
const responseBody = {
results: [{ name: 'engine1' }],
meta: { page: { total_results: 1 } },
};

EnterpriseSearchAPI.mockReturn(responseBody);

const requestHandler = createEnterpriseSearchRequestHandler({
config: mockConfig,
log: mockLogger,
path: '/as/credentials/collection',
});

await makeAPICall(requestHandler, {
query: {
type: 'indexed',
pageIndex: 1,
},
});

EnterpriseSearchAPI.shouldHaveBeenCalledWith(
'http://localhost:3002/as/credentials/collection?type=indexed&pageIndex=1'
);

expect(responseMock.ok).toHaveBeenCalledWith({
body: responseBody,
});
});

describe('when an API request fails', () => {
it('should return 502 with a message', async () => {
EnterpriseSearchAPI.mockReturnError();

const requestHandler = createEnterpriseSearchRequestHandler({
config: mockConfig,
log: mockLogger,
path: '/as/credentials/collection',
});

await makeAPICall(requestHandler);

EnterpriseSearchAPI.shouldHaveBeenCalledWith(
'http://localhost:3002/as/credentials/collection'
);

expect(responseMock.customError).toHaveBeenCalledWith({
body: 'Error connecting or fetching data from Enterprise Search',
statusCode: 502,
});
});
});

describe('when `hasValidData` fails', () => {
it('should return 502 with a message', async () => {
const responseBody = {
foo: 'bar',
};

EnterpriseSearchAPI.mockReturn(responseBody);

const requestHandler = createEnterpriseSearchRequestHandler({
config: mockConfig,
log: mockLogger,
path: '/as/credentials/collection',
hasValidData: (body?: any) =>
Array.isArray(body?.results) && typeof body?.meta?.page?.total_results === 'number',
});

await makeAPICall(requestHandler);

EnterpriseSearchAPI.shouldHaveBeenCalledWith(
'http://localhost:3002/as/credentials/collection'
);

expect(responseMock.customError).toHaveBeenCalledWith({
body: 'Error connecting or fetching data from Enterprise Search',
statusCode: 502,
});
});
});
});

const makeAPICall = (handler: Function, params = {}) => {
const request = { headers: { authorization: KibanaAuthHeader }, ...params };
return handler(null, request, responseMock);
};

const EnterpriseSearchAPI = {
shouldHaveBeenCalledWith(expectedUrl: string, expectedParams = {}) {
expect(fetchMock).toHaveBeenCalledWith(expectedUrl, {
headers: { Authorization: KibanaAuthHeader },
...expectedParams,
});
},
mockReturn(response: object) {
fetchMock.mockImplementation(() => {
return Promise.resolve(new Response(JSON.stringify(response)));
});
},
mockReturnError() {
fetchMock.mockImplementation(() => {
return Promise.reject('Failed');
});
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import fetch from 'node-fetch';
import querystring from 'querystring';
import {
RequestHandlerContext,
KibanaRequest,
KibanaResponseFactory,
Logger,
} from 'src/core/server';
import { ConfigType } from '../index';

interface IEnterpriseSearchRequestParams<ResponseBody> {
config: ConfigType;
log: Logger;
path: string;
hasValidData?: (body?: ResponseBody) => boolean;
}

/**
* This helper function creates a single standard DRY way of handling
* Enterprise Search API requests.
*
* This handler assumes that it will essentially just proxy the
* Enterprise Search API request, so the request body and request
* parameters are simply passed through.
*/
export function createEnterpriseSearchRequestHandler<ResponseBody>({
config,
log,
path,
hasValidData = () => true,
}: IEnterpriseSearchRequestParams<ResponseBody>) {
return async (
_context: RequestHandlerContext,
request: KibanaRequest<unknown, Readonly<{}>, unknown>,
response: KibanaResponseFactory
) => {
try {
const enterpriseSearchUrl = config.host as string;
const params = request.query ? `?${querystring.stringify(request.query)}` : '';
const url = `${encodeURI(enterpriseSearchUrl)}${path}${params}`;

const apiResponse = await fetch(url, {
headers: { Authorization: request.headers.authorization as string },
});

const body = await apiResponse.json();

if (hasValidData(body)) {
return response.ok({ body });
} else {
throw new Error(`Invalid data received: ${JSON.stringify(body)}`);
}
} catch (e) {
log.error(`Cannot connect to Enterprise Search: ${e.toString()}`);
if (e instanceof Error) log.debug(e.stack as string);

return response.customError({
statusCode: 502,
body: 'Error connecting or fetching data from Enterprise Search',
});
}
};
}
2 changes: 2 additions & 0 deletions x-pack/plugins/enterprise_search/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { registerTelemetryRoute } from './routes/enterprise_search/telemetry';
import { appSearchTelemetryType } from './saved_objects/app_search/telemetry';
import { registerTelemetryUsageCollector as registerASTelemetryUsageCollector } from './collectors/app_search/telemetry';
import { registerEnginesRoute } from './routes/app_search/engines';
import { registerCredentialsRoutes } from './routes/app_search/credentials';

import { workplaceSearchTelemetryType } from './saved_objects/workplace_search/telemetry';
import { registerTelemetryUsageCollector as registerWSTelemetryUsageCollector } from './collectors/workplace_search/telemetry';
Expand Down Expand Up @@ -107,6 +108,7 @@ export class EnterpriseSearchPlugin implements Plugin {

registerConfigDataRoute(dependencies);
registerEnginesRoute(dependencies);
registerCredentialsRoutes(dependencies);
registerWSOverviewRoute(dependencies);

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { MockRouter, mockConfig, mockLogger } from '../../__mocks__';

import { registerCredentialsRoutes } from './credentials';

jest.mock('../../lib/enterprise_search_request_handler', () => ({
createEnterpriseSearchRequestHandler: jest.fn(),
}));
import { createEnterpriseSearchRequestHandler } from '../../lib/enterprise_search_request_handler';

describe('credentials routes', () => {
describe('GET /api/app_search/credentials', () => {
let mockRouter: MockRouter;

beforeEach(() => {
jest.clearAllMocks();
mockRouter = new MockRouter({ method: 'get', payload: 'query' });

registerCredentialsRoutes({
router: mockRouter.router,
log: mockLogger,
config: mockConfig,
});
});

it('creates a handler with createEnterpriseSearchRequestHandler', () => {
expect(createEnterpriseSearchRequestHandler).toHaveBeenCalledWith({
config: mockConfig,
log: mockLogger,
path: '/as/credentials/collection',
hasValidData: expect.any(Function),
});
});

describe('hasValidData', () => {
it('should correctly validate that a response has data', () => {
const response = {
meta: {
page: {
current: 1,
total_pages: 1,
total_results: 1,
size: 25,
},
},
results: [
{
id: 'loco_moco_account_id:5f3575de2b76ff13405f3155|name:asdfasdf',
key: 'search-fe49u2z8d5gvf9s4ekda2ad4',
name: 'asdfasdf',
type: 'search',
access_all_engines: true,
},
],
};

const {
hasValidData,
} = (createEnterpriseSearchRequestHandler as jest.Mock).mock.calls[0][0];

expect(hasValidData(response)).toBe(true);
});

it('should correctly validate that a response does not have data', () => {
const response = {
foo: 'bar',
};

const hasValidData = (createEnterpriseSearchRequestHandler as jest.Mock).mock.calls[0][0]
.hasValidData;

expect(hasValidData(response)).toBe(false);
JasonStoltz marked this conversation as resolved.
Show resolved Hide resolved
});
});

describe('validates', () => {
it('correctly', () => {
const request = { query: { 'page[current]': 1 } };
mockRouter.shouldValidate(request);
});

it('missing page[current]', () => {
const request = { query: {} };
mockRouter.shouldThrow(request);
});
});
});
});
Loading