mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[savedObjects] fix error handling when Kibana index is missing (#14141)
* [savedObjects/delete+bulk_get] add failing tests * [savedObjects/delete+bulk_get] improve 404 handling * [savedObjects/client] fix mocha tests * [savedObjects/tests] remove extra test wrapper * [apiIntegration/kbnServer] basically disable es healthcheck * [savedObjects/create] add integration test * [savedObjects/find] add failing integration tests * [savedObjects/find] fix failing test * [savedObjects/client] explain reason for generic 404s * [savedObjects/get] add integration tests * [savedObjects/find] test request with unkown type * [savedObjects/find] add some more weird param tests * [savedObjects/find] test that weird params pass when no index * [savedObjects/update] use generic 404 * fix typos * [savedObjects/update] add integration tests * remove debugging uncomment * [savedObjects/tests] move backup kibana index delete out of tests * [savedObjects/tests/esArchives] remove logstash data * [savedObjects] update test * [uiSettings] remove detailed previously leaked from API * [functional/dashboard] wrap check that is only failing on Jenkins * [savedObjects/error] replace decorateNotFound with createGenericNotFound * fix typo * [savedObjectsClient/errors] fix decorateEsError() test * [savedObjectsClient] fix typos * [savedObjects/tests/functional] delete document that would normally exist * [savedObjectsClient/tests] use sinon assertions * [savedObjects/apiTests] create without index responds with 503 after #14202
This commit is contained in:
parent
0a4a2a1219
commit
e84761217e
20 changed files with 1076 additions and 131 deletions
|
@ -127,11 +127,9 @@ describe('SavedObjectsClient', () => {
|
|||
title: 'Logstash'
|
||||
});
|
||||
|
||||
expect(callAdminCluster.calledOnce).to.be(true);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWith(callAdminCluster, 'index');
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
|
||||
const args = callAdminCluster.getCall(0).args;
|
||||
expect(args[0]).to.be('index');
|
||||
});
|
||||
|
||||
it('should use create action if ID defined and overwrite=false', async () => {
|
||||
|
@ -141,11 +139,9 @@ describe('SavedObjectsClient', () => {
|
|||
id: 'logstash-*',
|
||||
});
|
||||
|
||||
expect(callAdminCluster.calledOnce).to.be(true);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWith(callAdminCluster, 'create');
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
|
||||
const args = callAdminCluster.getCall(0).args;
|
||||
expect(args[0]).to.be('create');
|
||||
});
|
||||
|
||||
it('allows for id to be provided', async () => {
|
||||
|
@ -153,11 +149,12 @@ describe('SavedObjectsClient', () => {
|
|||
title: 'Logstash'
|
||||
}, { id: 'logstash-*' });
|
||||
|
||||
expect(callAdminCluster.calledOnce).to.be(true);
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
id: 'index-pattern:logstash-*'
|
||||
}));
|
||||
|
||||
const args = callAdminCluster.getCall(0).args;
|
||||
expect(args[1].id).to.be('index-pattern:logstash-*');
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('self-generates an ID', async () => {
|
||||
|
@ -165,11 +162,12 @@ describe('SavedObjectsClient', () => {
|
|||
title: 'Logstash'
|
||||
});
|
||||
|
||||
expect(callAdminCluster.calledOnce).to.be(true);
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
id: sinon.match(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/)
|
||||
}));
|
||||
|
||||
const args = callAdminCluster.getCall(0).args;
|
||||
expect(args[1].id).to.match(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/);
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -182,18 +180,17 @@ describe('SavedObjectsClient', () => {
|
|||
{ type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }
|
||||
]);
|
||||
|
||||
expect(callAdminCluster.calledOnce).to.be(true);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({
|
||||
body: [
|
||||
{ create: { _type: 'doc', _id: 'config:one' } },
|
||||
{ type: 'config', ...mockTimestampFields, config: { title: 'Test One' } },
|
||||
{ create: { _type: 'doc', _id: 'index-pattern:two' } },
|
||||
{ type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' } }
|
||||
]
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
|
||||
const args = callAdminCluster.getCall(0).args;
|
||||
|
||||
expect(args[0]).to.be('bulk');
|
||||
expect(args[1].body).to.eql([
|
||||
{ create: { _type: 'doc', _id: 'config:one' } },
|
||||
{ type: 'config', ...mockTimestampFields, config: { title: 'Test One' } },
|
||||
{ create: { _type: 'doc', _id: 'index-pattern:two' } },
|
||||
{ type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' } }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should overwrite objects if overwrite is truthy', async () => {
|
||||
|
@ -201,7 +198,6 @@ describe('SavedObjectsClient', () => {
|
|||
|
||||
await savedObjectsClient.bulkCreate([{ type: 'foo', id: 'bar', attributes: {} }], { overwrite: false });
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({
|
||||
body: [
|
||||
// uses create because overwriting is not allowed
|
||||
|
@ -210,12 +206,13 @@ describe('SavedObjectsClient', () => {
|
|||
]
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
|
||||
callAdminCluster.reset();
|
||||
onBeforeWrite.reset();
|
||||
|
||||
await savedObjectsClient.bulkCreate([{ type: 'foo', id: 'bar', attributes: {} }], { overwrite: true });
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({
|
||||
body: [
|
||||
// uses index because overwriting is allowed
|
||||
|
@ -224,7 +221,7 @@ describe('SavedObjectsClient', () => {
|
|||
]
|
||||
}));
|
||||
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('returns document errors', async () => {
|
||||
|
@ -310,7 +307,9 @@ describe('SavedObjectsClient', () => {
|
|||
|
||||
describe('#delete', () => {
|
||||
it('throws notFound when ES is unable to find the document', async () => {
|
||||
callAdminCluster.returns(Promise.resolve({ found: false }));
|
||||
callAdminCluster.returns(Promise.resolve({
|
||||
result: 'not_found'
|
||||
}));
|
||||
|
||||
try {
|
||||
await savedObjectsClient.delete('index-pattern', 'logstash-*');
|
||||
|
@ -323,20 +322,21 @@ describe('SavedObjectsClient', () => {
|
|||
});
|
||||
|
||||
it('passes the parameters to callAdminCluster', async () => {
|
||||
callAdminCluster.returns({});
|
||||
callAdminCluster.returns({
|
||||
result: 'deleted'
|
||||
});
|
||||
await savedObjectsClient.delete('index-pattern', 'logstash-*');
|
||||
|
||||
expect(callAdminCluster.calledOnce).to.be(true);
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
|
||||
const [method, args] = callAdminCluster.getCall(0).args;
|
||||
expect(method).to.be('delete');
|
||||
expect(args).to.eql({
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'delete', {
|
||||
type: 'doc',
|
||||
id: 'index-pattern:logstash-*',
|
||||
refresh: 'wait_for',
|
||||
index: '.kibana-test'
|
||||
index: '.kibana-test',
|
||||
ignore: [404],
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -416,24 +416,26 @@ describe('SavedObjectsClient', () => {
|
|||
it('accepts per_page/page', async () => {
|
||||
await savedObjectsClient.find({ perPage: 10, page: 6 });
|
||||
|
||||
expect(callAdminCluster.calledOnce).to.be(true);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
size: 10,
|
||||
from: 50
|
||||
}));
|
||||
|
||||
const options = callAdminCluster.getCall(0).args[1];
|
||||
expect(options.size).to.be(10);
|
||||
expect(options.from).to.be(50);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('can filter by fields', async () => {
|
||||
await savedObjectsClient.find({ fields: ['title'] });
|
||||
|
||||
expect(callAdminCluster.calledOnce).to.be(true);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
_source: [
|
||||
'*.title', 'type', 'title'
|
||||
]
|
||||
}));
|
||||
|
||||
const options = callAdminCluster.getCall(0).args[1];
|
||||
expect(options._source).to.eql([
|
||||
'*.title', 'type', 'title'
|
||||
]);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -471,9 +473,11 @@ describe('SavedObjectsClient', () => {
|
|||
await savedObjectsClient.get('index-pattern', 'logstash-*');
|
||||
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
const [, args] = callAdminCluster.getCall(0).args;
|
||||
expect(args.id).to.eql('index-pattern:logstash-*');
|
||||
expect(args.type).to.eql('doc');
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
id: 'index-pattern:logstash-*',
|
||||
type: 'doc'
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -486,14 +490,17 @@ describe('SavedObjectsClient', () => {
|
|||
{ id: 'two', type: 'index-pattern' }
|
||||
]);
|
||||
|
||||
expect(callAdminCluster.calledOnce).to.be(true);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
body: {
|
||||
docs: [
|
||||
{ _type: 'doc', _id: 'config:one' },
|
||||
{ _type: 'doc', _id: 'index-pattern:two' }
|
||||
]
|
||||
}
|
||||
}));
|
||||
|
||||
const options = callAdminCluster.getCall(0).args[1];
|
||||
expect(options.body.docs).to.eql([
|
||||
{ _type: 'doc', _id: 'config:one' },
|
||||
{ _type: 'doc', _id: 'index-pattern:two' }
|
||||
]);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
});
|
||||
|
||||
it('returns early for empty objects argument', async () => {
|
||||
|
@ -502,7 +509,7 @@ describe('SavedObjectsClient', () => {
|
|||
const response = await savedObjectsClient.bulkGet([]);
|
||||
|
||||
expect(response.saved_objects).to.have.length(0);
|
||||
expect(callAdminCluster.notCalled).to.be(true);
|
||||
sinon.assert.notCalled(callAdminCluster);
|
||||
sinon.assert.notCalled(onBeforeWrite);
|
||||
});
|
||||
|
||||
|
@ -578,29 +585,29 @@ describe('SavedObjectsClient', () => {
|
|||
{ version: newVersion - 1 }
|
||||
);
|
||||
|
||||
const esParams = callAdminCluster.getCall(0).args[1];
|
||||
expect(esParams.version).to.be(newVersion - 1);
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
|
||||
version: newVersion - 1
|
||||
}));
|
||||
});
|
||||
|
||||
it('passes the parameters to callAdminCluster', async () => {
|
||||
await savedObjectsClient.update('index-pattern', 'logstash-*', { title: 'Testing' });
|
||||
|
||||
expect(callAdminCluster.calledOnce).to.be(true);
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
|
||||
const args = callAdminCluster.getCall(0).args;
|
||||
|
||||
expect(args[0]).to.be('update');
|
||||
expect(args[1]).to.eql({
|
||||
sinon.assert.calledOnce(callAdminCluster);
|
||||
sinon.assert.calledWithExactly(callAdminCluster, 'update', {
|
||||
type: 'doc',
|
||||
id: 'index-pattern:logstash-*',
|
||||
version: undefined,
|
||||
body: {
|
||||
doc: { updated_at: mockTimestamp, 'index-pattern': { title: 'Testing' } }
|
||||
},
|
||||
ignore: [404],
|
||||
refresh: 'wait_for',
|
||||
index: '.kibana-test'
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(onBeforeWrite);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -66,11 +66,13 @@ describe('savedObjectsClient/decorateEsError', () => {
|
|||
expect(isForbiddenError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('makes es.NotFound a SavedObjectsClient/NotFound error', () => {
|
||||
it('discards es.NotFound errors and returns a generic 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);
|
||||
const genericError = decorateEsError(error);
|
||||
expect(genericError).to.not.be(error);
|
||||
expect(isNotFoundError(error)).to.be(false);
|
||||
expect(isNotFoundError(genericError)).to.be(true);
|
||||
});
|
||||
|
||||
it('makes es.BadRequest a SavedObjectsClient/BadRequest error', () => {
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
isNotAuthorizedError,
|
||||
decorateForbiddenError,
|
||||
isForbiddenError,
|
||||
decorateNotFoundError,
|
||||
createGenericNotFoundError,
|
||||
isNotFoundError,
|
||||
decorateConflictError,
|
||||
isConflictError,
|
||||
|
@ -145,42 +145,26 @@ describe('savedObjectsClient/errorTypes', () => {
|
|||
});
|
||||
});
|
||||
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);
|
||||
describe('createGenericNotFoundError', () => {
|
||||
it('makes an error identifiable as a NotFound error', () => {
|
||||
const error = createGenericNotFoundError();
|
||||
expect(isNotFoundError(error)).to.be(true);
|
||||
});
|
||||
|
||||
it('adds boom properties', () => {
|
||||
const error = decorateNotFoundError(new Error());
|
||||
it('is a boom error, has boom properties', () => {
|
||||
const error = createGenericNotFoundError();
|
||||
expect(error).to.have.property('isBoom', true);
|
||||
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('Uses "Not Found" message', () => {
|
||||
const error = createGenericNotFoundError();
|
||||
expect(error.output.payload).to.have.property('message', 'Not Found');
|
||||
});
|
||||
it('sets statusCode to 404', () => {
|
||||
const error = decorateNotFoundError(new Error('foo'));
|
||||
const error = createGenericNotFoundError();
|
||||
expect(error.output).to.have.property('statusCode', 404);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
decorateBadRequestError,
|
||||
decorateNotAuthorizedError,
|
||||
decorateForbiddenError,
|
||||
decorateNotFoundError,
|
||||
createGenericNotFoundError,
|
||||
decorateConflictError,
|
||||
decorateEsUnavailableError,
|
||||
decorateGeneralError,
|
||||
|
@ -51,7 +51,7 @@ export function decorateEsError(error) {
|
|||
}
|
||||
|
||||
if (error instanceof NotFound) {
|
||||
return decorateNotFoundError(error, reason);
|
||||
return createGenericNotFoundError();
|
||||
}
|
||||
|
||||
if (error instanceof BadRequest) {
|
||||
|
|
|
@ -54,8 +54,8 @@ export function isForbiddenError(error) {
|
|||
|
||||
// 404 - Not Found
|
||||
const CODE_NOT_FOUND = 'SavedObjectsClient/notFound';
|
||||
export function decorateNotFoundError(error, reason) {
|
||||
return decorate(error, CODE_NOT_FOUND, 404, reason);
|
||||
export function createGenericNotFoundError() {
|
||||
return decorate(Boom.notFound(), CODE_NOT_FOUND, 404);
|
||||
}
|
||||
export function isNotFoundError(error) {
|
||||
return error && error[code] === CODE_NOT_FOUND;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Boom from 'boom';
|
||||
import uuid from 'uuid';
|
||||
import Boom from 'boom';
|
||||
|
||||
import { getRootType } from '../../mappings';
|
||||
|
||||
|
@ -27,6 +27,62 @@ export class SavedObjectsClient {
|
|||
this._unwrappedCallCluster = callCluster;
|
||||
}
|
||||
|
||||
/**
|
||||
* ## SavedObjectsClient errors
|
||||
*
|
||||
* Since the SavedObjectsClient has its hands in everything we
|
||||
* are a little paranoid about the way we present errors back to
|
||||
* to application code. Ideally, all errors will be either:
|
||||
*
|
||||
* 1. Caused by bad implementation (ie. undefined is not a function) and
|
||||
* as such unpredictable
|
||||
* 2. An error that has been classified and decorated appropriately
|
||||
* by the decorators in `./lib/errors`
|
||||
*
|
||||
* Type 1 errors are inevitable, but since all expected/handle-able errors
|
||||
* should be Type 2 the `isXYZError()` helpers exposed at
|
||||
* `savedObjectsClient.errors` should be used to understand and manage error
|
||||
* responses from the `SavedObjectsClient`.
|
||||
*
|
||||
* Type 2 errors are decorated versions of the source error, so if
|
||||
* the elasticsearch client threw an error it will be decorated based
|
||||
* on its type. That means that rather than looking for `error.body.error.type` or
|
||||
* doing substring checks on `error.body.error.reason`, just use the helpers to
|
||||
* understand the meaning of the error:
|
||||
*
|
||||
* ```js
|
||||
* if (savedObjectsClient.errors.isNotFoundError(error)) {
|
||||
* // handle 404
|
||||
* }
|
||||
*
|
||||
* if (savedObjectsClient.errors.isNotAuthorizedError(error)) {
|
||||
* // 401 handling should be automatic, but in case you wanted to know
|
||||
* }
|
||||
*
|
||||
* // always rethrow the error unless you handle it
|
||||
* throw error;
|
||||
* ```
|
||||
*
|
||||
* ### 404s from missing index
|
||||
*
|
||||
* From the perspective of application code and APIs the SavedObjectsClient is
|
||||
* a black box that persists objects. One of the internal details that users have
|
||||
* no control over is that we use an elasticsearch index for persistance and that
|
||||
* index might be missing.
|
||||
*
|
||||
* At the time of writing we are in the process of transitioning away from the
|
||||
* operating assumption that the SavedObjects index is always available. Part of
|
||||
* this transition is handling errors resulting from an index missing. These used
|
||||
* to trigger a 500 error in most cases, and in others cause 404s with different
|
||||
* error messages.
|
||||
*
|
||||
* From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The
|
||||
* object the request/call was targetting could not be found. This is why #14141
|
||||
* takes special care to ensure that 404 errors are generic and don't distinguish
|
||||
* between index missing or document missing.
|
||||
*
|
||||
* @type {ErrorHelpers} see ./lib/errors
|
||||
*/
|
||||
static errors = errors
|
||||
errors = errors
|
||||
|
||||
|
@ -168,11 +224,24 @@ export class SavedObjectsClient {
|
|||
type: this._type,
|
||||
index: this._index,
|
||||
refresh: 'wait_for',
|
||||
ignore: [404],
|
||||
});
|
||||
|
||||
if (response.found === false) {
|
||||
throw errors.decorateNotFoundError(Boom.notFound());
|
||||
const deleted = response.result === 'deleted';
|
||||
if (deleted) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const docNotFound = response.result === 'not_found';
|
||||
const indexNotFound = response.error && response.error.type === 'index_not_found_exception';
|
||||
if (docNotFound || indexNotFound) {
|
||||
// see "404s from missing index" above
|
||||
throw errors.createGenericNotFoundError();
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unexpected Elasticsearch DELETE response: ${JSON.stringify({ type, id, response, })}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -213,6 +282,7 @@ export class SavedObjectsClient {
|
|||
size: perPage,
|
||||
from: perPage * (page - 1),
|
||||
_source: includedFields(type, fields),
|
||||
ignore: [404],
|
||||
body: {
|
||||
version: true,
|
||||
...getSearchDsl(this._mappings, {
|
||||
|
@ -227,6 +297,17 @@ export class SavedObjectsClient {
|
|||
|
||||
const response = await this._callCluster('search', esOptions);
|
||||
|
||||
if (response.status === 404) {
|
||||
// 404 is only possible here if the index is missing, which
|
||||
// we don't want to leak, see "404s from missing index" above
|
||||
return {
|
||||
page,
|
||||
per_page: perPage,
|
||||
total: 0,
|
||||
saved_objects: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
per_page: perPage,
|
||||
|
@ -275,13 +356,14 @@ export class SavedObjectsClient {
|
|||
saved_objects: response.docs.map((doc, i) => {
|
||||
const { id, type } = objects[i];
|
||||
|
||||
if (doc.found === false) {
|
||||
if (!doc.found) {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
error: { statusCode: 404, message: 'Not found' }
|
||||
};
|
||||
}
|
||||
|
||||
const time = doc._source.updated_at;
|
||||
return {
|
||||
id,
|
||||
|
@ -306,7 +388,16 @@ export class SavedObjectsClient {
|
|||
id: this._generateEsId(type, id),
|
||||
type: this._type,
|
||||
index: this._index,
|
||||
ignore: [404]
|
||||
});
|
||||
|
||||
const docNotFound = response.found === false;
|
||||
const indexNotFound = response.status === 404;
|
||||
if (docNotFound || indexNotFound) {
|
||||
// see "404s from missing index" above
|
||||
throw errors.createGenericNotFoundError();
|
||||
}
|
||||
|
||||
const { updated_at: updatedAt } = response._source;
|
||||
|
||||
return {
|
||||
|
@ -335,6 +426,7 @@ export class SavedObjectsClient {
|
|||
index: this._index,
|
||||
version: options.version,
|
||||
refresh: 'wait_for',
|
||||
ignore: [404],
|
||||
body: {
|
||||
doc: {
|
||||
updated_at: time,
|
||||
|
@ -343,6 +435,11 @@ export class SavedObjectsClient {
|
|||
},
|
||||
});
|
||||
|
||||
if (response.status === 404) {
|
||||
// see "404s from missing index" above
|
||||
throw errors.createGenericNotFoundError();
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
|
|
|
@ -60,7 +60,7 @@ describe('SavedObjectsClient', () => {
|
|||
|
||||
savedObjectsClient._request('POST', '/api/path', params);
|
||||
|
||||
expect($http.calledOnce).to.be(true);
|
||||
sinon.assert.calledOnce($http);
|
||||
});
|
||||
|
||||
it('throws error when body is provided for GET', async () => {
|
||||
|
@ -227,8 +227,9 @@ describe('SavedObjectsClient', () => {
|
|||
|
||||
savedObjectsClient.update('index-pattern', 'logstash-*', attributes, options);
|
||||
sinon.assert.calledOnce($http);
|
||||
|
||||
expect($http.getCall(0).args[0].data).to.eql(body);
|
||||
sinon.assert.calledWithExactly($http, sinon.match({
|
||||
data: body
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -268,7 +269,9 @@ describe('SavedObjectsClient', () => {
|
|||
savedObjectsClient.create('index-pattern', attributes, { id: 'myId' });
|
||||
|
||||
sinon.assert.calledOnce($http);
|
||||
expect($http.getCall(0).args[0].url).to.eql(url);
|
||||
sinon.assert.calledWithExactly($http, sinon.match({
|
||||
url
|
||||
}));
|
||||
});
|
||||
|
||||
it('makes HTTP call', () => {
|
||||
|
@ -276,7 +279,12 @@ describe('SavedObjectsClient', () => {
|
|||
savedObjectsClient.create('index-pattern', attributes);
|
||||
|
||||
sinon.assert.calledOnce($http);
|
||||
expect($http.getCall(0).args[0].data.attributes).to.eql(attributes);
|
||||
sinon.assert.calledWithExactly($http, sinon.match({
|
||||
url: sinon.match.string,
|
||||
data: {
|
||||
attributes
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -295,31 +303,30 @@ describe('SavedObjectsClient', () => {
|
|||
const body = { type: 'index-pattern', invalid: true };
|
||||
|
||||
savedObjectsClient.find(body);
|
||||
expect($http.calledOnce).to.be(true);
|
||||
|
||||
const options = $http.getCall(0).args[0];
|
||||
expect(options.url).to.eql(`${basePath}/api/saved_objects/?type=index-pattern&invalid=true`);
|
||||
sinon.assert.calledOnce($http);
|
||||
sinon.assert.calledWithExactly($http, sinon.match({
|
||||
url: `${basePath}/api/saved_objects/?type=index-pattern&invalid=true`
|
||||
}));
|
||||
});
|
||||
|
||||
it('accepts fields', () => {
|
||||
const body = { fields: ['title', 'description'] };
|
||||
|
||||
savedObjectsClient.find(body);
|
||||
expect($http.calledOnce).to.be(true);
|
||||
|
||||
const options = $http.getCall(0).args[0];
|
||||
expect(options.url).to.eql(`${basePath}/api/saved_objects/?fields=title&fields=description`);
|
||||
sinon.assert.calledOnce($http);
|
||||
sinon.assert.calledWithExactly($http, sinon.match({
|
||||
url: `${basePath}/api/saved_objects/?fields=title&fields=description`
|
||||
}));
|
||||
});
|
||||
|
||||
it('accepts from/size', () => {
|
||||
const body = { from: 50, size: 10 };
|
||||
|
||||
savedObjectsClient.find(body);
|
||||
expect($http.calledOnce).to.be(true);
|
||||
|
||||
const options = $http.getCall(0).args[0];
|
||||
expect(options.url).to.eql(`${basePath}/api/saved_objects/?from=50&size=10`);
|
||||
|
||||
sinon.assert.calledOnce($http);
|
||||
sinon.assert.alwaysCalledWith($http, sinon.match({
|
||||
url: `${basePath}/api/saved_objects/?from=50&size=10`
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,6 @@ export function assertDocMissingResponse({ result }) {
|
|||
assertSinonMatch(result, {
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: sinon.match('document_missing_exception')
|
||||
.and(sinon.match('document missing'))
|
||||
message: 'Not Found'
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { esTestConfig } from '../../src/test_utils/es';
|
||||
import { kibanaTestServerUrlParts } from '../../test/kibana_test_server_url_parts';
|
||||
|
||||
const SECOND = 1000;
|
||||
const MINUTE = 60 * SECOND;
|
||||
const HOUR = 60 * MINUTE;
|
||||
|
||||
module.exports = function (grunt) {
|
||||
const platform = require('os').platform();
|
||||
const binScript = /^win/.test(platform) ? '.\\bin\\kibana.bat' : './bin/kibana';
|
||||
|
@ -55,6 +59,7 @@ module.exports = function (grunt) {
|
|||
...stdDevArgs,
|
||||
'--optimize.enabled=false',
|
||||
'--elasticsearch.url=' + esTestConfig.getUrl(),
|
||||
'--elasticsearch.healthCheck.delay=' + HOUR,
|
||||
'--server.port=' + kibanaTestServerUrlParts.port,
|
||||
'--server.xsrf.disableProtection=true',
|
||||
...kbnServerFlags,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export default function ({ loadTestFile }) {
|
||||
describe('apis', () => {
|
||||
loadTestFile(require.resolve('./index_patterns'));
|
||||
loadTestFile(require.resolve('./saved_objects'));
|
||||
loadTestFile(require.resolve('./scripts'));
|
||||
loadTestFile(require.resolve('./search'));
|
||||
loadTestFile(require.resolve('./suggestions'));
|
||||
|
|
122
test/api_integration/apis/saved_objects/bulk_get.js
Normal file
122
test/api_integration/apis/saved_objects/bulk_get.js
Normal file
|
@ -0,0 +1,122 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
const BULK_REQUESTS = [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'does not exist',
|
||||
},
|
||||
{
|
||||
type: 'config',
|
||||
id: '7.0.0-alpha1',
|
||||
},
|
||||
];
|
||||
|
||||
describe('bulk_get', () => {
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
||||
it('should return 200 with individual responses', async () => (
|
||||
await supertest
|
||||
.post(`/api/saved_objects/bulk_get`)
|
||||
.send(BULK_REQUESTS)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
type: 'visualization',
|
||||
updated_at: '2017-09-21T18:51:23.794Z',
|
||||
version: resp.body.saved_objects[0].version,
|
||||
attributes: {
|
||||
title: 'Count of requests',
|
||||
description: '',
|
||||
version: 1,
|
||||
// cheat for some of the more complex attributes
|
||||
visState: resp.body.saved_objects[0].attributes.visState,
|
||||
uiStateJSON: resp.body.saved_objects[0].attributes.uiStateJSON,
|
||||
kibanaSavedObjectMeta: resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'does not exist',
|
||||
type: 'dashboard',
|
||||
error: {
|
||||
statusCode: 404,
|
||||
message: 'Not found'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '7.0.0-alpha1',
|
||||
type: 'config',
|
||||
updated_at: '2017-09-21T18:49:16.302Z',
|
||||
version: resp.body.saved_objects[2].version,
|
||||
attributes: {
|
||||
buildNum: 8467,
|
||||
defaultIndex: '91200a00-9efd-11e7-acb3-3dab96693fab'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
describe('without kibana index', () => {
|
||||
before(async () => (
|
||||
// just in case the kibana server has recreated it
|
||||
await es.indices.delete({
|
||||
index: '.kibana',
|
||||
ignore: [404],
|
||||
})
|
||||
));
|
||||
|
||||
it('should return 200 with individual responses', async () => (
|
||||
await supertest
|
||||
.post('/api/saved_objects/bulk_get')
|
||||
.send(BULK_REQUESTS)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
type: 'visualization',
|
||||
error: {
|
||||
statusCode: 404,
|
||||
message: 'Not found'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'does not exist',
|
||||
type: 'dashboard',
|
||||
error: {
|
||||
statusCode: 404,
|
||||
message: 'Not found'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '7.0.0-alpha1',
|
||||
type: 'config',
|
||||
error: {
|
||||
statusCode: 404,
|
||||
message: 'Not found'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
77
test/api_integration/apis/saved_objects/create.js
Normal file
77
test/api_integration/apis/saved_objects/create.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('create', () => {
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
it('should return 200', async () => {
|
||||
await supertest
|
||||
.post(`/api/saved_objects/visualization`)
|
||||
.send({
|
||||
attributes: {
|
||||
title: 'My favorite vis'
|
||||
}
|
||||
})
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
// loose uuid validation
|
||||
expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/);
|
||||
|
||||
// loose ISO8601 UTC time with milliseconds validation
|
||||
expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/);
|
||||
|
||||
expect(resp.body).to.eql({
|
||||
id: resp.body.id,
|
||||
type: 'visualization',
|
||||
updated_at: resp.body.updated_at,
|
||||
version: 1,
|
||||
attributes: {
|
||||
title: 'My favorite vis'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without kibana index', () => {
|
||||
before(async () => (
|
||||
// just in case the kibana server has recreated it
|
||||
await es.indices.delete({
|
||||
index: '.kibana',
|
||||
ignore: [404],
|
||||
})
|
||||
));
|
||||
|
||||
it('should return 503 and not create kibana index', async () => {
|
||||
await supertest
|
||||
.post(`/api/saved_objects/visualization`)
|
||||
.send({
|
||||
attributes: {
|
||||
title: 'My favorite vis'
|
||||
}
|
||||
})
|
||||
.expect(503)
|
||||
.then(resp => {
|
||||
// loose uuid validation
|
||||
expect(resp.body).to.eql({
|
||||
error: 'Service Unavailable',
|
||||
statusCode: 503,
|
||||
message: 'Service Unavailable'
|
||||
});
|
||||
});
|
||||
|
||||
const index = await es.indices.get({
|
||||
index: '.kibana',
|
||||
ignore: [404]
|
||||
});
|
||||
|
||||
expect(index).to.have.property('status', 404);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
59
test/api_integration/apis/saved_objects/delete.js
Normal file
59
test/api_integration/apis/saved_objects/delete.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('delete', () => {
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
||||
it('should return 200 when deleting a doc', async () => (
|
||||
await supertest
|
||||
.delete(`/api/saved_objects/dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab`)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({});
|
||||
})
|
||||
));
|
||||
|
||||
it('should return generic 404 when deleting an unknown doc', async () => (
|
||||
await supertest
|
||||
.delete(`/api/saved_objects/dashboard/not-a-real-id`)
|
||||
.expect(404)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found'
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
describe('without kibana index', () => {
|
||||
before(async () => (
|
||||
// just in case the kibana server has recreated it
|
||||
await es.indices.delete({
|
||||
index: '.kibana',
|
||||
ignore: [404],
|
||||
})
|
||||
));
|
||||
|
||||
it('returns generic 404 when kibana index is missing', async () => (
|
||||
await supertest
|
||||
.delete(`/api/saved_objects/dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab`)
|
||||
.expect(404)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found'
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
157
test/api_integration/apis/saved_objects/find.js
Normal file
157
test/api_integration/apis/saved_objects/find.js
Normal file
|
@ -0,0 +1,157 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('find', () => {
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
||||
it('should return 200 with individual responses', async () => (
|
||||
await supertest
|
||||
.get('/api/saved_objects/visualization?fields=title')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
version: 1,
|
||||
attributes: {
|
||||
'title': 'Count of requests'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
));
|
||||
|
||||
describe('unknown type', () => {
|
||||
it('should return 200 with empty response', async () => (
|
||||
await supertest
|
||||
.get('/api/saved_objects/wigwags')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
saved_objects: []
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
describe('page beyond total', () => {
|
||||
it('should return 200 with empty response', async () => (
|
||||
await supertest
|
||||
.get('/api/saved_objects/visualization?page=100&per_page=100')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 100,
|
||||
per_page: 100,
|
||||
total: 1,
|
||||
saved_objects: []
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
describe('unknown search field', () => {
|
||||
it('should return 200 with empty response', async () => (
|
||||
await supertest
|
||||
.get('/api/saved_objects/wigwags?search_fields=a')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
saved_objects: []
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('without kibana index', () => {
|
||||
before(async () => (
|
||||
// just in case the kibana server has recreated it
|
||||
await es.indices.delete({
|
||||
index: '.kibana',
|
||||
ignore: [404],
|
||||
})
|
||||
));
|
||||
|
||||
it('should return 200 with empty response', async () => (
|
||||
await supertest
|
||||
.get('/api/saved_objects/visualization')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
saved_objects: []
|
||||
});
|
||||
})
|
||||
));
|
||||
|
||||
describe('unknown type', () => {
|
||||
it('should return 200 with empty response', async () => (
|
||||
await supertest
|
||||
.get('/api/saved_objects/wigwags')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
saved_objects: []
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
describe('page beyond total', () => {
|
||||
it('should return 200 with empty response', async () => (
|
||||
await supertest
|
||||
.get('/api/saved_objects/visualization?page=100&per_page=100')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 100,
|
||||
per_page: 100,
|
||||
total: 0,
|
||||
saved_objects: []
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
describe('unknown search field', () => {
|
||||
it('should return 200 with empty response', async () => (
|
||||
await supertest
|
||||
.get('/api/saved_objects/wigwags?search_fields=a')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
saved_objects: []
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
75
test/api_integration/apis/saved_objects/get.js
Normal file
75
test/api_integration/apis/saved_objects/get.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('get', () => {
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
||||
it('should return 200', async () => (
|
||||
await supertest
|
||||
.get(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
type: 'visualization',
|
||||
updated_at: '2017-09-21T18:51:23.794Z',
|
||||
version: resp.body.version,
|
||||
attributes: {
|
||||
title: 'Count of requests',
|
||||
description: '',
|
||||
version: 1,
|
||||
// cheat for some of the more complex attributes
|
||||
visState: resp.body.attributes.visState,
|
||||
uiStateJSON: resp.body.attributes.uiStateJSON,
|
||||
kibanaSavedObjectMeta: resp.body.attributes.kibanaSavedObjectMeta
|
||||
}
|
||||
});
|
||||
})
|
||||
));
|
||||
|
||||
describe('doc does not exist', () => {
|
||||
it('should return same generic error as when index does not exist', async () => (
|
||||
await supertest
|
||||
.get(`/api/saved_objects/visualization/foobar`)
|
||||
.expect(404)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
statusCode: 404,
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('without kibana index', () => {
|
||||
before(async () => (
|
||||
// just in case the kibana server has recreated it
|
||||
await es.indices.delete({
|
||||
index: '.kibana',
|
||||
ignore: [404],
|
||||
})
|
||||
));
|
||||
|
||||
it('should return basic 404 without mentioning index', async () => (
|
||||
await supertest
|
||||
.get('/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab')
|
||||
.expect(404)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
statusCode: 404,
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
10
test/api_integration/apis/saved_objects/index.js
Normal file
10
test/api_integration/apis/saved_objects/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
export default function ({ loadTestFile }) {
|
||||
describe('saved_objects', () => {
|
||||
loadTestFile(require.resolve('./bulk_get'));
|
||||
loadTestFile(require.resolve('./create'));
|
||||
loadTestFile(require.resolve('./delete'));
|
||||
loadTestFile(require.resolve('./find'));
|
||||
loadTestFile(require.resolve('./get'));
|
||||
loadTestFile(require.resolve('./update'));
|
||||
});
|
||||
}
|
89
test/api_integration/apis/saved_objects/update.js
Normal file
89
test/api_integration/apis/saved_objects/update.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('update', () => {
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
it('should return 200', async () => {
|
||||
await supertest
|
||||
.put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`)
|
||||
.send({
|
||||
attributes: {
|
||||
title: 'My second favorite vis'
|
||||
}
|
||||
})
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
// loose uuid validation
|
||||
expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/);
|
||||
|
||||
// loose ISO8601 UTC time with milliseconds validation
|
||||
expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/);
|
||||
|
||||
expect(resp.body).to.eql({
|
||||
id: resp.body.id,
|
||||
type: 'visualization',
|
||||
updated_at: resp.body.updated_at,
|
||||
version: 2,
|
||||
attributes: {
|
||||
title: 'My second favorite vis'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unknown id', () => {
|
||||
it('should return a generic 404', async () => {
|
||||
await supertest
|
||||
.put(`/api/saved_objects/visualization/not an id`)
|
||||
.send({
|
||||
attributes: {
|
||||
title: 'My second favorite vis'
|
||||
}
|
||||
})
|
||||
.expect(404)
|
||||
.then(resp => {
|
||||
expect(resp.body).eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without kibana index', () => {
|
||||
before(async () => (
|
||||
// just in case the kibana server has recreated it
|
||||
await es.indices.delete({
|
||||
index: '.kibana',
|
||||
ignore: [404],
|
||||
})
|
||||
));
|
||||
|
||||
it('should return generic 404', async () => (
|
||||
await supertest
|
||||
.put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`)
|
||||
.send({
|
||||
attributes: {
|
||||
title: 'My second favorite vis'
|
||||
}
|
||||
})
|
||||
.expect(404)
|
||||
.then(resp => {
|
||||
expect(resp.body).eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found'
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,252 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"number_of_replicas": "1"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"doc": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"config": {
|
||||
"dynamic": "true",
|
||||
"properties": {
|
||||
"buildNum": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"defaultIndex": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 256
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"panelsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"refreshInterval": {
|
||||
"properties": {
|
||||
"display": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"pause": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"section": {
|
||||
"type": "integer"
|
||||
},
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeFrom": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timeRestore": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"timeTo": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"index-pattern": {
|
||||
"properties": {
|
||||
"fieldFormatMap": {
|
||||
"type": "text"
|
||||
},
|
||||
"fields": {
|
||||
"type": "text"
|
||||
},
|
||||
"intervalName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"notExpandable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sourceFilters": {
|
||||
"type": "text"
|
||||
},
|
||||
"timeFieldName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"properties": {
|
||||
"columns": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timelion-sheet": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timelion_chart_height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_columns": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_other_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_rows": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_sheet": {
|
||||
"type": "text"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "date"
|
||||
},
|
||||
"url": {
|
||||
"properties": {
|
||||
"accessCount": {
|
||||
"type": "long"
|
||||
},
|
||||
"accessDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"createDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"url": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 2048
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualization": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"savedSearchId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"visState": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -45,9 +45,11 @@ export default function ({ getService, getPageObjects }) {
|
|||
await PageObjects.common.clickCancelOnModal();
|
||||
await PageObjects.dashboard.confirmClone();
|
||||
|
||||
// Should see the same confirmation if the title is the same.
|
||||
const isConfirmOpen = await PageObjects.common.isConfirmModalOpen();
|
||||
expect(isConfirmOpen).to.equal(true);
|
||||
await retry.try(async () => {
|
||||
// Should see the same confirmation if the title is the same.
|
||||
const isConfirmOpen = await PageObjects.common.isConfirmModalOpen();
|
||||
expect(isConfirmOpen).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('and doesn\'t save', async() => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue