mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[fix/UiSettings] ignore certain errors (#13079)
* [SavedObjectClient] emit detectable errors * [uiSettingsService] consume new SavedObjectsClient errors * [SavedObjectsClient] expose errorTypeHelpers as such * [elasticsearch/tests] recreate error for each test * [http] wait for elasticsearch plugin to be ready * [shortUrl/tests] ensure that create request responds with 200 * [shortUrl] use errorTypeHelpers to filter errors * [uiSettings/savedObjectsClientStub] stub errorTypeHelpers * [SavedObjectsClient/errors] expose error module so tests can make errors * [shortUrl/tests] use actual SavedObjectsClient errors * [uiSettings/savedObjectsClientStub] use actual errors lib * [SavedObjectsClient] use decorate instead of "wrap" * [server/routes/uiSettings] refactor routes to forward Boom errors from uiSettings * [uiSettings] colocate routes and service * [testUtils/esTestCluster] use more standard api style * [testUtils/es] add createCallCluster util * [testUtils/esTestCluster] add getters for client/callCluster * [es/healthcheck] ensure that healtcheck stops when server is stopped * [uiSettings/routes] add param/payload validation * [uiSettings/routes] add tests that verify error behaviors
This commit is contained in:
parent
924548864d
commit
8a64872ecb
42 changed files with 1187 additions and 179 deletions
|
@ -105,7 +105,7 @@
|
|||
"babel-register": "6.18.0",
|
||||
"bluebird": "2.9.34",
|
||||
"body-parser": "1.12.0",
|
||||
"boom": "2.8.0",
|
||||
"boom": "5.2.0",
|
||||
"brace": "0.5.1",
|
||||
"bunyan": "1.7.1",
|
||||
"check-hash": "1.0.1",
|
||||
|
|
|
@ -104,10 +104,12 @@ describe('plugins/elasticsearch', function () {
|
|||
|
||||
describe('wrap401Errors', () => {
|
||||
let handler;
|
||||
const error = new Error('Authentication required');
|
||||
error.statusCode = 401;
|
||||
let error;
|
||||
|
||||
beforeEach(() => {
|
||||
error = new Error('Authentication required');
|
||||
error.statusCode = 401;
|
||||
|
||||
handler = sinon.stub();
|
||||
});
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import { mkdirp as mkdirpNode } from 'mkdirp';
|
|||
|
||||
import manageUuid from './server/lib/manage_uuid';
|
||||
import search from './server/routes/api/search';
|
||||
import settings from './server/routes/api/settings';
|
||||
import { scrollSearchApi } from './server/routes/api/scroll_search';
|
||||
import { importApi } from './server/routes/api/import';
|
||||
import { exportApi } from './server/routes/api/export';
|
||||
|
@ -143,7 +142,6 @@ export default function (kibana) {
|
|||
manageUuid(server);
|
||||
// routes
|
||||
search(server);
|
||||
settings(server);
|
||||
scripts(server);
|
||||
scrollSearchApi(server);
|
||||
importApi(server);
|
||||
|
|
|
@ -11,7 +11,7 @@ export default function handleESError(error) {
|
|||
error instanceof esErrors.ServiceUnavailable ||
|
||||
error instanceof esErrors.NoConnections ||
|
||||
error instanceof esErrors.RequestTimeout) {
|
||||
return Boom.serverTimeout(error);
|
||||
return Boom.serverUnavailable(error);
|
||||
} else if (error instanceof esErrors.Conflict || _.contains(error.message, 'index_template_already_exists')) {
|
||||
return Boom.conflict(error);
|
||||
} else if (error instanceof esErrors[403]) {
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
export default function (server) {
|
||||
require('./register_get')(server);
|
||||
require('./register_set')(server);
|
||||
require('./register_set_many')(server);
|
||||
require('./register_delete')(server);
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
import Boom from 'boom';
|
||||
|
||||
export default function registerDelete(server) {
|
||||
server.route({
|
||||
path: '/api/kibana/settings/{key}',
|
||||
method: 'DELETE',
|
||||
handler: function (req, reply) {
|
||||
const { key } = req.params;
|
||||
const uiSettings = req.getUiSettingsService();
|
||||
|
||||
uiSettings
|
||||
.remove(key)
|
||||
.then(() => uiSettings
|
||||
.getUserProvided()
|
||||
.then(settings => reply({ settings }).type('application/json'))
|
||||
)
|
||||
.catch(err => reply(Boom.wrap(err, err.statusCode)));
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import Boom from 'boom';
|
||||
|
||||
export default function registerGet(server) {
|
||||
server.route({
|
||||
path: '/api/kibana/settings',
|
||||
method: 'GET',
|
||||
handler: function (req, reply) {
|
||||
req
|
||||
.getUiSettingsService()
|
||||
.getUserProvided()
|
||||
.then(settings => reply({ settings }).type('application/json'))
|
||||
.catch(err => reply(Boom.wrap(err, err.statusCode)));
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import Boom from 'boom';
|
||||
|
||||
export default function registerSet(server) {
|
||||
server.route({
|
||||
path: '/api/kibana/settings/{key}',
|
||||
method: 'POST',
|
||||
handler: function (req, reply) {
|
||||
const { key } = req.params;
|
||||
const { value } = req.payload;
|
||||
const uiSettings = req.getUiSettingsService();
|
||||
|
||||
uiSettings
|
||||
.set(key, value)
|
||||
.then(() => uiSettings
|
||||
.getUserProvided()
|
||||
.then(settings => reply({ settings }).type('application/json'))
|
||||
)
|
||||
.catch(err => reply(Boom.wrap(err, err.statusCode)));
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
import Boom from 'boom';
|
||||
|
||||
export default function registerSet(server) {
|
||||
server.route({
|
||||
path: '/api/kibana/settings',
|
||||
method: 'POST',
|
||||
handler: function (req, reply) {
|
||||
const { changes } = req.payload;
|
||||
const uiSettings = req.getUiSettingsService();
|
||||
|
||||
uiSettings
|
||||
.setMany(changes)
|
||||
.then(() => uiSettings
|
||||
.getUserProvided()
|
||||
.then(settings => reply({ settings }).type('application/json'))
|
||||
)
|
||||
.catch(err => reply(Boom.wrap(err, err.statusCode)));
|
||||
}
|
||||
});
|
||||
}
|
|
@ -70,6 +70,8 @@ describe('routes', () => {
|
|||
|
||||
it('redirects shortened urls', (done) => {
|
||||
kbnTestServer.makeRequest(kbnServer, shortenOptions, (res) => {
|
||||
expect(res).to.have.property('statusCode', 200);
|
||||
|
||||
const gotoOptions = {
|
||||
method: 'GET',
|
||||
url: '/goto/' + res.payload
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import expect from 'expect.js';
|
||||
import sinon from 'sinon';
|
||||
import shortUrlLookupProvider from '../short_url_lookup';
|
||||
import { SavedObjectsClient } from '../../saved_objects/client';
|
||||
|
||||
describe('shortUrlLookupProvider', () => {
|
||||
const ID = 'bf00ad16941fc51420f91a93428b27a0';
|
||||
|
@ -17,7 +18,8 @@ describe('shortUrlLookupProvider', () => {
|
|||
savedObjectsClient = {
|
||||
get: sandbox.stub(),
|
||||
create: sandbox.stub().returns(Promise.resolve({ id: ID })),
|
||||
update: sandbox.stub()
|
||||
update: sandbox.stub(),
|
||||
errors: SavedObjectsClient.errors
|
||||
};
|
||||
|
||||
req = { getSavedObjectsClient: () => savedObjectsClient };
|
||||
|
@ -58,10 +60,8 @@ describe('shortUrlLookupProvider', () => {
|
|||
});
|
||||
|
||||
it('gracefully handles version conflict', async () => {
|
||||
const error = new Error('version conflict');
|
||||
error.data = { type: 'version_conflict_engine_exception' };
|
||||
const error = savedObjectsClient.errors.decorateConflictError(new Error());
|
||||
savedObjectsClient.create.throws(error);
|
||||
|
||||
const id = await shortUrl.generateUrlId(URL, req);
|
||||
expect(id).to.eql(ID);
|
||||
});
|
||||
|
|
|
@ -17,9 +17,11 @@ export default function (server) {
|
|||
return {
|
||||
async generateUrlId(url, req) {
|
||||
const id = crypto.createHash('md5').update(url).digest('hex');
|
||||
const savedObjectsClient = req.getSavedObjectsClient();
|
||||
const { isConflictError } = savedObjectsClient.errors;
|
||||
|
||||
try {
|
||||
const doc = await req.getSavedObjectsClient().create('url', {
|
||||
const doc = await savedObjectsClient.create('url', {
|
||||
url,
|
||||
accessCount: 0,
|
||||
createDate: new Date(),
|
||||
|
@ -27,12 +29,12 @@ export default function (server) {
|
|||
}, { id });
|
||||
|
||||
return doc.id;
|
||||
} catch(e) {
|
||||
if (get(e, 'data.type') === 'version_conflict_engine_exception') {
|
||||
} catch (error) {
|
||||
if (isConflictError(error)) {
|
||||
return id;
|
||||
}
|
||||
|
||||
throw e;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
import expect from 'expect.js';
|
||||
import { errors as esErrors } from 'elasticsearch';
|
||||
|
||||
import { decorateEsError } from '../decorate_es_error';
|
||||
import {
|
||||
isEsUnavailableError,
|
||||
isConflictError,
|
||||
isNotAuthorizedError,
|
||||
isForbiddenError,
|
||||
isNotFoundError,
|
||||
isBadRequestError,
|
||||
} from '../errors';
|
||||
|
||||
describe('savedObjectsClient/decorateEsError', () => {
|
||||
it('always returns the same error it receives', () => {
|
||||
const error = new Error();
|
||||
expect(decorateEsError(error)).to.be(error);
|
||||
});
|
||||
|
||||
it('makes es.ConnectionFault a SavedObjectsClient/EsUnavailable error', () => {
|
||||
const error = new esErrors.ConnectionFault();
|
||||
expect(isEsUnavailableError(error)).to.be(false);
|
||||
expect(decorateEsError(error)).to.be(error);
|
||||
expect(isEsUnavailableError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('makes es.ServiceUnavailable a SavedObjectsClient/EsUnavailable error', () => {
|
||||
const error = new esErrors.ServiceUnavailable();
|
||||
expect(isEsUnavailableError(error)).to.be(false);
|
||||
expect(decorateEsError(error)).to.be(error);
|
||||
expect(isEsUnavailableError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('makes es.NoConnections a SavedObjectsClient/EsUnavailable error', () => {
|
||||
const error = new esErrors.NoConnections();
|
||||
expect(isEsUnavailableError(error)).to.be(false);
|
||||
expect(decorateEsError(error)).to.be(error);
|
||||
expect(isEsUnavailableError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('makes es.RequestTimeout a SavedObjectsClient/EsUnavailable error', () => {
|
||||
const error = new esErrors.RequestTimeout();
|
||||
expect(isEsUnavailableError(error)).to.be(false);
|
||||
expect(decorateEsError(error)).to.be(error);
|
||||
expect(isEsUnavailableError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('makes es.Conflict a SavedObjectsClient/Conflict error', () => {
|
||||
const error = new esErrors.Conflict();
|
||||
expect(isConflictError(error)).to.be(false);
|
||||
expect(decorateEsError(error)).to.be(error);
|
||||
expect(isConflictError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('makes es.AuthenticationException a SavedObjectsClient/NotAuthorized error', () => {
|
||||
const error = new esErrors.AuthenticationException();
|
||||
expect(isNotAuthorizedError(error)).to.be(false);
|
||||
expect(decorateEsError(error)).to.be(error);
|
||||
expect(isNotAuthorizedError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('makes es.Forbidden a SavedObjectsClient/Forbidden error', () => {
|
||||
const error = new esErrors.Forbidden();
|
||||
expect(isForbiddenError(error)).to.be(false);
|
||||
expect(decorateEsError(error)).to.be(error);
|
||||
expect(isForbiddenError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('makes es.NotFound a SavedObjectsClient/NotFound error', () => {
|
||||
const error = new esErrors.NotFound();
|
||||
expect(isNotFoundError(error)).to.be(false);
|
||||
expect(decorateEsError(error)).to.be(error);
|
||||
expect(isNotFoundError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('makes es.BadRequest a SavedObjectsClient/BadRequest error', () => {
|
||||
const error = new esErrors.BadRequest();
|
||||
expect(isBadRequestError(error)).to.be(false);
|
||||
expect(decorateEsError(error)).to.be(error);
|
||||
expect(isBadRequestError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('returns other errors as Boom errors', () => {
|
||||
const error = new Error();
|
||||
expect(error).to.not.have.property('isBoom');
|
||||
expect(decorateEsError(error)).to.be(error);
|
||||
expect(error).to.have.property('isBoom');
|
||||
});
|
||||
});
|
304
src/server/saved_objects/client/lib/__tests__/errors.js
Normal file
304
src/server/saved_objects/client/lib/__tests__/errors.js
Normal file
|
@ -0,0 +1,304 @@
|
|||
import expect from 'expect.js';
|
||||
import Boom from 'boom';
|
||||
|
||||
import {
|
||||
decorateBadRequestError,
|
||||
isBadRequestError,
|
||||
decorateNotAuthorizedError,
|
||||
isNotAuthorizedError,
|
||||
decorateForbiddenError,
|
||||
isForbiddenError,
|
||||
decorateNotFoundError,
|
||||
isNotFoundError,
|
||||
decorateConflictError,
|
||||
isConflictError,
|
||||
decorateEsUnavailableError,
|
||||
isEsUnavailableError,
|
||||
decorateGeneralError,
|
||||
} from '../errors';
|
||||
|
||||
describe('savedObjectsClient/errorTypes', () => {
|
||||
describe('BadRequest error', () => {
|
||||
describe('decorateBadRequestError', () => {
|
||||
it('returns original object', () => {
|
||||
const error = new Error();
|
||||
expect(decorateBadRequestError(error)).to.be(error);
|
||||
});
|
||||
|
||||
it('makes the error identifiable as a BadRequest error', () => {
|
||||
const error = new Error();
|
||||
expect(isBadRequestError(error)).to.be(false);
|
||||
decorateBadRequestError(error);
|
||||
expect(isBadRequestError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('adds boom properties', () => {
|
||||
const error = decorateBadRequestError(new Error());
|
||||
expect(error.output).to.be.an('object');
|
||||
expect(error.output.statusCode).to.be(400);
|
||||
});
|
||||
|
||||
it('preserves boom properties of input', () => {
|
||||
const error = Boom.notFound();
|
||||
decorateBadRequestError(error);
|
||||
expect(error.output.statusCode).to.be(404);
|
||||
});
|
||||
|
||||
describe('error.output', () => {
|
||||
it('defaults to message of erorr', () => {
|
||||
const error = decorateBadRequestError(new Error('foobar'));
|
||||
expect(error.output.payload).to.have.property('message', 'foobar');
|
||||
});
|
||||
it('prefixes message with passed reason', () => {
|
||||
const error = decorateBadRequestError(new Error('foobar'), 'biz');
|
||||
expect(error.output.payload).to.have.property('message', 'biz: foobar');
|
||||
});
|
||||
it('sets statusCode to 400', () => {
|
||||
const error = decorateBadRequestError(new Error('foo'));
|
||||
expect(error.output).to.have.property('statusCode', 400);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('NotAuthorized error', () => {
|
||||
describe('decorateNotAuthorizedError', () => {
|
||||
it('returns original object', () => {
|
||||
const error = new Error();
|
||||
expect(decorateNotAuthorizedError(error)).to.be(error);
|
||||
});
|
||||
|
||||
it('makes the error identifiable as a NotAuthorized error', () => {
|
||||
const error = new Error();
|
||||
expect(isNotAuthorizedError(error)).to.be(false);
|
||||
decorateNotAuthorizedError(error);
|
||||
expect(isNotAuthorizedError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('adds boom properties', () => {
|
||||
const error = decorateNotAuthorizedError(new Error());
|
||||
expect(error.output).to.be.an('object');
|
||||
expect(error.output.statusCode).to.be(401);
|
||||
});
|
||||
|
||||
it('preserves boom properties of input', () => {
|
||||
const error = Boom.notFound();
|
||||
decorateNotAuthorizedError(error);
|
||||
expect(error.output.statusCode).to.be(404);
|
||||
});
|
||||
|
||||
describe('error.output', () => {
|
||||
it('defaults to message of erorr', () => {
|
||||
const error = decorateNotAuthorizedError(new Error('foobar'));
|
||||
expect(error.output.payload).to.have.property('message', 'foobar');
|
||||
});
|
||||
it('prefixes message with passed reason', () => {
|
||||
const error = decorateNotAuthorizedError(new Error('foobar'), 'biz');
|
||||
expect(error.output.payload).to.have.property('message', 'biz: foobar');
|
||||
});
|
||||
it('sets statusCode to 401', () => {
|
||||
const error = decorateNotAuthorizedError(new Error('foo'));
|
||||
expect(error.output).to.have.property('statusCode', 401);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Forbidden error', () => {
|
||||
describe('decorateForbiddenError', () => {
|
||||
it('returns original object', () => {
|
||||
const error = new Error();
|
||||
expect(decorateForbiddenError(error)).to.be(error);
|
||||
});
|
||||
|
||||
it('makes the error identifiable as a Forbidden error', () => {
|
||||
const error = new Error();
|
||||
expect(isForbiddenError(error)).to.be(false);
|
||||
decorateForbiddenError(error);
|
||||
expect(isForbiddenError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('adds boom properties', () => {
|
||||
const error = decorateForbiddenError(new Error());
|
||||
expect(error.output).to.be.an('object');
|
||||
expect(error.output.statusCode).to.be(403);
|
||||
});
|
||||
|
||||
it('preserves boom properties of input', () => {
|
||||
const error = Boom.notFound();
|
||||
decorateForbiddenError(error);
|
||||
expect(error.output.statusCode).to.be(404);
|
||||
});
|
||||
|
||||
describe('error.output', () => {
|
||||
it('defaults to message of erorr', () => {
|
||||
const error = decorateForbiddenError(new Error('foobar'));
|
||||
expect(error.output.payload).to.have.property('message', 'foobar');
|
||||
});
|
||||
it('prefixes message with passed reason', () => {
|
||||
const error = decorateForbiddenError(new Error('foobar'), 'biz');
|
||||
expect(error.output.payload).to.have.property('message', 'biz: foobar');
|
||||
});
|
||||
it('sets statusCode to 403', () => {
|
||||
const error = decorateForbiddenError(new Error('foo'));
|
||||
expect(error.output).to.have.property('statusCode', 403);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('NotFound error', () => {
|
||||
describe('decorateNotFoundError', () => {
|
||||
it('returns original object', () => {
|
||||
const error = new Error();
|
||||
expect(decorateNotFoundError(error)).to.be(error);
|
||||
});
|
||||
|
||||
it('makes the error identifiable as a NotFound error', () => {
|
||||
const error = new Error();
|
||||
expect(isNotFoundError(error)).to.be(false);
|
||||
decorateNotFoundError(error);
|
||||
expect(isNotFoundError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('adds boom properties', () => {
|
||||
const error = decorateNotFoundError(new Error());
|
||||
expect(error.output).to.be.an('object');
|
||||
expect(error.output.statusCode).to.be(404);
|
||||
});
|
||||
|
||||
it('preserves boom properties of input', () => {
|
||||
const error = Boom.forbidden();
|
||||
decorateNotFoundError(error);
|
||||
expect(error.output.statusCode).to.be(403);
|
||||
});
|
||||
|
||||
describe('error.output', () => {
|
||||
it('defaults to message of erorr', () => {
|
||||
const error = decorateNotFoundError(new Error('foobar'));
|
||||
expect(error.output.payload).to.have.property('message', 'foobar');
|
||||
});
|
||||
it('prefixes message with passed reason', () => {
|
||||
const error = decorateNotFoundError(new Error('foobar'), 'biz');
|
||||
expect(error.output.payload).to.have.property('message', 'biz: foobar');
|
||||
});
|
||||
it('sets statusCode to 404', () => {
|
||||
const error = decorateNotFoundError(new Error('foo'));
|
||||
expect(error.output).to.have.property('statusCode', 404);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Conflict error', () => {
|
||||
describe('decorateConflictError', () => {
|
||||
it('returns original object', () => {
|
||||
const error = new Error();
|
||||
expect(decorateConflictError(error)).to.be(error);
|
||||
});
|
||||
|
||||
it('makes the error identifiable as a Conflict error', () => {
|
||||
const error = new Error();
|
||||
expect(isConflictError(error)).to.be(false);
|
||||
decorateConflictError(error);
|
||||
expect(isConflictError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('adds boom properties', () => {
|
||||
const error = decorateConflictError(new Error());
|
||||
expect(error.output).to.be.an('object');
|
||||
expect(error.output.statusCode).to.be(409);
|
||||
});
|
||||
|
||||
it('preserves boom properties of input', () => {
|
||||
const error = Boom.notFound();
|
||||
decorateConflictError(error);
|
||||
expect(error.output.statusCode).to.be(404);
|
||||
});
|
||||
|
||||
describe('error.output', () => {
|
||||
it('defaults to message of erorr', () => {
|
||||
const error = decorateConflictError(new Error('foobar'));
|
||||
expect(error.output.payload).to.have.property('message', 'foobar');
|
||||
});
|
||||
it('prefixes message with passed reason', () => {
|
||||
const error = decorateConflictError(new Error('foobar'), 'biz');
|
||||
expect(error.output.payload).to.have.property('message', 'biz: foobar');
|
||||
});
|
||||
it('sets statusCode to 409', () => {
|
||||
const error = decorateConflictError(new Error('foo'));
|
||||
expect(error.output).to.have.property('statusCode', 409);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('EsUnavailable error', () => {
|
||||
describe('decorateEsUnavailableError', () => {
|
||||
it('returns original object', () => {
|
||||
const error = new Error();
|
||||
expect(decorateEsUnavailableError(error)).to.be(error);
|
||||
});
|
||||
|
||||
it('makes the error identifiable as a EsUnavailable error', () => {
|
||||
const error = new Error();
|
||||
expect(isEsUnavailableError(error)).to.be(false);
|
||||
decorateEsUnavailableError(error);
|
||||
expect(isEsUnavailableError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('adds boom properties', () => {
|
||||
const error = decorateEsUnavailableError(new Error());
|
||||
expect(error.output).to.be.an('object');
|
||||
expect(error.output.statusCode).to.be(503);
|
||||
});
|
||||
|
||||
it('preserves boom properties of input', () => {
|
||||
const error = Boom.notFound();
|
||||
decorateEsUnavailableError(error);
|
||||
expect(error.output.statusCode).to.be(404);
|
||||
});
|
||||
|
||||
describe('error.output', () => {
|
||||
it('defaults to message of erorr', () => {
|
||||
const error = decorateEsUnavailableError(new Error('foobar'));
|
||||
expect(error.output.payload).to.have.property('message', 'foobar');
|
||||
});
|
||||
it('prefixes message with passed reason', () => {
|
||||
const error = decorateEsUnavailableError(new Error('foobar'), 'biz');
|
||||
expect(error.output.payload).to.have.property('message', 'biz: foobar');
|
||||
});
|
||||
it('sets statusCode to 503', () => {
|
||||
const error = decorateEsUnavailableError(new Error('foo'));
|
||||
expect(error.output).to.have.property('statusCode', 503);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('General error', () => {
|
||||
describe('decorateGeneralError', () => {
|
||||
it('returns original object', () => {
|
||||
const error = new Error();
|
||||
expect(decorateGeneralError(error)).to.be(error);
|
||||
});
|
||||
|
||||
it('adds boom properties', () => {
|
||||
const error = decorateGeneralError(new Error());
|
||||
expect(error.output).to.be.an('object');
|
||||
expect(error.output.statusCode).to.be(500);
|
||||
});
|
||||
|
||||
it('preserves boom properties of input', () => {
|
||||
const error = Boom.notFound();
|
||||
decorateGeneralError(error);
|
||||
expect(error.output.statusCode).to.be(404);
|
||||
});
|
||||
|
||||
describe('error.output', () => {
|
||||
it('ignores error message', () => {
|
||||
const error = decorateGeneralError(new Error('foobar'));
|
||||
expect(error.output.payload).to.have.property('message').match(/internal server error/i);
|
||||
});
|
||||
it('sets statusCode to 500', () => {
|
||||
const error = decorateGeneralError(new Error('foo'));
|
||||
expect(error.output).to.have.property('statusCode', 500);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
62
src/server/saved_objects/client/lib/decorate_es_error.js
Normal file
62
src/server/saved_objects/client/lib/decorate_es_error.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import elasticsearch from 'elasticsearch';
|
||||
import { get } from 'lodash';
|
||||
|
||||
const {
|
||||
ConnectionFault,
|
||||
ServiceUnavailable,
|
||||
NoConnections,
|
||||
RequestTimeout,
|
||||
Conflict,
|
||||
401: NotAuthorized,
|
||||
403: Forbidden,
|
||||
NotFound,
|
||||
BadRequest
|
||||
} = elasticsearch.errors;
|
||||
|
||||
import {
|
||||
decorateBadRequestError,
|
||||
decorateNotAuthorizedError,
|
||||
decorateForbiddenError,
|
||||
decorateNotFoundError,
|
||||
decorateConflictError,
|
||||
decorateEsUnavailableError,
|
||||
decorateGeneralError,
|
||||
} from './errors';
|
||||
|
||||
export function decorateEsError(error) {
|
||||
if (!(error instanceof Error)) {
|
||||
throw new Error('Expected an instance of Error');
|
||||
}
|
||||
|
||||
const { reason } = get(error, 'body.error', {});
|
||||
if (
|
||||
error instanceof ConnectionFault ||
|
||||
error instanceof ServiceUnavailable ||
|
||||
error instanceof NoConnections ||
|
||||
error instanceof RequestTimeout
|
||||
) {
|
||||
return decorateEsUnavailableError(error, reason);
|
||||
}
|
||||
|
||||
if (error instanceof Conflict) {
|
||||
return decorateConflictError(error, reason);
|
||||
}
|
||||
|
||||
if (error instanceof NotAuthorized) {
|
||||
return decorateNotAuthorizedError(error, reason);
|
||||
}
|
||||
|
||||
if (error instanceof Forbidden) {
|
||||
return decorateForbiddenError(error, reason);
|
||||
}
|
||||
|
||||
if (error instanceof NotFound) {
|
||||
return decorateNotFoundError(error, reason);
|
||||
}
|
||||
|
||||
if (error instanceof BadRequest) {
|
||||
return decorateBadRequestError(error, reason);
|
||||
}
|
||||
|
||||
return decorateGeneralError(error, reason);
|
||||
}
|
81
src/server/saved_objects/client/lib/errors.js
Normal file
81
src/server/saved_objects/client/lib/errors.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
import Boom from 'boom';
|
||||
|
||||
const code = Symbol('SavedObjectsClientErrorCode');
|
||||
|
||||
function decorate(error, errorCode, statusCode, message) {
|
||||
const boom = Boom.boomify(error, {
|
||||
statusCode,
|
||||
message,
|
||||
override: false,
|
||||
});
|
||||
|
||||
boom[code] = errorCode;
|
||||
|
||||
return boom;
|
||||
}
|
||||
|
||||
// 400 - badRequest
|
||||
const CODE_BAD_REQUEST = 'SavedObjectsClient/badRequest';
|
||||
export function decorateBadRequestError(error, reason) {
|
||||
return decorate(error, CODE_BAD_REQUEST, 400, reason);
|
||||
}
|
||||
export function isBadRequestError(error) {
|
||||
return error && error[code] === CODE_BAD_REQUEST;
|
||||
}
|
||||
|
||||
|
||||
// 401 - Not Authorized
|
||||
const CODE_NOT_AUTHORIZED = 'SavedObjectsClient/notAuthorized';
|
||||
export function decorateNotAuthorizedError(error, reason) {
|
||||
return decorate(error, CODE_NOT_AUTHORIZED, 401, reason);
|
||||
}
|
||||
export function isNotAuthorizedError(error) {
|
||||
return error && error[code] === CODE_NOT_AUTHORIZED;
|
||||
}
|
||||
|
||||
|
||||
// 403 - Forbidden
|
||||
const CODE_FORBIDDEN = 'SavedObjectsClient/forbidden';
|
||||
export function decorateForbiddenError(error, reason) {
|
||||
return decorate(error, CODE_FORBIDDEN, 403, reason);
|
||||
}
|
||||
export function isForbiddenError(error) {
|
||||
return error && error[code] === CODE_FORBIDDEN;
|
||||
}
|
||||
|
||||
|
||||
// 404 - Not Found
|
||||
const CODE_NOT_FOUND = 'SavedObjectsClient/notFound';
|
||||
export function decorateNotFoundError(error, reason) {
|
||||
return decorate(error, CODE_NOT_FOUND, 404, reason);
|
||||
}
|
||||
export function isNotFoundError(error) {
|
||||
return error && error[code] === CODE_NOT_FOUND;
|
||||
}
|
||||
|
||||
|
||||
// 409 - Conflict
|
||||
const CODE_CONFLICT = 'SavedObjectsClient/conflict';
|
||||
export function decorateConflictError(error, reason) {
|
||||
return decorate(error, CODE_CONFLICT, 409, reason);
|
||||
}
|
||||
export function isConflictError(error) {
|
||||
return error && error[code] === CODE_CONFLICT;
|
||||
}
|
||||
|
||||
|
||||
// 500 - Es Unavailable
|
||||
const CODE_ES_UNAVAILABLE = 'SavedObjectsClient/esUnavailable';
|
||||
export function decorateEsUnavailableError(error, reason) {
|
||||
return decorate(error, CODE_ES_UNAVAILABLE, 503, reason);
|
||||
}
|
||||
export function isEsUnavailableError(error) {
|
||||
return error && error[code] === CODE_ES_UNAVAILABLE;
|
||||
}
|
||||
|
||||
|
||||
// 500 - General Error
|
||||
const CODE_GENERAL_ERROR = 'SavedObjectsClient/generalError';
|
||||
export function decorateGeneralError(error, reason) {
|
||||
return decorate(error, CODE_GENERAL_ERROR, 500, reason);
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
import elasticsearch from 'elasticsearch';
|
||||
import Boom from 'boom';
|
||||
import { get } from 'lodash';
|
||||
|
||||
const {
|
||||
ConnectionFault,
|
||||
ServiceUnavailable,
|
||||
NoConnections,
|
||||
RequestTimeout,
|
||||
Conflict,
|
||||
403: Forbidden,
|
||||
NotFound,
|
||||
BadRequest
|
||||
} = elasticsearch.errors;
|
||||
|
||||
export function handleEsError(error) {
|
||||
if (!(error instanceof Error)) {
|
||||
throw new Error('Expected an instance of Error');
|
||||
}
|
||||
|
||||
const { reason, type } = get(error, 'body.error', {});
|
||||
const details = { type };
|
||||
|
||||
if (
|
||||
error instanceof ConnectionFault ||
|
||||
error instanceof ServiceUnavailable ||
|
||||
error instanceof NoConnections ||
|
||||
error instanceof RequestTimeout
|
||||
) {
|
||||
throw Boom.serverTimeout();
|
||||
}
|
||||
|
||||
if (error instanceof Conflict) {
|
||||
throw Boom.conflict(reason, details);
|
||||
}
|
||||
|
||||
if (error instanceof Forbidden) {
|
||||
throw Boom.forbidden(reason, details);
|
||||
}
|
||||
|
||||
if (error instanceof NotFound) {
|
||||
throw Boom.notFound(reason, details);
|
||||
}
|
||||
|
||||
if (error instanceof BadRequest) {
|
||||
throw Boom.badRequest(reason, details);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
export { getSearchDsl } from './search_dsl';
|
||||
export { handleEsError } from './handle_es_error';
|
||||
export { trimIdPrefix } from './trim_id_prefix';
|
||||
export { includedFields } from './included_fields';
|
||||
export { decorateEsError } from './decorate_es_error';
|
||||
|
||||
import * as errors from './errors';
|
||||
export { errors };
|
||||
|
|
|
@ -5,9 +5,10 @@ import { getRootType } from '../../mappings';
|
|||
|
||||
import {
|
||||
getSearchDsl,
|
||||
handleEsError,
|
||||
trimIdPrefix,
|
||||
includedFields
|
||||
includedFields,
|
||||
decorateEsError,
|
||||
errors,
|
||||
} from './lib';
|
||||
|
||||
export class SavedObjectsClient {
|
||||
|
@ -18,6 +19,9 @@ export class SavedObjectsClient {
|
|||
this._callAdminCluster = callAdminCluster;
|
||||
}
|
||||
|
||||
static errors = errors
|
||||
errors = errors
|
||||
|
||||
/**
|
||||
* Persists an object
|
||||
*
|
||||
|
@ -316,7 +320,7 @@ export class SavedObjectsClient {
|
|||
index: this._kibanaIndex,
|
||||
});
|
||||
} catch (err) {
|
||||
throw handleEsError(err);
|
||||
throw decorateEsError(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
21
src/test_utils/es/create_call_cluster.js
Normal file
21
src/test_utils/es/create_call_cluster.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { get } from 'lodash';
|
||||
import toPath from 'lodash/internal/toPath';
|
||||
|
||||
/**
|
||||
* Create a callCluster function that properly executes methods on an
|
||||
* elasticsearch-js client
|
||||
*
|
||||
* @param {elasticsearch.Client} esClient
|
||||
* @return {Function}
|
||||
*/
|
||||
export function createCallCluster(esClient) {
|
||||
return function callCluster(method, params) {
|
||||
const path = toPath(method);
|
||||
const contextPath = path.slice(0, -1);
|
||||
|
||||
const action = get(esClient, path);
|
||||
const context = contextPath.length ? get(esClient, contextPath) : esClient;
|
||||
|
||||
return action.call(context, params);
|
||||
};
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import { resolve } from 'path';
|
||||
|
||||
import libesvm from 'libesvm';
|
||||
import elasticsearch from 'elasticsearch';
|
||||
|
||||
import { esTestConfig } from './es_test_config';
|
||||
import { createCallCluster } from './create_call_cluster';
|
||||
|
||||
const ESVM_DIR = resolve(__dirname, '../../../esvm/test_utils/es_test_cluster');
|
||||
const BRANCHES_DOWNLOADED = [];
|
||||
|
@ -33,12 +35,27 @@ export function createEsTestCluster(options = {}) {
|
|||
|
||||
// assigned in use.start(), reassigned in use.stop()
|
||||
let cluster;
|
||||
let client;
|
||||
|
||||
return new class EsTestCluster {
|
||||
getStartTimeout() {
|
||||
return esTestConfig.getLibesvmStartTimeout();
|
||||
}
|
||||
|
||||
getClient() {
|
||||
if (!client) {
|
||||
client = new elasticsearch.Client({
|
||||
host: esTestConfig.getUrl()
|
||||
});
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
getCallCluster() {
|
||||
return createCallCluster(this.getClient());
|
||||
}
|
||||
|
||||
async start() {
|
||||
const download = isDownloadNeeded(branch);
|
||||
|
||||
|
@ -89,6 +106,12 @@ export function createEsTestCluster(options = {}) {
|
|||
}
|
||||
|
||||
async stop() {
|
||||
if (client) {
|
||||
const c = client;
|
||||
client = null;
|
||||
await c.close();
|
||||
}
|
||||
|
||||
if (cluster) {
|
||||
const c = cluster;
|
||||
cluster = null;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export { esTestConfig } from './es_test_config';
|
||||
export { createEsTestCluster } from './es_test_cluster';
|
||||
export { createCallCluster } from './create_call_cluster';
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import sinon from 'sinon';
|
||||
import expect from 'expect.js';
|
||||
import { SavedObjectsClient } from '../../../../server/saved_objects/client';
|
||||
|
||||
export const savedObjectsClientErrors = SavedObjectsClient.errors;
|
||||
|
||||
export function createObjectsClientStub(type, id, esDocSource = {}) {
|
||||
const savedObjectsClient = {
|
||||
update: sinon.stub().returns(Promise.resolve()),
|
||||
get: sinon.stub().returns({ attributes: esDocSource })
|
||||
get: sinon.stub().returns({ attributes: esDocSource }),
|
||||
errors: savedObjectsClientErrors
|
||||
};
|
||||
|
||||
savedObjectsClient.assertGetQuery = () => {
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
export { createObjectsClientStub } from './create_objects_client_stub';
|
||||
export {
|
||||
createObjectsClientStub,
|
||||
savedObjectsClientErrors,
|
||||
} from './create_objects_client_stub';
|
||||
|
|
|
@ -37,6 +37,7 @@ describe('uiSettingsMixin()', () => {
|
|||
// mock hapi server
|
||||
const server = {
|
||||
log: sinon.stub(),
|
||||
route: sinon.stub(),
|
||||
config: () => config,
|
||||
addMemoizedFactoryToRequest(name, factory) {
|
||||
this.decorate('request', name, function () {
|
||||
|
|
|
@ -5,7 +5,10 @@ import Chance from 'chance';
|
|||
|
||||
import { UiSettingsService } from '../ui_settings_service';
|
||||
|
||||
import { createObjectsClientStub } from './lib';
|
||||
import {
|
||||
createObjectsClientStub,
|
||||
savedObjectsClientErrors,
|
||||
} from './lib';
|
||||
|
||||
const TYPE = 'config';
|
||||
const ID = 'kibana-version';
|
||||
|
@ -197,9 +200,12 @@ describe('ui settings', () => {
|
|||
|
||||
it('throws 401 errors', async () => {
|
||||
const { uiSettings } = setup({
|
||||
savedObjectsClient: { async get() {
|
||||
throw new esErrors[401]();
|
||||
} }
|
||||
savedObjectsClient: {
|
||||
errors: savedObjectsClientErrors,
|
||||
async get() {
|
||||
throw new esErrors[401]();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
|
@ -214,9 +220,12 @@ describe('ui settings', () => {
|
|||
const expectedUnexpectedError = new Error('unexpected');
|
||||
|
||||
const { uiSettings } = setup({
|
||||
savedObjectsClient: { async get() {
|
||||
throw expectedUnexpectedError;
|
||||
} }
|
||||
savedObjectsClient: {
|
||||
errors: savedObjectsClientErrors,
|
||||
async get() {
|
||||
throw expectedUnexpectedError;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
134
src/ui/ui_settings/routes/__tests__/doc_exists.js
Normal file
134
src/ui/ui_settings/routes/__tests__/doc_exists.js
Normal file
|
@ -0,0 +1,134 @@
|
|||
import expect from 'expect.js';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {
|
||||
getServices,
|
||||
chance,
|
||||
assertSinonMatch,
|
||||
} from './lib';
|
||||
|
||||
export function docExistsSuite() {
|
||||
async function setup(options = {}) {
|
||||
const {
|
||||
initialSettings
|
||||
} = options;
|
||||
|
||||
const { kbnServer, uiSettings } = getServices();
|
||||
|
||||
if (initialSettings) {
|
||||
await uiSettings.setMany(initialSettings);
|
||||
}
|
||||
|
||||
return { kbnServer, uiSettings };
|
||||
}
|
||||
|
||||
describe('get route', () => {
|
||||
it('returns a 200 and includes userValues', async () => {
|
||||
const defaultIndex = chance.word({ length: 10 });
|
||||
const { kbnServer } = await setup({
|
||||
initialSettings: {
|
||||
defaultIndex
|
||||
}
|
||||
});
|
||||
|
||||
const { statusCode, result } = await kbnServer.inject({
|
||||
method: 'GET',
|
||||
url: '/api/kibana/settings'
|
||||
});
|
||||
|
||||
expect(statusCode).to.be(200);
|
||||
assertSinonMatch(result, {
|
||||
settings: {
|
||||
buildNum: {
|
||||
userValue: sinon.match.number
|
||||
},
|
||||
defaultIndex: {
|
||||
userValue: defaultIndex
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('set route', () => {
|
||||
it('returns a 200 and all values including update', async () => {
|
||||
const { kbnServer } = await setup();
|
||||
|
||||
const defaultIndex = chance.word();
|
||||
const { statusCode, result } = await kbnServer.inject({
|
||||
method: 'POST',
|
||||
url: '/api/kibana/settings/defaultIndex',
|
||||
payload: {
|
||||
value: defaultIndex
|
||||
}
|
||||
});
|
||||
|
||||
expect(statusCode).to.be(200);
|
||||
assertSinonMatch(result, {
|
||||
settings: {
|
||||
buildNum: {
|
||||
userValue: sinon.match.number
|
||||
},
|
||||
defaultIndex: {
|
||||
userValue: defaultIndex
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMany route', () => {
|
||||
it('returns a 200 and all values including updates', async () => {
|
||||
const { kbnServer } = await setup();
|
||||
|
||||
const defaultIndex = chance.word();
|
||||
const { statusCode, result } = await kbnServer.inject({
|
||||
method: 'POST',
|
||||
url: '/api/kibana/settings',
|
||||
payload: {
|
||||
changes: {
|
||||
defaultIndex
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(statusCode).to.be(200);
|
||||
assertSinonMatch(result, {
|
||||
settings: {
|
||||
buildNum: {
|
||||
userValue: sinon.match.number
|
||||
},
|
||||
defaultIndex: {
|
||||
userValue: defaultIndex
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete route', () => {
|
||||
it('returns a 200 and deletes the setting', async () => {
|
||||
const defaultIndex = chance.word({ length: 10 });
|
||||
|
||||
const { kbnServer, uiSettings } = await setup({
|
||||
initialSettings: { defaultIndex }
|
||||
});
|
||||
|
||||
expect(await uiSettings.get('defaultIndex')).to.be(defaultIndex);
|
||||
|
||||
const { statusCode, result } = await kbnServer.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/kibana/settings/defaultIndex'
|
||||
});
|
||||
|
||||
expect(statusCode).to.be(200);
|
||||
assertSinonMatch(result, {
|
||||
settings: {
|
||||
buildNum: {
|
||||
userValue: sinon.match.number
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
77
src/ui/ui_settings/routes/__tests__/doc_missing.js
Normal file
77
src/ui/ui_settings/routes/__tests__/doc_missing.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
import {
|
||||
getServices,
|
||||
chance,
|
||||
assertDocMissingResponse
|
||||
} from './lib';
|
||||
|
||||
export function docMissingSuite() {
|
||||
async function setup() {
|
||||
const { kbnServer, savedObjectsClient } = getServices();
|
||||
|
||||
// delete all config docs
|
||||
const { saved_objects: objs } = await savedObjectsClient.find({ type: 'config' });
|
||||
|
||||
for (const obj of objs) {
|
||||
await savedObjectsClient.delete(obj.type, obj.id);
|
||||
}
|
||||
|
||||
return { kbnServer };
|
||||
}
|
||||
|
||||
describe('get route', () => {
|
||||
it('returns a 200 with empty values', async () => {
|
||||
const { kbnServer } = await setup();
|
||||
|
||||
const { statusCode, result } = await kbnServer.inject({
|
||||
method: 'GET',
|
||||
url: '/api/kibana/settings'
|
||||
});
|
||||
|
||||
expect(statusCode).to.be(200);
|
||||
expect(result).to.eql({ settings: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe('set route', () => {
|
||||
it('returns a 404', async () => {
|
||||
const { kbnServer } = await setup();
|
||||
|
||||
assertDocMissingResponse(await kbnServer.inject({
|
||||
method: 'POST',
|
||||
url: '/api/kibana/settings/defaultIndex',
|
||||
payload: {
|
||||
value: chance.word()
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMany route', () => {
|
||||
it('returns a 404', async () => {
|
||||
const { kbnServer } = await setup();
|
||||
|
||||
assertDocMissingResponse(await kbnServer.inject({
|
||||
method: 'POST',
|
||||
url: '/api/kibana/settings',
|
||||
payload: {
|
||||
changes: {
|
||||
defaultIndex: chance.word()
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete route', () => {
|
||||
it('returns a 404', async () => {
|
||||
const { kbnServer } = await setup();
|
||||
|
||||
assertDocMissingResponse(await kbnServer.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/kibana/settings/defaultIndex'
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
23
src/ui/ui_settings/routes/__tests__/index.js
Normal file
23
src/ui/ui_settings/routes/__tests__/index.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import {
|
||||
startServers,
|
||||
stopServers,
|
||||
} from './lib';
|
||||
|
||||
import { docExistsSuite } from './doc_exists';
|
||||
import { docMissingSuite } from './doc_missing';
|
||||
import { indexMissingSuite } from './index_missing';
|
||||
|
||||
describe('uiSettings/routes', function () {
|
||||
this.slow(2000);
|
||||
this.timeout(10000);
|
||||
|
||||
// these tests rely on getting sort of lucky with
|
||||
// the healthcheck, so we retry if they fail
|
||||
this.retries(3);
|
||||
|
||||
before(startServers);
|
||||
describe('doc exists', docExistsSuite);
|
||||
describe('doc missing', docMissingSuite);
|
||||
describe('index missing', indexMissingSuite);
|
||||
after(stopServers);
|
||||
});
|
116
src/ui/ui_settings/routes/__tests__/index_missing.js
Normal file
116
src/ui/ui_settings/routes/__tests__/index_missing.js
Normal file
|
@ -0,0 +1,116 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
import {
|
||||
getServices,
|
||||
chance,
|
||||
assertDocMissingResponse
|
||||
} from './lib';
|
||||
|
||||
export function indexMissingSuite() {
|
||||
beforeEach(async function () {
|
||||
const { kbnServer } = getServices();
|
||||
await kbnServer.server.plugins.elasticsearch.waitUntilReady();
|
||||
});
|
||||
|
||||
function getNumberOfShards(index) {
|
||||
return parseInt(Object.values(index)[0].settings.index.number_of_shards, 10);
|
||||
}
|
||||
|
||||
async function getIndex(callCluster, indexName) {
|
||||
return await callCluster('indices.get', {
|
||||
index: indexName,
|
||||
});
|
||||
}
|
||||
|
||||
async function setup() {
|
||||
const { callCluster, kbnServer } = getServices();
|
||||
const indexName = kbnServer.config.get('kibana.index');
|
||||
const initialIndex = await getIndex(callCluster, indexName);
|
||||
|
||||
await callCluster('indices.delete', {
|
||||
index: indexName,
|
||||
});
|
||||
|
||||
return {
|
||||
kbnServer,
|
||||
|
||||
// an incorrect number of shards is how we determine when the index was not created by Kibana,
|
||||
// but automatically by writing to es when index didn't exist
|
||||
async assertInvalidKibanaIndex() {
|
||||
const index = await getIndex(callCluster, indexName);
|
||||
|
||||
expect(getNumberOfShards(index))
|
||||
.to.not.be(getNumberOfShards(initialIndex));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
const { kbnServer, callCluster } = getServices();
|
||||
await callCluster('indices.delete', {
|
||||
index: kbnServer.config.get('kibana.index'),
|
||||
ignore: 404
|
||||
});
|
||||
});
|
||||
|
||||
describe('get route', () => {
|
||||
it('returns a 200 and with empty values', async () => {
|
||||
const { kbnServer } = await setup();
|
||||
|
||||
const { statusCode, result } = await kbnServer.inject({
|
||||
method: 'GET',
|
||||
url: '/api/kibana/settings'
|
||||
});
|
||||
|
||||
expect(statusCode).to.be(200);
|
||||
expect(result).to.eql({ settings: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe('set route', () => {
|
||||
it('creates an invalid Kibana index and returns a 404 document missing error', async () => {
|
||||
const { kbnServer, assertInvalidKibanaIndex } = await setup();
|
||||
|
||||
assertDocMissingResponse(await kbnServer.inject({
|
||||
method: 'POST',
|
||||
url: '/api/kibana/settings/defaultIndex',
|
||||
payload: {
|
||||
value: chance.word()
|
||||
}
|
||||
}));
|
||||
|
||||
await assertInvalidKibanaIndex();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMany route', () => {
|
||||
it('creates an invalid Kibana index and returns a 404 document missing error', async () => {
|
||||
const { kbnServer, assertInvalidKibanaIndex } = await setup();
|
||||
|
||||
assertDocMissingResponse(await kbnServer.inject({
|
||||
method: 'POST',
|
||||
url: '/api/kibana/settings',
|
||||
payload: {
|
||||
changes: {
|
||||
defaultIndex: chance.word()
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
await assertInvalidKibanaIndex();
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete route', () => {
|
||||
it('creates an invalid Kibana index and returns a 404 document missing error', async () => {
|
||||
const { kbnServer, assertInvalidKibanaIndex } = await setup();
|
||||
|
||||
assertDocMissingResponse(await kbnServer.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/kibana/settings/defaultIndex'
|
||||
}));
|
||||
|
||||
await assertInvalidKibanaIndex();
|
||||
});
|
||||
});
|
||||
}
|
16
src/ui/ui_settings/routes/__tests__/lib/assert.js
Normal file
16
src/ui/ui_settings/routes/__tests__/lib/assert.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import sinon from 'sinon';
|
||||
|
||||
export function assertSinonMatch(value, match) {
|
||||
const stub = sinon.stub();
|
||||
stub(value);
|
||||
sinon.assert.calledWithExactly(stub, match);
|
||||
}
|
||||
|
||||
export function assertDocMissingResponse({ result }) {
|
||||
assertSinonMatch(result, {
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: sinon.match('document_missing_exception')
|
||||
.and(sinon.match('document missing'))
|
||||
});
|
||||
}
|
3
src/ui/ui_settings/routes/__tests__/lib/chance.js
Normal file
3
src/ui/ui_settings/routes/__tests__/lib/chance.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Chance from 'chance';
|
||||
|
||||
export const chance = new Chance();
|
14
src/ui/ui_settings/routes/__tests__/lib/index.js
Normal file
14
src/ui/ui_settings/routes/__tests__/lib/index.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
export {
|
||||
startServers,
|
||||
getServices,
|
||||
stopServers
|
||||
} from './servers';
|
||||
|
||||
export {
|
||||
chance
|
||||
} from './chance';
|
||||
|
||||
export {
|
||||
assertSinonMatch,
|
||||
assertDocMissingResponse
|
||||
} from './assert';
|
53
src/ui/ui_settings/routes/__tests__/lib/servers.js
Normal file
53
src/ui/ui_settings/routes/__tests__/lib/servers.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { createEsTestCluster } from '../../../../../test_utils/es';
|
||||
import * as kbnTestServer from '../../../../../test_utils/kbn_server';
|
||||
|
||||
let kbnServer;
|
||||
let services;
|
||||
const es = createEsTestCluster({
|
||||
name: 'ui_settings/routes'
|
||||
});
|
||||
|
||||
export async function startServers() {
|
||||
this.timeout(es.getStartTimeout());
|
||||
await es.start();
|
||||
|
||||
kbnServer = kbnTestServer.createServerWithCorePlugins();
|
||||
await kbnServer.ready();
|
||||
await kbnServer.server.plugins.elasticsearch.waitUntilReady();
|
||||
}
|
||||
|
||||
export function getServices() {
|
||||
if (services) {
|
||||
return services;
|
||||
}
|
||||
|
||||
const callCluster = es.getCallCluster();
|
||||
|
||||
const savedObjectsClient = kbnServer.server.savedObjectsClientFactory({
|
||||
callCluster,
|
||||
});
|
||||
|
||||
const uiSettings = kbnServer.server.uiSettingsServiceFactory({
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
services = {
|
||||
kbnServer,
|
||||
callCluster,
|
||||
savedObjectsClient,
|
||||
uiSettings
|
||||
};
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
export async function stopServers() {
|
||||
services = null;
|
||||
|
||||
if (kbnServer) {
|
||||
await kbnServer.close();
|
||||
kbnServer = null;
|
||||
}
|
||||
|
||||
await es.stop();
|
||||
}
|
17
src/ui/ui_settings/routes/delete.js
Normal file
17
src/ui/ui_settings/routes/delete.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
async function handleRequest(request) {
|
||||
const { key } = request.params;
|
||||
const uiSettings = request.getUiSettingsService();
|
||||
|
||||
await uiSettings.remove(key);
|
||||
return {
|
||||
settings: await uiSettings.getUserProvided()
|
||||
};
|
||||
}
|
||||
|
||||
export const deleteRoute = {
|
||||
path: '/api/kibana/settings/{key}',
|
||||
method: 'DELETE',
|
||||
handler(request, reply) {
|
||||
reply(handleRequest(request));
|
||||
}
|
||||
};
|
14
src/ui/ui_settings/routes/get.js
Normal file
14
src/ui/ui_settings/routes/get.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
async function handleRequest(request) {
|
||||
const uiSettings = request.getUiSettingsService();
|
||||
return {
|
||||
settings: await uiSettings.getUserProvided()
|
||||
};
|
||||
}
|
||||
|
||||
export const getRoute = {
|
||||
path: '/api/kibana/settings',
|
||||
method: 'GET',
|
||||
handler: function (request, reply) {
|
||||
reply(handleRequest(request));
|
||||
}
|
||||
};
|
4
src/ui/ui_settings/routes/index.js
Normal file
4
src/ui/ui_settings/routes/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
export { deleteRoute } from './delete';
|
||||
export { getRoute } from './get';
|
||||
export { setManyRoute } from './set_many';
|
||||
export { setRoute } from './set';
|
31
src/ui/ui_settings/routes/set.js
Normal file
31
src/ui/ui_settings/routes/set.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import Joi from 'joi';
|
||||
|
||||
async function handleRequest(request) {
|
||||
const { key } = request.params;
|
||||
const { value } = request.payload;
|
||||
const uiSettings = request.getUiSettingsService();
|
||||
|
||||
await uiSettings.set(key, value);
|
||||
return {
|
||||
settings: await uiSettings.getUserProvided()
|
||||
};
|
||||
}
|
||||
|
||||
export const setRoute = {
|
||||
path: '/api/kibana/settings/{key}',
|
||||
method: 'POST',
|
||||
config: {
|
||||
validate: {
|
||||
params: Joi.object().keys({
|
||||
key: Joi.string().required(),
|
||||
}).default(),
|
||||
|
||||
payload: Joi.object().keys({
|
||||
value: Joi.any().required()
|
||||
}).required()
|
||||
},
|
||||
handler(request, reply) {
|
||||
reply(handleRequest(request));
|
||||
}
|
||||
}
|
||||
};
|
26
src/ui/ui_settings/routes/set_many.js
Normal file
26
src/ui/ui_settings/routes/set_many.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import Joi from 'joi';
|
||||
|
||||
async function handleRequest(request) {
|
||||
const { changes } = request.payload;
|
||||
const uiSettings = request.getUiSettingsService();
|
||||
|
||||
await uiSettings.setMany(changes);
|
||||
return {
|
||||
settings: await uiSettings.getUserProvided()
|
||||
};
|
||||
}
|
||||
|
||||
export const setManyRoute = {
|
||||
path: '/api/kibana/settings',
|
||||
method: 'POST',
|
||||
config: {
|
||||
validate: {
|
||||
payload: Joi.object().keys({
|
||||
changes: Joi.object().unknown(true).required()
|
||||
}).required()
|
||||
},
|
||||
handler(request, reply) {
|
||||
reply(handleRequest(request));
|
||||
}
|
||||
}
|
||||
};
|
|
@ -2,6 +2,12 @@ import { uiSettingsServiceFactory } from './ui_settings_service_factory';
|
|||
import { getUiSettingsServiceForRequest } from './ui_settings_service_for_request';
|
||||
import { mirrorStatus } from './mirror_status';
|
||||
import { UiExportsConsumer } from './ui_exports_consumer';
|
||||
import {
|
||||
deleteRoute,
|
||||
getRoute,
|
||||
setManyRoute,
|
||||
setRoute,
|
||||
} from './routes';
|
||||
|
||||
export function uiSettingsMixin(kbnServer, server, config) {
|
||||
const status = kbnServer.status.create('ui settings');
|
||||
|
@ -56,4 +62,9 @@ export function uiSettingsMixin(kbnServer, server, config) {
|
|||
server.uiSettings has been removed, see https://github.com/elastic/kibana/pull/12243.
|
||||
`);
|
||||
});
|
||||
|
||||
server.route(deleteRoute);
|
||||
server.route(getRoute);
|
||||
server.route(setManyRoute);
|
||||
server.route(setRoute);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { defaultsDeep, noop } from 'lodash';
|
||||
import { errors as esErrors } from 'elasticsearch';
|
||||
|
||||
function hydrateUserSettings(userSettings) {
|
||||
return Object.keys(userSettings)
|
||||
|
@ -106,11 +105,18 @@ export class UiSettingsService {
|
|||
ignore401Errors = false
|
||||
} = options;
|
||||
|
||||
const {
|
||||
isNotFoundError,
|
||||
isForbiddenError,
|
||||
isEsUnavailableError,
|
||||
isNotAuthorizedError
|
||||
} = this._savedObjectsClient.errors;
|
||||
|
||||
const isIgnorableError = error => (
|
||||
error instanceof esErrors[404] ||
|
||||
error instanceof esErrors[403] ||
|
||||
error instanceof esErrors.NoConnections ||
|
||||
(ignore401Errors && error instanceof esErrors[401])
|
||||
isNotFoundError(error) ||
|
||||
isForbiddenError(error) ||
|
||||
isEsUnavailableError(error) ||
|
||||
(ignore401Errors && isNotAuthorizedError(error))
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
|
@ -1,20 +1,6 @@
|
|||
import { get } from 'lodash';
|
||||
import toPath from 'lodash/internal/toPath';
|
||||
|
||||
import { createCallCluster } from '../../../../src/test_utils/es';
|
||||
import { SavedObjectsClient } from '../../../../src/server/saved_objects';
|
||||
|
||||
function createCallCluster(es) {
|
||||
return function callCluster(method, params) {
|
||||
const path = toPath(method);
|
||||
const contextPath = path.slice(0, -1);
|
||||
|
||||
const action = get(es, path);
|
||||
const context = contextPath.length ? get(es, contextPath) : es;
|
||||
|
||||
return action.call(context, params);
|
||||
};
|
||||
}
|
||||
|
||||
export class KibanaServerUiSettings {
|
||||
constructor(log, es, kibanaIndex, kibanaVersion) {
|
||||
this._log = log;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue