From ddc1d9c5bf0a7c368ff2e9e282112c11157fbc46 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 2 Oct 2017 18:51:58 -0700 Subject: [PATCH] [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 --- .../client/__tests__/saved_objects_client.js | 147 +++++----- .../client/lib/__tests__/decorate_es_error.js | 8 +- .../client/lib/__tests__/errors.js | 38 +-- .../client/lib/decorate_es_error.js | 4 +- src/server/saved_objects/client/lib/errors.js | 4 +- .../client/saved_objects_client.js | 105 +++++++- .../__tests__/saved_objects_client.js | 43 +-- .../routes/__tests__/lib/assert.js | 3 +- tasks/config/run.js | 5 + test/api_integration/apis/index.js | 1 + .../apis/saved_objects/bulk_get.js | 122 +++++++++ .../apis/saved_objects/create.js | 77 ++++++ .../apis/saved_objects/delete.js | 59 ++++ .../apis/saved_objects/find.js | 157 +++++++++++ .../api_integration/apis/saved_objects/get.js | 75 ++++++ .../apis/saved_objects/index.js | 10 + .../apis/saved_objects/update.js | 89 +++++++ .../saved_objects/basic/data.json.gz | Bin 0 -> 1803 bytes .../saved_objects/basic/mappings.json | 252 ++++++++++++++++++ .../apps/dashboard/_dashboard_clone.js | 8 +- 20 files changed, 1076 insertions(+), 131 deletions(-) create mode 100644 test/api_integration/apis/saved_objects/bulk_get.js create mode 100644 test/api_integration/apis/saved_objects/create.js create mode 100644 test/api_integration/apis/saved_objects/delete.js create mode 100644 test/api_integration/apis/saved_objects/find.js create mode 100644 test/api_integration/apis/saved_objects/get.js create mode 100644 test/api_integration/apis/saved_objects/index.js create mode 100644 test/api_integration/apis/saved_objects/update.js create mode 100644 test/api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz create mode 100644 test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json diff --git a/src/server/saved_objects/client/__tests__/saved_objects_client.js b/src/server/saved_objects/client/__tests__/saved_objects_client.js index bec53649412d2d..866450e5aae32f 100644 --- a/src/server/saved_objects/client/__tests__/saved_objects_client.js +++ b/src/server/saved_objects/client/__tests__/saved_objects_client.js @@ -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(onBeforeWrite); - - const args = callAdminCluster.getCall(0).args; + 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' } } + ] + })); - 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' } } - ]); + sinon.assert.calledOnce(onBeforeWrite); }); 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); }); }); diff --git a/src/server/saved_objects/client/lib/__tests__/decorate_es_error.js b/src/server/saved_objects/client/lib/__tests__/decorate_es_error.js index 4a6c09495c3316..a8d8e64b90cd49 100644 --- a/src/server/saved_objects/client/lib/__tests__/decorate_es_error.js +++ b/src/server/saved_objects/client/lib/__tests__/decorate_es_error.js @@ -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', () => { diff --git a/src/server/saved_objects/client/lib/__tests__/errors.js b/src/server/saved_objects/client/lib/__tests__/errors.js index f912d209448508..e4b7802509d3b5 100644 --- a/src/server/saved_objects/client/lib/__tests__/errors.js +++ b/src/server/saved_objects/client/lib/__tests__/errors.js @@ -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); }); }); diff --git a/src/server/saved_objects/client/lib/decorate_es_error.js b/src/server/saved_objects/client/lib/decorate_es_error.js index 9f721e25eb6ba9..969b2791bff283 100644 --- a/src/server/saved_objects/client/lib/decorate_es_error.js +++ b/src/server/saved_objects/client/lib/decorate_es_error.js @@ -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) { diff --git a/src/server/saved_objects/client/lib/errors.js b/src/server/saved_objects/client/lib/errors.js index 6ec533e84928a3..2aa22c7db817f8 100644 --- a/src/server/saved_objects/client/lib/errors.js +++ b/src/server/saved_objects/client/lib/errors.js @@ -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; diff --git a/src/server/saved_objects/client/saved_objects_client.js b/src/server/saved_objects/client/saved_objects_client.js index 9afc0ba9fddefe..b9e840069d1b11 100644 --- a/src/server/saved_objects/client/saved_objects_client.js +++ b/src/server/saved_objects/client/saved_objects_client.js @@ -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, diff --git a/src/ui/public/saved_objects/__tests__/saved_objects_client.js b/src/ui/public/saved_objects/__tests__/saved_objects_client.js index 742cf40afdd2f7..eaa0e04af351ae 100644 --- a/src/ui/public/saved_objects/__tests__/saved_objects_client.js +++ b/src/ui/public/saved_objects/__tests__/saved_objects_client.js @@ -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` + })); }); }); }); diff --git a/src/ui/ui_settings/routes/__tests__/lib/assert.js b/src/ui/ui_settings/routes/__tests__/lib/assert.js index 2cf35d3d1a0307..48e8efefe6447b 100644 --- a/src/ui/ui_settings/routes/__tests__/lib/assert.js +++ b/src/ui/ui_settings/routes/__tests__/lib/assert.js @@ -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' }); } diff --git a/tasks/config/run.js b/tasks/config/run.js index 04b56baed0f687..8944cbb12bc139 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -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, diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index 8f533c898eabe4..cc2f5375eee61f 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -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')); diff --git a/test/api_integration/apis/saved_objects/bulk_get.js b/test/api_integration/apis/saved_objects/bulk_get.js new file mode 100644 index 00000000000000..a34f81a06abe94 --- /dev/null +++ b/test/api_integration/apis/saved_objects/bulk_get.js @@ -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' + } + } + ] + }); + }) + )); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects/create.js b/test/api_integration/apis/saved_objects/create.js new file mode 100644 index 00000000000000..86d31da38653be --- /dev/null +++ b/test/api_integration/apis/saved_objects/create.js @@ -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); + }); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects/delete.js b/test/api_integration/apis/saved_objects/delete.js new file mode 100644 index 00000000000000..39e239936e9d05 --- /dev/null +++ b/test/api_integration/apis/saved_objects/delete.js @@ -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' + }); + }) + )); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js new file mode 100644 index 00000000000000..b24a6a6fe69aff --- /dev/null +++ b/test/api_integration/apis/saved_objects/find.js @@ -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: [] + }); + }) + )); + }); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects/get.js b/test/api_integration/apis/saved_objects/get.js new file mode 100644 index 00000000000000..d44e76be86d23a --- /dev/null +++ b/test/api_integration/apis/saved_objects/get.js @@ -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, + }); + }) + )); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects/index.js b/test/api_integration/apis/saved_objects/index.js new file mode 100644 index 00000000000000..62c9802da29ea5 --- /dev/null +++ b/test/api_integration/apis/saved_objects/index.js @@ -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')); + }); +} diff --git a/test/api_integration/apis/saved_objects/update.js b/test/api_integration/apis/saved_objects/update.js new file mode 100644 index 00000000000000..e1afc1bb691449 --- /dev/null +++ b/test/api_integration/apis/saved_objects/update.js @@ -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' + }); + }) + )); + }); + }); +} diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz b/test/api_integration/fixtures/es_archiver/saved_objects/basic/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..c07188439b0e07bf044e928e7f3a9346c2e46092 GIT binary patch literal 1803 zcmV+m2lV(KiwFP!000026V+Q=Z`(E$e$THkytg4XBs+~0J+{7_@jK zaiv6!q;hGX|2~qsC(dnY>f@I-IwTK2e&a04be(HdkH9QrfQBc3-ogosXj9~Ki5uI7_DC9A{z z{lj@gR?T=N3K@1&_a+(Wth<}$BBw+HT@r0O%{+fG@eU_5|JXkW_6`Goe>z)ue|P;lOMY;m{XgH1VY0kW?a_bOLU$_BC6%Hg8Gr7>thb8$q zIGB>p>QaOHArAcmz=WI`df2r*SU>+{EO4PAQ%q!doTr6^$$?@J(@+RWSw?QSl4)G# zg4GeY0c6w^R!mGO86ty&iJ`>wGE*bkEH;QRMO-2jqKanGU}ER`=5=$`?76qDk2I8n zgUV7U>O5!JH$(}?!NFvul!OV(U@8!i)M3KNlq4yZ6qG_L9PS#=DoOk6(j?3L8L?8i;v)6w9T4dV3gCJ6~kVB9P$|44I zY2QNPcma6}(!5x4rjl)dsvy*;ULD+x6H^tv1*s-6dNvJ4#7v4fFu(QPw3uCU2opD- zEJgM4?-qbtz+sR8ixZ8W${Oc3FnZ3YPVf`zVB+ReF%3&xyYM?e;^xvF$H2^^1Bjf% z=l#C=bP5eD+zb|yVj~`rFS{01b4@GLE2YdUA-Zdl&5h=KS+br!tm#}^& zhi6O`gtPC&{yGdOU4$f>J@Jm%KG-c)lc4Z^mCsfj;?*Nmzl3iIR1y@aq=1BBM1I?ALH zaoz=N3#4?RPa0IMPHvFfQ$DtxlNIoK)k8Jrzbtf_Ht;>q_H8)5D$7&z=)CdfYQ9wB zb}4PfdIynSw?wDs=K8ql{*=|3=2X!7l!=fj;rFdr3SF(&=FEt3ZBK`^_Pv7-^Cw*B z&fBcTPhFCg4AiH_vA%XQI(2Z1C$_MaCgHdG?2`#6QqAQS(4TC#<=(HBNKIjI?)e+& zbAarVJ>#!>92yX>nG8Afgr*C~T<~g%10K&BlioD2TPLCPSP1T}hP1_m(svL;MmrIA z&!756mfd$nd&|y})F?F1?}_}@*`TaS)HQdjoCz{pw0d)O0n~D&!UR&%9FvkPycYJQ zE3mcN**$7)c3UP)X%ROE3VZq$#A$P}sh?NeMb&k;3LW`pq%tchw5V!$Vf3eO`)#x8 zqhZRjrAhD0rpo)f_5BlSa_-6qX}zTKwCrceg1IF5=h4Rxb)r=HWreNwx^9n=6`WUr zz*`fP18u!`jaJ(r9fMSr*zZZJI(0uZ zy6RYJW7s*B$^NolYxC#H*!ANiiwpN6()xW7Lgte8xJ6a|%qmTZN>+kM8mwTxn9qq@ zDE#jV-RgT_bzTH}-t=I$->c4@iuING^O}O3Fm117dk^!MoMa{|H|BPkl6{AzHTeE+ z{a`INcM)a!zOCKCAC!{*E~1|pAF|h}x6Qm(DqW)X%{I3OQ#kOd=a$susACiO?H2yd zcq{LZ5t|r38#0Bo)VjPgY&;fL)f>r1N05*FgORb{i;+JIW_!W>VD!iP3~;5s9fs+}5O5F6q@-zcy6!!{?s7@E?V}G3DSv0085qc~bxY literal 0 HcmV?d00001 diff --git a/test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json b/test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json new file mode 100644 index 00000000000000..26c62bca335d94 --- /dev/null +++ b/test/api_integration/fixtures/es_archiver/saved_objects/basic/mappings.json @@ -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" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/test/functional/apps/dashboard/_dashboard_clone.js b/test/functional/apps/dashboard/_dashboard_clone.js index 3f51f01019f52b..ffe737f0d4d687 100644 --- a/test/functional/apps/dashboard/_dashboard_clone.js +++ b/test/functional/apps/dashboard/_dashboard_clone.js @@ -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() => {