-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Enterprise Search] Added an App Search route for listing Credentials (…
…#75487) In addition to a route for listing Credentials, this also adds a utility function which helps create API routes which simply proxy the App Search API. The reasoning for this is as follows; 1. Creating new routes takes less effort and cognitive load if we can simply just create proxy routes that use the APIs as is. 2. It keeps the App Search API as the source of truth. All logic is implemented in the underlying API. 3. It makes unit testing routes much simpler. We do not need to verify any connectivity to the underlying App Search API, because that is already tested as part of the utility.
- Loading branch information
1 parent
b802af8
commit f065191
Showing
15 changed files
with
355 additions
and
8 deletions.
There are no files selected for viewing
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
133 changes: 133 additions & 0 deletions
133
x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
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'); | ||
}); | ||
}, | ||
}; |
69 changes: 69 additions & 0 deletions
69
x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}); | ||
} | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
|
||
describe('validates', () => { | ||
it('correctly', () => { | ||
const request = { query: { 'page[current]': 1 } }; | ||
mockRouter.shouldValidate(request); | ||
}); | ||
|
||
it('missing page[current]', () => { | ||
const request = { query: {} }; | ||
mockRouter.shouldThrow(request); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.