diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f23d2df2fb092..4f050e3bf422f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -826,6 +826,12 @@ packages/kbn-yarn-lock-validator @elastic/kibana-operations /x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js @elastic/kibana-data-discovery /x-pack/test/stack_functional_integration/apps/management/_index_pattern_create.js @elastic/kibana-data-discovery /x-pack/test/upgrade/apps/discover @elastic/kibana-data-discovery +/x-pack/test_serverless/api_integration/test_suites/common/data_views @elastic/kibana-data-discovery +/x-pack/test_serverless/api_integration/test_suites/common/data_view_field_editor @elastic/kibana-data-discovery +/x-pack/test_serverless/api_integration/test_suites/common/kql_telemetry @elastic/kibana-data-discovery +/x-pack/test_serverless/api_integration/test_suites/common/scripts_tests @elastic/kibana-data-discovery +/x-pack/test_serverless/api_integration/test_suites/common/search_oss @elastic/kibana-data-discovery +/x-pack/test_serverless/api_integration/test_suites/common/search_xpack @elastic/kibana-data-discovery # Visualizations /src/plugins/visualize/ @elastic/kibana-visualizations diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_view_field_editor/field_preview.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_view_field_editor/field_preview.ts new file mode 100644 index 0000000000000..d351c91faea7f --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_view_field_editor/field_preview.ts @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { getErrorCodeFromErrorReason } from '@kbn/data-view-field-editor-plugin/public/lib/runtime_field_validation'; +import { + FIELD_PREVIEW_PATH, + INITIAL_REST_VERSION, +} from '@kbn/data-view-field-editor-plugin/common/constants'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; + +const INDEX_NAME = 'api-integration-test-field-preview'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const svlCommonApi = getService('svlCommonApi'); + + const document = { foo: 1, bar: 'hello' }; + + const createIndex = async () => { + await es.indices.create({ + index: INDEX_NAME, + body: { + mappings: { + properties: { + foo: { + type: 'integer', + }, + bar: { + type: 'keyword', + }, + }, + }, + }, + }); + }; + + const deleteIndex = async () => { + await es.indices.delete({ + index: INDEX_NAME, + }); + }; + + describe('Field preview', function () { + before(async () => await createIndex()); + after(async () => await deleteIndex()); + + describe('should return the script value', () => { + const tests = [ + { + context: 'keyword_field', + script: { + source: 'emit("test")', + }, + expected: 'test', + }, + { + context: 'long_field', + script: { + source: 'emit(doc["foo"].value + 1)', + }, + expected: 2, + }, + { + context: 'keyword_field', + script: { + source: 'emit(doc["bar"].value + " world")', + }, + expected: 'hello world', + }, + ]; + + tests.forEach((test) => { + it(`> ${test.context}`, async () => { + const payload = { + script: test.script, + document, + context: test.context, + index: INDEX_NAME, + }; + + const { body: response } = await supertest + .post(FIELD_PREVIEW_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send(payload) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(response.values).eql([test.expected]); + }); + }); + }); + + describe('payload validation', () => { + it('should require a script', async () => { + await supertest + .post(FIELD_PREVIEW_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + context: 'keyword_field', + index: INDEX_NAME, + }) + .set('kbn-xsrf', 'xxx') + .expect(400); + }); + + it('should require a context', async () => { + await supertest + .post(FIELD_PREVIEW_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + script: { source: 'emit("hello")' }, + index: INDEX_NAME, + }) + .set('kbn-xsrf', 'xxx') + .expect(400); + }); + + it('should require an index', async () => { + await supertest + .post(FIELD_PREVIEW_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + script: { source: 'emit("hello")' }, + context: 'keyword_field', + }) + .set('kbn-xsrf', 'xxx') + .expect(400); + }); + }); + + describe('Error messages', () => { + // As ES does not return error codes we will add a test to make sure its error message string + // does not change overtime as we rely on it to extract our own error code. + // If this test fail we'll need to update the "getErrorCodeFromErrorReason()" handler + // TODO: `response.error?.caused_by?.reason` returns + // `class_cast_exception: Cannot cast from [int] to [java.lang.String].` + // in Serverless, which causes `getErrorCodeFromErrorReason` to fail + it.skip('should detect a script casting error', async () => { + const { body: response } = await supertest + .post(FIELD_PREVIEW_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + script: { source: 'emit(123)' }, // We send a long but the type is "keyword" + context: 'keyword_field', + index: INDEX_NAME, + }) + .set('kbn-xsrf', 'xxx'); + + const errorCode = getErrorCodeFromErrorReason(response.error?.caused_by?.reason); + + expect(errorCode).be('CAST_ERROR'); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_view_field_editor/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_view_field_editor/index.ts new file mode 100644 index 0000000000000..561b4798d2c28 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_view_field_editor/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('index pattern field editor', () => { + loadTestFile(require.resolve('./field_preview')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/constants.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/constants.ts new file mode 100644 index 0000000000000..4292c1da7fa78 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/constants.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + DATA_VIEW_PATH_LEGACY, + SERVICE_KEY_LEGACY, + DATA_VIEW_PATH, + SERVICE_KEY, + SERVICE_PATH, + SERVICE_PATH_LEGACY, +} from '@kbn/data-views-plugin/server'; + +const legacyConfig = { + name: 'legacy index pattern api', + path: DATA_VIEW_PATH_LEGACY, + basePath: SERVICE_PATH_LEGACY, + serviceKey: SERVICE_KEY_LEGACY, +}; + +export const dataViewConfig = { + name: 'data view api', + path: DATA_VIEW_PATH, + basePath: SERVICE_PATH, + serviceKey: SERVICE_KEY, +}; + +export const configArray = [legacyConfig, dataViewConfig]; diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/create_data_view/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/create_data_view/index.ts new file mode 100644 index 0000000000000..b4955e4947bae --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/create_data_view/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('create_index_pattern', () => { + loadTestFile(require.resolve('./validation')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/create_data_view/main.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/create_data_view/main.ts new file mode 100644 index 0000000000000..c68f5bba43355 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/create_data_view/main.ts @@ -0,0 +1,334 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const svlCommonApi = getService('svlCommonApi'); + describe('main', () => { + configArray.forEach((config) => { + describe(config.name, () => { + it('can create an index_pattern with just a title', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + }, + }); + + expect(response.status).to.be(200); + }); + + it('returns back the created index_pattern object', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + }, + }); + + expect(typeof response.body[config.serviceKey]).to.be('object'); + expect(response.body[config.serviceKey].title).to.be(title); + expect(typeof response.body[config.serviceKey].id).to.be('string'); + expect(response.body[config.serviceKey].id.length > 0).to.be(true); + }); + + it('can specify primitive optional attributes when creating an index pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const id = `test-id-${Date.now()}-${Math.random()}*`; + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + id, + type: 'test-type', + timeFieldName: 'test-timeFieldName', + }, + }); + + expect(response.status).to.be(200); + expect(response.body[config.serviceKey].title).to.be(title); + expect(response.body[config.serviceKey].id).to.be(id); + expect(response.body[config.serviceKey].type).to.be('test-type'); + expect(response.body[config.serviceKey].timeFieldName).to.be('test-timeFieldName'); + }); + + it('can specify optional sourceFilters attribute when creating an index pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + sourceFilters: [ + { + value: 'foo', + }, + ], + }, + }); + + expect(response.status).to.be(200); + expect(response.body[config.serviceKey].title).to.be(title); + expect(response.body[config.serviceKey].sourceFilters[0].value).to.be('foo'); + }); + + describe('creating fields', () => { + before(async () => { + await esArchiver.load( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('can specify optional fields attribute when creating an index pattern', async () => { + const title = `basic_index*`; + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + fields: { + foo: { + name: 'foo', + // TODO: Scripted fields code dropped since they are not supported in Serverless + customLabel: 'Custom Label', + }, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body[config.serviceKey].title).to.be(title); + expect(response.body[config.serviceKey].fields.foo.name).to.be('foo'); + // TODO: Scripted fields code dropped since they are not supported in Serverless + expect(response.body[config.serviceKey].fields.foo.customLabel).to.be('Custom Label'); + + expect(response.body[config.serviceKey].fields.bar.name).to.be('bar'); // created from es index + expect(response.body[config.serviceKey].fields.bar.type).to.be('boolean'); + }); + + // TODO: Scripted fields code dropped since they are not supported in Serverless + it('can add fields created from es index', async () => { + const title = `basic_index*`; + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + fields: { + foo: { + name: 'foo', + type: 'string', + }, + fake: { + name: 'fake', + type: 'string', + }, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body[config.serviceKey].title).to.be(title); + + expect(response.body[config.serviceKey].fields.foo.name).to.be('foo'); + expect(response.body[config.serviceKey].fields.foo.type).to.be('number'); // picked up from index + + expect(response.body[config.serviceKey].fields.fake).to.be(undefined); // not in index, so not created + }); + + it('can add runtime fields', async () => { + const title = `basic_index*`; + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: 'emit(doc["foo"].value)', + }, + }, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body[config.serviceKey].title).to.be(title); + + expect(response.body[config.serviceKey].runtimeFieldMap.runtimeFoo.type).to.be( + 'keyword' + ); + expect(response.body[config.serviceKey].runtimeFieldMap.runtimeFoo.script.source).to.be( + 'emit(doc["foo"].value)' + ); + }); + }); + + it('can specify optional typeMeta attribute when creating an index pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + typeMeta: {}, + }, + }); + + expect(response.status).to.be(200); + }); + + it('can specify optional fieldFormats attribute when creating an index pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + fieldFormats: { + foo: { + id: 'test-id', + params: {}, + }, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body[config.serviceKey].fieldFormats.foo.id).to.be('test-id'); + expect(response.body[config.serviceKey].fieldFormats.foo.params).to.eql({}); + }); + + it('can specify optional fieldFormats attribute when creating an index pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + fieldAttrs: { + foo: { + count: 123, + customLabel: 'test', + }, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body[config.serviceKey].fieldAttrs.foo.count).to.be(123); + expect(response.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('test'); + }); + + describe('when creating index pattern with existing name', () => { + it('returns error, by default', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + }, + }); + const response2 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + }, + }); + + expect(response1.status).to.be(200); + expect(response2.status).to.be(400); + }); + + it('succeeds, override flag is set', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + timeFieldName: 'foo', + }, + }); + const response2 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + timeFieldName: 'bar', + }, + }); + + expect(response1.status).to.be(200); + expect(response2.status).to.be(200); + + expect(response1.body[config.serviceKey].timeFieldName).to.be('foo'); + expect(response2.body[config.serviceKey].timeFieldName).to.be('bar'); + + expect(response1.body[config.serviceKey].id).to.be( + response1.body[config.serviceKey].id + ); + }); + }); + }); + }); + + // TODO: Removed spaces tests since non-default spaces aren't supported in Serverless + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/create_data_view/validation.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/create_data_view/validation.ts new file mode 100644 index 0000000000000..f4fc964092f65 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/create_data_view/validation.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('validation', () => { + configArray.forEach((config) => { + describe(config.name, () => { + it('returns error when index_pattern object is not provided', async () => { + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + '[request body]: expected a plain object value, but found [null] instead.' + ); + }); + + it('returns error on empty index_pattern object', async () => { + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: {}, + }); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + `[request body.${config.serviceKey}.title]: expected value of type [string] but got [undefined]` + ); + }); + + it('returns error when "override" parameter is not a boolean', async () => { + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: 123, + [config.serviceKey]: { + title: 'foo', + }, + }); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + '[request body.override]: expected value of type [boolean] but got [number]' + ); + }); + + it('returns error when "refresh_fields" parameter is not a boolean', async () => { + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + refresh_fields: 123, + [config.serviceKey]: { + title: 'foo', + }, + }); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + '[request body.refresh_fields]: expected value of type [boolean] but got [number]' + ); + }); + + it('returns an error when unknown runtime field type', async () => { + const title = `basic_index*`; + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'wrong-type', + script: { + source: 'emit(doc["foo"].value)', + }, + }, + }, + }, + }); + + expect(response.status).to.be(400); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/delete_data_view/errors.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/delete_data_view/errors.ts new file mode 100644 index 0000000000000..16681d19c3e45 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/delete_data_view/errors.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('errors', () => { + configArray.forEach((config) => { + describe(config.name, () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .delete(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response.status).to.be(404); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest + .delete(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/delete_data_view/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/delete_data_view/index.ts new file mode 100644 index 0000000000000..42eec1aaa1704 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/delete_data_view/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('delete_index_pattern', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/delete_data_view/main.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/delete_data_view/main.ts new file mode 100644 index 0000000000000..e0fea14780885 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/delete_data_view/main.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { INITIAL_REST_VERSION } from '@kbn/data-views-plugin/server/constants'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('main', () => { + configArray.forEach((config) => { + describe(config.name, () => { + it('deletes an index_pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + }, + }); + + const response2 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response2.status).to.be(200); + + const response3 = await supertest + .delete(`${config.path}/${response1.body[config.serviceKey].id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.status).to.be(200); + + const response4 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response4.status).to.be(404); + }); + }); + + it('returns nothing', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + + .post(config.path) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + }, + }); + + await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + const response2 = await supertest + .delete(`${config.path}/${response1.body[config.serviceKey].id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + // verify empty response + expect(Object.keys(response2.body).length).to.be(0); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_view/errors.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_view/errors.ts new file mode 100644 index 0000000000000..fd69fbaa7e46e --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_view/errors.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('errors', () => { + configArray.forEach((config) => { + describe(config.name, () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .get(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response.status).to.be(404); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest + .get(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_view/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_view/index.ts new file mode 100644 index 0000000000000..63cb32bd2f868 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_view/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('get_index_pattern', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_view/main.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_view/main.ts new file mode 100644 index 0000000000000..d7f3d3b735746 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_view/main.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('main', () => { + configArray.forEach((config) => { + describe(config.name, () => { + it('can retrieve an index_pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + }, + }); + const response2 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response2.body[config.serviceKey].title).to.be(title); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_views/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_views/index.ts new file mode 100644 index 0000000000000..eec88c71b32c6 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_views/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('get_data_views', () => { + loadTestFile(require.resolve('./main')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_views/main.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_views/main.ts new file mode 100644 index 0000000000000..6b716616de8d7 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/get_data_views/main.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { dataViewConfig } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('main', () => { + describe('get data views api', () => { + it('returns list of data views', async () => { + const response = await supertest + .get(dataViewConfig.basePath) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + expect(response.status).to.be(200); + expect(response.body).to.have.property(dataViewConfig.serviceKey); + expect(response.body[dataViewConfig.serviceKey]).to.be.an('array'); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/index.ts new file mode 100644 index 0000000000000..2cf06c123af57 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('index_pattern_crud', () => { + loadTestFile(require.resolve('./create_data_view')); + loadTestFile(require.resolve('./get_data_view')); + loadTestFile(require.resolve('./delete_data_view')); + loadTestFile(require.resolve('./update_data_view')); + loadTestFile(require.resolve('./get_data_views')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/update_data_view/errors.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/update_data_view/errors.ts new file mode 100644 index 0000000000000..3bbe13af43896 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/update_data_view/errors.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('errors', () => { + configArray.forEach((config) => { + describe(config.name, () => { + it('returns error when index_pattern object is not provided', async () => { + const response = await supertest + .post(`${config.path}/foo`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + '[request body]: expected a plain object value, but found [null] instead.' + ); + }); + + it('returns error on non-existing index_pattern', async () => { + const response = await supertest + .post(`${config.path}/non-existing-index-pattern`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: {}, + }); + + expect(response.status).to.be(404); + expect(response.body.statusCode).to.be(404); + expect(response.body.message).to.be( + 'Saved object [index-pattern/non-existing-index-pattern] not found' + ); + }); + + it('returns error when "refresh_fields" parameter is not a boolean', async () => { + const response = await supertest + .post(`${config.path}/foo`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + refresh_fields: 123, + [config.serviceKey]: { + title: 'foo', + }, + }); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + '[request body.refresh_fields]: expected value of type [boolean] but got [number]' + ); + }); + + it('returns success when update patch is empty', async () => { + const title1 = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title: title1, + }, + }); + const id = response.body[config.serviceKey].id; + const response2 = await supertest + .post(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: {}, + }); + + expect(response2.status).to.be(200); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/update_data_view/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/update_data_view/index.ts new file mode 100644 index 0000000000000..9e821943a31e3 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/update_data_view/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('update_index_pattern', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/update_data_view/main.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/update_data_view/main.ts new file mode 100644 index 0000000000000..15e22957a2ca5 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/data_views_crud/update_data_view/main.ts @@ -0,0 +1,445 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('main', () => { + configArray.forEach((config) => { + describe(config.name, () => { + it('can update data view title', async () => { + const title1 = `foo-${Date.now()}-${Math.random()}*`; + const title2 = `bar-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title: title1, + }, + }); + + expect(response1.body[config.serviceKey].title).to.be(title1); + + const id = response1.body[config.serviceKey].id; + const response2 = await supertest + .post(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title: title2, + }, + }); + + expect(response2.body[config.serviceKey].title).to.be(title2); + + const response3 = await supertest + .get(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.body[config.serviceKey].title).to.be(title2); + }); + + it('can update data view name', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const name1 = 'good data view name'; + const name2 = 'better data view name'; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + name: name1, + }, + }); + + expect(response1.body[config.serviceKey].name).to.be(name1); + + const id = response1.body[config.serviceKey].id; + const response2 = await supertest + .post(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + name: name2, + }, + }); + + expect(response2.body[config.serviceKey].name).to.be(name2); + + const response3 = await supertest + .get(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.body[config.serviceKey].name).to.be(name2); + }); + + it('can update data view timeFieldName', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + timeFieldName: 'timeFieldName1', + }, + }); + + expect(response1.body[config.serviceKey].timeFieldName).to.be('timeFieldName1'); + + const id = response1.body[config.serviceKey].id; + const response2 = await supertest + .post(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + timeFieldName: 'timeFieldName2', + }, + }); + + expect(response2.body[config.serviceKey].timeFieldName).to.be('timeFieldName2'); + + const response3 = await supertest + .get(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.body[config.serviceKey].timeFieldName).to.be('timeFieldName2'); + }); + + it('can update data view sourceFilters', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + sourceFilters: [ + { + value: 'foo', + }, + ], + }, + }); + + expect(response1.body[config.serviceKey].sourceFilters).to.eql([ + { + value: 'foo', + }, + ]); + + const id = response1.body[config.serviceKey].id; + const response2 = await supertest + .post(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + sourceFilters: [ + { + value: 'bar', + }, + { + value: 'baz', + }, + ], + }, + }); + + expect(response2.body[config.serviceKey].sourceFilters).to.eql([ + { + value: 'bar', + }, + { + value: 'baz', + }, + ]); + + const response3 = await supertest + .get(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.body[config.serviceKey].sourceFilters).to.eql([ + { + value: 'bar', + }, + { + value: 'baz', + }, + ]); + }); + + it('can update data view fieldFormats', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + fieldFormats: { + foo: { + id: 'foo', + params: { + bar: 'baz', + }, + }, + }, + }, + }); + + expect(response1.body[config.serviceKey].fieldFormats).to.eql({ + foo: { + id: 'foo', + params: { + bar: 'baz', + }, + }, + }); + + const id = response1.body[config.serviceKey].id; + const response2 = await supertest + .post(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + fieldFormats: { + a: { + id: 'a', + params: { + b: 'v', + }, + }, + }, + }, + }); + + expect(response2.body[config.serviceKey].fieldFormats).to.eql({ + a: { + id: 'a', + params: { + b: 'v', + }, + }, + }); + + const response3 = await supertest + .get(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.body[config.serviceKey].fieldFormats).to.eql({ + a: { + id: 'a', + params: { + b: 'v', + }, + }, + }); + }); + + it('can update data view type', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + type: 'foo', + }, + }); + + expect(response1.body[config.serviceKey].type).to.be('foo'); + + const id = response1.body[config.serviceKey].id; + const response2 = await supertest + .post(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + type: 'bar', + }, + }); + + expect(response2.body[config.serviceKey].type).to.be('bar'); + + const response3 = await supertest + .get(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.body[config.serviceKey].type).to.be('bar'); + }); + + it('can update data view typeMeta', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + typeMeta: { foo: 'bar' }, + }, + }); + + expect(response1.body[config.serviceKey].typeMeta).to.eql({ foo: 'bar' }); + + const id = response1.body[config.serviceKey].id; + const response2 = await supertest + .post(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + typeMeta: { foo: 'baz' }, + }, + }); + + expect(response2.body[config.serviceKey].typeMeta).to.eql({ foo: 'baz' }); + + const response3 = await supertest + .get(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.body[config.serviceKey].typeMeta).to.eql({ foo: 'baz' }); + }); + + it('can update multiple data view fields at once', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + timeFieldName: 'timeFieldName1', + typeMeta: { foo: 'bar' }, + }, + }); + + expect(response1.body[config.serviceKey].timeFieldName).to.be('timeFieldName1'); + expect(response1.body[config.serviceKey].typeMeta.foo).to.be('bar'); + + const id = response1.body[config.serviceKey].id; + const response2 = await supertest + .post(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + timeFieldName: 'timeFieldName2', + typeMeta: { baz: 'qux' }, + }, + }); + + expect(response2.body[config.serviceKey].timeFieldName).to.be('timeFieldName2'); + expect(response2.body[config.serviceKey].typeMeta.baz).to.be('qux'); + + const response3 = await supertest + .get(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.body[config.serviceKey].timeFieldName).to.be('timeFieldName2'); + expect(response3.body[config.serviceKey].typeMeta.baz).to.be('qux'); + }); + + it('can update runtime fields', async () => { + const title = `basic_index*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: 'emit(doc["foo"].value)', + }, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body[config.serviceKey].title).to.be(title); + + expect(response1.body[config.serviceKey].runtimeFieldMap.runtimeFoo.type).to.be( + 'keyword' + ); + expect(response1.body[config.serviceKey].runtimeFieldMap.runtimeFoo.script.source).to.be( + 'emit(doc["foo"].value)' + ); + + const id = response1.body[config.serviceKey].id; + const response2 = await supertest + .post(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + runtimeFieldMap: { + runtimeBar: { + type: 'keyword', + script: { + source: 'emit(doc["foo"].value)', + }, + }, + }, + }, + }); + + expect(response2.body[config.serviceKey].runtimeFieldMap.runtimeBar.type).to.be( + 'keyword' + ); + expect(response2.body[config.serviceKey].runtimeFieldMap.runtimeFoo).to.be(undefined); + + const response3 = await supertest + .get(`${config.path}/${id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.body[config.serviceKey].runtimeFieldMap.runtimeBar.type).to.be( + 'keyword' + ); + expect(response3.body[config.serviceKey].runtimeFieldMap.runtimeFoo).to.be(undefined); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/default_index_pattern/default_index_pattern.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/default_index_pattern/default_index_pattern.ts new file mode 100644 index 0000000000000..bdd688f1718aa --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/default_index_pattern/default_index_pattern.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; +import { configArray } from '../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('default index pattern api', () => { + configArray.forEach((config) => { + describe(config.name, () => { + const newId = () => `default-id-${Date.now()}-${Math.random()}`; + it('can set default index pattern', async () => { + const defaultId = newId(); + const defaultPath = `${config.basePath}/default`; + const serviceKeyId = `${config.serviceKey}_id`; + const response1 = await supertest + .post(defaultPath) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [serviceKeyId]: defaultId, + force: true, + }); + expect(response1.status).to.be(200); + expect(response1.body.acknowledged).to.be(true); + + const response2 = await supertest + .get(defaultPath) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + expect(response2.status).to.be(200); + expect(response2.body[serviceKeyId]).to.be(defaultId); + + const response3 = await supertest + .post(defaultPath) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [serviceKeyId]: newId(), + // no force this time, so this new default shouldn't be set + }); + + expect(response3.status).to.be(200); + const response4 = await supertest + .get(defaultPath) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + expect(response4.body[serviceKeyId]).to.be(defaultId); // original default id is used + + const response5 = await supertest + .post(defaultPath) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [serviceKeyId]: null, + force: true, + }); + expect(response5.status).to.be(200); + + const response6 = await supertest + .get(defaultPath) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + expect(response6.body[serviceKeyId]).to.be(null); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/default_index_pattern/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/default_index_pattern/index.ts new file mode 100644 index 0000000000000..94d73e6141b08 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/default_index_pattern/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('default index pattern', () => { + loadTestFile(require.resolve('./default_index_pattern')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/es_errors/errors.js b/x-pack/test_serverless/api_integration/test_suites/common/data_views/es_errors/errors.js new file mode 100644 index 0000000000000..ddbc88cce8045 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/es_errors/errors.js @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { errors as esErrors } from '@elastic/elasticsearch'; +import Boom from '@hapi/boom'; + +import { + isEsIndexNotFoundError, + createNoMatchingIndicesError, + isNoMatchingIndicesError, + convertEsError, +} from '@kbn/data-views-plugin/server/fetcher/lib/errors'; + +import { getIndexNotFoundError, getDocNotFoundError } from './lib'; + +export default function ({ getService }) { + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + describe('index_patterns/* error handler', () => { + let indexNotFoundError; + let docNotFoundError; + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + indexNotFoundError = await getIndexNotFoundError(es); + docNotFoundError = await getDocNotFoundError(es); + }); + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + describe('isEsIndexNotFoundError()', () => { + it('identifies index not found errors', () => { + if (!isEsIndexNotFoundError(indexNotFoundError)) { + throw new Error(`Expected isEsIndexNotFoundError(indexNotFoundError) to be true`); + } + }); + + it('rejects doc not found errors', () => { + if (isEsIndexNotFoundError(docNotFoundError)) { + throw new Error(`Expected isEsIndexNotFoundError(docNotFoundError) to be true`); + } + }); + }); + + describe('createNoMatchingIndicesError()', () => { + it('returns a boom error', () => { + const error = createNoMatchingIndicesError(); + if (!error || !error.isBoom) { + throw new Error(`expected ${error} to be a Boom error`); + } + }); + + it('sets output code to "no_matching_indices"', () => { + const error = createNoMatchingIndicesError(); + expect(error.output.payload).to.have.property('code', 'no_matching_indices'); + }); + }); + + describe('isNoMatchingIndicesError()', () => { + it('returns true for errors from createNoMatchingIndicesError()', () => { + if (!isNoMatchingIndicesError(createNoMatchingIndicesError())) { + throw new Error( + 'Expected isNoMatchingIndicesError(createNoMatchingIndicesError()) to be true' + ); + } + }); + + it('returns false for indexNotFoundError', () => { + if (isNoMatchingIndicesError(indexNotFoundError)) { + throw new Error('expected isNoMatchingIndicesError(indexNotFoundError) to be false'); + } + }); + + it('returns false for docNotFoundError', async () => { + if (isNoMatchingIndicesError(docNotFoundError)) { + throw new Error('expected isNoMatchingIndicesError(docNotFoundError) to be false'); + } + }); + }); + + describe('convertEsError()', () => { + const indices = ['foo', 'bar']; + + it('converts indexNotFoundErrors into NoMatchingIndices errors', async () => { + const converted = convertEsError(indices, indexNotFoundError); + if (!isNoMatchingIndicesError(converted)) { + throw new Error( + 'expected convertEsError(indexNotFoundError) to return NoMatchingIndices error' + ); + } + }); + + it('wraps other errors in Boom', async () => { + const error = new esErrors.ResponseError({ + body: { + root_cause: [ + { + type: 'security_exception', + reason: 'action [indices:data/read/field_caps] is unauthorized for user [standard]', + }, + ], + type: 'security_exception', + reason: 'action [indices:data/read/field_caps] is unauthorized for user [standard]', + }, + statusCode: 403, + }); + + expect(error).to.not.have.property('isBoom'); + const converted = convertEsError(indices, error); + expect(converted).to.have.property('isBoom'); + expect(converted.output.statusCode).to.be(403); + }); + + it('handles errors that are already Boom errors', () => { + const error = new Error(); + error.statusCode = 401; + const boomError = Boom.boomify(error, { statusCode: error.statusCode }); + + const converted = convertEsError(indices, boomError); + + expect(converted.output.statusCode).to.be(401); + }); + + it('preserves headers from Boom errors', () => { + const error = new Error(); + error.statusCode = 401; + const boomError = Boom.boomify(error, { statusCode: error.statusCode }); + const wwwAuthenticate = 'Basic realm="Authorization Required"'; + boomError.output.headers['WWW-Authenticate'] = wwwAuthenticate; + const converted = convertEsError(indices, boomError); + + expect(converted.output.headers['WWW-Authenticate']).to.be(wwwAuthenticate); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/es_errors/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/es_errors/index.ts new file mode 100644 index 0000000000000..3fc4c06262100 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/es_errors/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('index_patterns/service/lib', () => { + loadTestFile(require.resolve('./errors')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/es_errors/lib/get_es_errors.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/es_errors/lib/get_es_errors.ts new file mode 100644 index 0000000000000..9821cbd73c142 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/es_errors/lib/get_es_errors.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { Client } from '@elastic/elasticsearch'; + +export async function getIndexNotFoundError(es: Client) { + try { + await es.indices.get({ + index: 'SHOULD NOT EXIST', + }); + } catch (err) { + expect(err).to.have.property('statusCode', 404); // basic check + return err; + } + + throw new Error('Expected es.indices.get() call to fail'); +} + +export async function getDocNotFoundError(es: Client) { + try { + await es.get({ + index: 'basic_index', + id: '1234', + }); + } catch (err) { + expect(err).to.have.property('statusCode', 404); // basic check + return err; + } + + throw new Error('Expected es.get() call to fail'); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/es_errors/lib/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/es_errors/lib/index.ts new file mode 100644 index 0000000000000..b188b824512ef --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/es_errors/lib/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getIndexNotFoundError, getDocNotFoundError } from './get_es_errors'; diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_api/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_api/index.ts new file mode 100644 index 0000000000000..0fcf5454e7eba --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_api/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('fields_api', () => { + loadTestFile(require.resolve('./update_fields')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_api/update_fields/errors.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_api/update_fields/errors.ts new file mode 100644 index 0000000000000..2e8e8556b17e9 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_api/update_fields/errors.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('errors', () => { + configArray.forEach((config) => { + describe(config.name, () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .post(`${config.path}/${id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + foo: {}, + }, + }); + + expect(response.status).to.be(404); + }); + + it('returns error when "fields" payload attribute is invalid', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + }, + }); + const response2 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: 123, + }); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be( + '[request body.fields]: expected value of type [object] but got [number]' + ); + }); + + it('returns error if not changes are specified', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + }, + }); + + const response2 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + foo: {}, + bar: {}, + baz: {}, + }, + }); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be('Change set is empty.'); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_api/update_fields/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_api/update_fields/index.ts new file mode 100644 index 0000000000000..a716524e8ba22 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_api/update_fields/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('update_fields', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_api/update_fields/main.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_api/update_fields/main.ts new file mode 100644 index 0000000000000..2e72648900747 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_api/update_fields/main.ts @@ -0,0 +1,523 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const svlCommonApi = getService('svlCommonApi'); + + describe('main', () => { + const basicIndex = 'ba*ic_index'; + let indexPattern: any; + + configArray.forEach((config) => { + describe(config.name, () => { + before(async () => { + await esArchiver.load( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + + indexPattern = ( + await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title: basicIndex, + }, + }) + ).body[config.serviceKey]; + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + + if (indexPattern) { + await supertest + .delete(`${config.path}/${indexPattern.id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + } + }); + + it('can update multiple fields', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body[config.serviceKey].fieldAttrs.foo).to.be(undefined); + expect(response1.body[config.serviceKey].fieldAttrs.bar).to.be(undefined); + + const response2 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + foo: { + count: 123, + customLabel: 'test', + }, + bar: { + count: 456, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey].fieldAttrs.foo.count).to.be(123); + expect(response2.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('test'); + expect(response2.body[config.serviceKey].fieldAttrs.bar.count).to.be(456); + + const response3 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.status).to.be(200); + expect(response3.body[config.serviceKey].fieldAttrs.foo.count).to.be(123); + expect(response3.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('test'); + expect(response3.body[config.serviceKey].fieldAttrs.bar.count).to.be(456); + }); + + describe('count', () => { + it('can set field "count" attribute on non-existing field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body[config.serviceKey].fieldAttrs.foo).to.be(undefined); + + const response2 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + foo: { + count: 123, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey].fieldAttrs.foo.count).to.be(123); + + const response3 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.status).to.be(200); + expect(response3.body[config.serviceKey].fieldAttrs.foo.count).to.be(123); + }); + + it('can update "count" attribute in index_pattern attribute map', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + fieldAttrs: { + foo: { + count: 1, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body[config.serviceKey].fieldAttrs.foo.count).to.be(1); + + const response2 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + foo: { + count: 2, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey].fieldAttrs.foo.count).to.be(2); + + const response3 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.status).to.be(200); + expect(response3.body[config.serviceKey].fieldAttrs.foo.count).to.be(2); + }); + + it('can delete "count" attribute from index_pattern attribute map', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + fieldAttrs: { + foo: { + count: 1, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body[config.serviceKey].fieldAttrs.foo.count).to.be(1); + + const response2 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + foo: { + count: null, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey].fieldAttrs.foo.count).to.be(undefined); + + const response3 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.status).to.be(200); + expect(response3.body[config.serviceKey].fieldAttrs.foo.count).to.be(undefined); + }); + }); + + describe('customLabel', () => { + it('can set field "customLabel" attribute on non-existing field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body[config.serviceKey].fieldAttrs.foo).to.be(undefined); + + const response2 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + foo: { + customLabel: 'foo', + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('foo'); + + const response3 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.status).to.be(200); + expect(response3.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('foo'); + }); + + it('can update "customLabel" attribute in index_pattern attribute map', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + fieldAttrs: { + foo: { + customLabel: 'foo', + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('foo'); + + const response2 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + foo: { + customLabel: 'bar', + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('bar'); + + const response3 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.status).to.be(200); + expect(response3.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('bar'); + }); + + it('can delete "customLabel" attribute from index_pattern attribute map', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + fieldAttrs: { + foo: { + customLabel: 'foo', + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be('foo'); + + const response2 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + foo: { + customLabel: null, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be(undefined); + + const response3 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.status).to.be(200); + expect(response3.body[config.serviceKey].fieldAttrs.foo.customLabel).to.be(undefined); + }); + + it('can set field "customLabel" attribute on an existing field', async () => { + await supertest + .post(`${config.path}/${indexPattern.id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + foo: { + customLabel: 'baz', + }, + }, + }); + + const response1 = await supertest + .get(`${config.path}/${indexPattern.id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response1.status).to.be(200); + expect(response1.body[config.serviceKey].fields.foo.customLabel).to.be('baz'); + }); + }); + + describe('format', () => { + it('can set field "format" attribute on non-existing field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body[config.serviceKey].fieldFormats.foo).to.be(undefined); + + const response2 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + foo: { + format: { + id: 'bar', + params: { baz: 'qux' }, + }, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey].fieldFormats.foo).to.eql({ + id: 'bar', + params: { baz: 'qux' }, + }); + + const response3 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.status).to.be(200); + expect(response3.body[config.serviceKey].fieldFormats.foo).to.eql({ + id: 'bar', + params: { baz: 'qux' }, + }); + }); + + it('can update "format" attribute in index_pattern format map', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + fieldFormats: { + foo: { + id: 'bar', + params: { + baz: 'qux', + }, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body[config.serviceKey].fieldFormats.foo).to.eql({ + id: 'bar', + params: { + baz: 'qux', + }, + }); + + const response2 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + foo: { + format: { + id: 'bar-2', + params: { baz: 'qux-2' }, + }, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey].fieldFormats.foo).to.eql({ + id: 'bar-2', + params: { baz: 'qux-2' }, + }); + + const response3 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.status).to.be(200); + expect(response3.body[config.serviceKey].fieldFormats.foo).to.eql({ + id: 'bar-2', + params: { baz: 'qux-2' }, + }); + }); + + it('can remove "format" attribute from index_pattern format map', async () => { + const response2 = await supertest + .post(`${config.path}/${indexPattern.id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + foo: { + format: null, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey].fieldFormats.foo).to.be(undefined); + + const response3 = await supertest + .get(`${config.path}/${indexPattern.id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.status).to.be(200); + expect(response3.body[config.serviceKey].fieldFormats.foo).to.be(undefined); + }); + + // TODO: Scripted fields code dropped since they are not supported in Serverless + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/conflicts.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/conflicts.ts new file mode 100644 index 0000000000000..d1d08c1e88832 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/conflicts.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { INITIAL_REST_VERSION_INTERNAL } from '@kbn/data-views-plugin/server/constants'; +import { FIELDS_FOR_WILDCARD_PATH } from '@kbn/data-views-plugin/common/constants'; +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const svlCommonApi = getService('svlCommonApi'); + + describe('conflicts', () => { + before(() => + esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/conflicts') + ); + after(() => + esArchiver.unload('test/api_integration/fixtures/es_archiver/index_patterns/conflicts') + ); + + it('flags fields with mismatched types as conflicting', () => + supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ pattern: 'logs-*' }) + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ + fields: [ + { + name: '@timestamp', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + readFromDocValues: true, + metadata_field: false, + }, + { + name: 'number_conflict', + type: 'number', + esTypes: ['float', 'integer'], + aggregatable: true, + searchable: true, + readFromDocValues: true, + metadata_field: false, + }, + { + name: 'string_conflict', + type: 'string', + esTypes: ['keyword', 'text'], + aggregatable: true, + searchable: true, + readFromDocValues: true, + metadata_field: false, + }, + { + name: 'success', + type: 'conflict', + esTypes: ['keyword', 'boolean'], + aggregatable: true, + searchable: true, + readFromDocValues: false, + conflictDescriptions: { + boolean: ['logs-2017.01.02'], + keyword: ['logs-2017.01.01'], + }, + metadata_field: false, + }, + ], + indices: ['logs-2017.01.01', 'logs-2017.01.02'], + }); + })); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/filter.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/filter.ts new file mode 100644 index 0000000000000..ff2ad5e5d00a1 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/filter.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { INITIAL_REST_VERSION_INTERNAL } from '@kbn/data-views-plugin/server/constants'; +import { FIELDS_FOR_WILDCARD_PATH } from '@kbn/data-views-plugin/common/constants'; +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const svlCommonApi = getService('svlCommonApi'); + + describe('filter fields', () => { + before(async () => { + await es.index({ + index: 'helloworld1', + refresh: true, + id: 'helloworld', + body: { hello: 'world' }, + }); + + await es.index({ + index: 'helloworld2', + refresh: true, + id: 'helloworld2', + body: { bye: 'world' }, + }); + }); + + it('can filter', async () => { + const a = await supertest + .put(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ pattern: 'helloworld*' }) + .send({ index_filter: { exists: { field: 'bye' } } }); + + const fieldNames = a.body.fields.map((fld: { name: string }) => fld.name); + + expect(fieldNames.indexOf('bye') > -1).to.be(true); + expect(fieldNames.indexOf('hello') === -1).to.be(true); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/index.ts new file mode 100644 index 0000000000000..a64ddadf2f7a2 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('index_patterns/_fields_for_wildcard route', () => { + loadTestFile(require.resolve('./params')); + loadTestFile(require.resolve('./conflicts')); + loadTestFile(require.resolve('./response')); + loadTestFile(require.resolve('./filter')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/params.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/params.ts new file mode 100644 index 0000000000000..035ca1af4b5a8 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/params.ts @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { INITIAL_REST_VERSION_INTERNAL } from '@kbn/data-views-plugin/server/constants'; +import { FIELDS_FOR_WILDCARD_PATH } from '@kbn/data-views-plugin/common/constants'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + const randomness = getService('randomness'); + const svlCommonApi = getService('svlCommonApi'); + + describe('params', () => { + before(() => + esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index') + ); + after(() => + esArchiver.unload('test/api_integration/fixtures/es_archiver/index_patterns/basic_index') + ); + + it('requires a pattern query param', () => + supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({}) + .expect(400)); + + it('accepts include_unmapped param', () => + supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ + pattern: '*', + include_unmapped: true, + }) + .expect(200)); + + it('rejects unexpected query params', () => + supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ + pattern: randomness.word(), + [randomness.word()]: randomness.word(), + }) + .expect(400)); + + describe('fields', () => { + it('accepts a JSON formatted fields query param', () => + supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ + pattern: '*', + fields: JSON.stringify(['baz']), + }) + .expect(200)); + + it('accepts meta_fields query param in string array', () => + supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ + pattern: '*', + fields: ['baz', 'foo'], + }) + .expect(200)); + + it('accepts single array fields query param', () => + supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ + pattern: '*', + fields: ['baz'], + }) + .expect(200)); + + it('accepts single fields query param', () => + supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ + pattern: '*', + fields: 'baz', + }) + .expect(200)); + + it('rejects a comma-separated list of fields', () => + supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ + pattern: '*', + fields: 'foo,bar', + }) + .expect(400)); + }); + + describe('meta_fields', () => { + it('accepts a JSON formatted meta_fields query param', () => + supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ + pattern: '*', + meta_fields: JSON.stringify(['meta']), + }) + .expect(200)); + + it('accepts meta_fields query param in string array', () => + supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ + pattern: '*', + meta_fields: ['_id', 'meta'], + }) + .expect(200)); + + it('accepts single meta_fields query param', () => + supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ + pattern: '*', + meta_fields: ['_id'], + }) + .expect(200)); + + it('rejects a comma-separated list of meta_fields', () => + supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ + pattern: '*', + meta_fields: 'foo,bar', + }) + .expect(400)); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/response.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/response.ts new file mode 100644 index 0000000000000..a073dac456b7f --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/fields_for_wildcard_route/response.ts @@ -0,0 +1,243 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { INITIAL_REST_VERSION_INTERNAL } from '@kbn/data-views-plugin/server/constants'; +import { FIELDS_FOR_WILDCARD_PATH } from '@kbn/data-views-plugin/common/constants'; +import expect from '@kbn/expect'; +import { sortBy } from 'lodash'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + const ensureFieldsAreSorted = (resp: { body: { fields: { name: string } } }) => { + expect(resp.body.fields).to.eql(sortBy(resp.body.fields, 'name')); + }; + + const testFields = [ + { + type: 'boolean', + esTypes: ['boolean'], + searchable: true, + aggregatable: true, + name: 'bar', + readFromDocValues: true, + metadata_field: false, + }, + { + type: 'string', + esTypes: ['text'], + searchable: true, + aggregatable: false, + name: 'baz', + readFromDocValues: false, + metadata_field: false, + }, + { + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: true, + name: 'baz.keyword', + readFromDocValues: true, + subType: { multi: { parent: 'baz' } }, + metadata_field: false, + }, + { + type: 'number', + esTypes: ['long'], + searchable: true, + aggregatable: true, + name: 'foo', + readFromDocValues: true, + metadata_field: false, + }, + { + aggregatable: true, + esTypes: ['keyword'], + name: 'nestedField.child', + readFromDocValues: true, + searchable: true, + subType: { + nested: { + path: 'nestedField', + }, + }, + type: 'string', + metadata_field: false, + }, + ]; + + describe('fields_for_wildcard_route response', () => { + before(() => + esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index') + ); + after(() => + esArchiver.unload('test/api_integration/fixtures/es_archiver/index_patterns/basic_index') + ); + + it('returns a flattened version of the fields in es', async () => { + await supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ pattern: 'basic_index' }) + .expect(200, { + fields: testFields, + indices: ['basic_index'], + }) + .then(ensureFieldsAreSorted); + }); + + it('returns a single field as requested', async () => { + await supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ pattern: 'basic_index', fields: JSON.stringify(['bar']) }) + .expect(200, { + fields: [testFields[0]], + indices: ['basic_index'], + }); + }); + + it('always returns a field for all passed meta fields', async () => { + await supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ + pattern: 'basic_index', + meta_fields: JSON.stringify(['_id', '_source', 'crazy_meta_field']), + }) + .expect(200, { + fields: [ + { + aggregatable: false, + name: '_id', + esTypes: ['_id'], + readFromDocValues: false, + searchable: true, + type: 'string', + metadata_field: true, + }, + { + aggregatable: false, + name: '_source', + esTypes: ['_source'], + readFromDocValues: false, + searchable: false, + type: '_source', + metadata_field: true, + }, + { + type: 'boolean', + esTypes: ['boolean'], + searchable: true, + aggregatable: true, + name: 'bar', + readFromDocValues: true, + metadata_field: false, + }, + { + aggregatable: false, + name: 'baz', + esTypes: ['text'], + readFromDocValues: false, + searchable: true, + type: 'string', + metadata_field: false, + }, + { + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: true, + name: 'baz.keyword', + readFromDocValues: true, + subType: { multi: { parent: 'baz' } }, + metadata_field: false, + }, + { + aggregatable: false, + name: 'crazy_meta_field', + readFromDocValues: false, + searchable: false, + type: 'string', + metadata_field: true, + }, + { + type: 'number', + esTypes: ['long'], + searchable: true, + aggregatable: true, + name: 'foo', + readFromDocValues: true, + metadata_field: false, + }, + { + aggregatable: true, + esTypes: ['keyword'], + name: 'nestedField.child', + readFromDocValues: true, + searchable: true, + subType: { + nested: { + path: 'nestedField', + }, + }, + type: 'string', + metadata_field: false, + }, + ], + indices: ['basic_index'], + }) + .then(ensureFieldsAreSorted); + }); + + it('returns fields when one pattern exists and the other does not', async () => { + await supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ pattern: 'bad_index,basic_index' }) + .expect(200, { + fields: testFields, + indices: ['basic_index'], + }); + }); + + it('returns 404 when neither exists', async () => { + await supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ pattern: 'bad_index,bad_index_2' }) + .expect(404); + }); + + it('returns 404 when no patterns exist', async () => { + await supertest + .get(FIELDS_FOR_WILDCARD_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .query({ + pattern: 'bad_index', + }) + .expect(404); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/has_user_index_pattern/has_user_index_pattern.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/has_user_index_pattern/has_user_index_pattern.ts new file mode 100644 index 0000000000000..19f4d82d79a8e --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/has_user_index_pattern/has_user_index_pattern.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { + INITIAL_REST_VERSION, + INITIAL_REST_VERSION_INTERNAL, +} from '@kbn/data-views-plugin/server/constants'; +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; +import { configArray } from '../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + const svlCommonApi = getService('svlCommonApi'); + const kibanaServer = getService('kibanaServer'); + + describe('has user index pattern API', () => { + configArray.forEach((config) => { + describe(config.name, () => { + beforeEach(async () => { + // TODO: emptyKibanaIndex fails in Serverless with + // "index_not_found_exception: no such index [.kibana_ingest]", + // so it was switched to `savedObjects.cleanStandardList()` + await kibanaServer.savedObjects.cleanStandardList(); + if (await es.indices.exists({ index: 'metrics-test' })) { + await es.indices.delete({ index: 'metrics-test' }); + } + + if (await es.indices.exists({ index: 'logs-test' })) { + await es.indices.delete({ index: 'logs-test' }); + } + }); + + const servicePath = `${config.basePath}/has_user_${config.serviceKey}`; + + it('should return false if no index patterns', async () => { + // Make sure all saved objects including data views are cleared + // TODO: emptyKibanaIndex fails in Serverless with + // "index_not_found_exception: no such index [.kibana_ingest]", + // so it was switched to `savedObjects.cleanStandardList()` + await kibanaServer.savedObjects.cleanStandardList(); + const response = await supertest + .get(servicePath) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + expect(response.status).to.be(200); + expect(response.body.result).to.be(false); + }); + + it('should return true if has index pattern with user data', async () => { + await esArchiver.load( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + await supertest + .post(config.path) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title: 'basic_index', + }, + }); + + const response = await supertest + .get(servicePath) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + expect(response.status).to.be(200); + expect(response.body.result).to.be(true); + + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('should return true if has user index pattern without data', async () => { + await supertest + .post(config.path) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title: 'basic_index', + allowNoIndex: true, + }, + }); + + const response = await supertest + .get(servicePath) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + expect(response.status).to.be(200); + expect(response.body.result).to.be(true); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/has_user_index_pattern/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/has_user_index_pattern/index.ts new file mode 100644 index 0000000000000..4ec3661feffde --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/has_user_index_pattern/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('has user index pattern', () => { + loadTestFile(require.resolve('./has_user_index_pattern')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/index.ts new file mode 100644 index 0000000000000..eb25b2530a5e7 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('index_patterns', () => { + loadTestFile(require.resolve('./es_errors')); + loadTestFile(require.resolve('./fields_for_wildcard_route')); + loadTestFile(require.resolve('./data_views_crud')); + // TODO: Removed `scripted_fields_crud` since + // scripted fields are not supported in Serverless + loadTestFile(require.resolve('./fields_api')); + loadTestFile(require.resolve('./default_index_pattern')); + loadTestFile(require.resolve('./runtime_fields_crud')); + loadTestFile(require.resolve('./integration')); + // TODO: Removed `deprecations` since + // scripted fields are not supported in Serverless + loadTestFile(require.resolve('./has_user_index_pattern')); + loadTestFile(require.resolve('./swap_references')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/integration/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/integration/index.ts new file mode 100644 index 0000000000000..55a9585ee7f1f --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/integration/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +/** + * Test usage of different index patterns APIs in combination + */ +export default function ({ loadTestFile }: FtrProviderContext) { + describe('integration', () => { + loadTestFile(require.resolve('./integration')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/integration/integration.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/integration/integration.ts new file mode 100644 index 0000000000000..8b9c215754f12 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/integration/integration.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import _ from 'lodash'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +/** + * Test usage of different index patterns APIs in combination + */ +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const svlCommonApi = getService('svlCommonApi'); + + describe('integration', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('create an index pattern, add a runtime field, add a field formatter, then re-create the same index pattern', async () => { + const title = `basic_index*`; + const response1 = await supertest + .post('/api/index_patterns/index_pattern') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + index_pattern: { + title, + }, + }); + const id = response1.body.index_pattern.id; + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${id}/runtime_field`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response2.status).to.be(200); + + const response3 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + runtimeBar: { + count: 123, + customLabel: 'test', + }, + }, + }); + + expect(response3.status).to.be(200); + + const response4 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fields: { + runtimeBar: { + format: { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'humanizePrecise' }, + }, + }, + }, + }); + + expect(response4.status).to.be(200); + + const response5 = await supertest + .get('/api/index_patterns/index_pattern/' + response1.body.index_pattern.id) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response5.status).to.be(200); + + const resultIndexPattern = response5.body.index_pattern; + + const runtimeField = resultIndexPattern.fields.runtimeBar; + expect(runtimeField.name).to.be('runtimeBar'); + expect(runtimeField.runtimeField.type).to.be('long'); + expect(runtimeField.runtimeField.script.source).to.be("emit(doc['field_name'].value)"); + expect(runtimeField.scripted).to.be(false); + + expect(resultIndexPattern.fieldFormats.runtimeBar.id).to.be('duration'); + expect(resultIndexPattern.fieldFormats.runtimeBar.params.inputFormat).to.be('milliseconds'); + expect(resultIndexPattern.fieldFormats.runtimeBar.params.outputFormat).to.be( + 'humanizePrecise' + ); + + expect(resultIndexPattern.fieldAttrs.runtimeBar.count).to.be(123); + expect(resultIndexPattern.fieldAttrs.runtimeBar.customLabel).to.be('test'); + + // check that retrieved object is transient and a clone can be created + const response6 = await supertest + .post('/api/index_patterns/index_pattern') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + index_pattern: resultIndexPattern, + }); + + expect(response6.status).to.be(200); + const recreatedIndexPattern = response6.body.index_pattern; + + expect(_.omit(recreatedIndexPattern, 'version', 'namespaces')).to.eql( + _.omit(resultIndexPattern, 'version', 'namespaces') + ); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/create_runtime_field/errors.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/create_runtime_field/errors.ts new file mode 100644 index 0000000000000..6fa86503eb75a --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/create_runtime_field/errors.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('errors', () => { + configArray.forEach((config) => { + describe(config.name, () => { + it('returns an error field object is not provided', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title, + }, + }); + const id = response1.body[config.serviceKey].id; + const response2 = await supertest + .post(`${config.path}/${id}/runtime_field`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({}); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be( + '[request body.name]: expected value of type [string] but got [undefined]' + ); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/create_runtime_field/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/create_runtime_field/index.ts new file mode 100644 index 0000000000000..226a2026ef945 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/create_runtime_field/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('create_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/create_runtime_field/main.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/create_runtime_field/main.ts new file mode 100644 index 0000000000000..9b01977f2fdc5 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/create_runtime_field/main.ts @@ -0,0 +1,229 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const svlCommonApi = getService('svlCommonApi'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + configArray.forEach((config) => { + describe(config.name, () => { + it('can create a new runtime field', async () => { + const title = `basic_index*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + }, + }); + const id = response1.body[config.serviceKey].id; + const response2 = await supertest + .post(`${config.path}/${id}/runtime_field`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey]).to.not.be.empty(); + + const field = + config.serviceKey === 'index_pattern' ? response2.body.field : response2.body.fields[0]; + + expect(field.name).to.be('runtimeBar'); + expect(field.runtimeField.type).to.be('long'); + expect(field.runtimeField.script.source).to.be("emit(doc['field_name'].value)"); + expect(field.scripted).to.be(false); + }); + + it('newly created runtime field is available in the index_pattern object', async () => { + const title = `basic_index`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + }, + }); + + await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + const response2 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey]).to.not.be.empty(); + + const field = response2.body[config.serviceKey].fields.runtimeBar; + + expect(field.name).to.be('runtimeBar'); + expect(field.runtimeField.type).to.be('long'); + expect(field.runtimeField.script.source).to.be("emit(doc['field_name'].value)"); + expect(field.scripted).to.be(false); + + const response3 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response3.status).to.be(400); + + await supertest + .delete(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + }); + + it('prevents field name collisions', async () => { + const title = `basic*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + }, + }); + + const response2 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response2.status).to.be(200); + + const response3 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response3.status).to.be(400); + + const response4 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'runtimeComposite', + runtimeField: { + type: 'composite', + script: { + source: 'emit("a","a"); emit("b","b")', + }, + fields: { + a: { + type: 'keyword', + }, + b: { + type: 'keyword', + }, + }, + }, + }); + + expect(response4.status).to.be(200); + + const response5 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'runtimeComposite', + runtimeField: { + type: 'composite', + script: { + source: 'emit("a","a"); emit("b","b")', + }, + fields: { + a: { + type: 'keyword', + }, + b: { + type: 'keyword', + }, + }, + }, + }); + + expect(response5.status).to.be(400); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/delete_runtime_field/errors.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/delete_runtime_field/errors.ts new file mode 100644 index 0000000000000..6cdd3c1e042d4 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/delete_runtime_field/errors.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('errors', () => { + const basicIndex = 'b*sic_index'; + let indexPattern: any; + + configArray.forEach((config) => { + describe(config.name, () => { + before(async () => { + await esArchiver.load( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + + indexPattern = ( + await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title: basicIndex, + }, + }) + ).body[config.serviceKey]; + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + + if (indexPattern) { + await supertest + .delete(`${config.path}/${indexPattern.id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + } + }); + + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .delete(`${config.path}/${id}/runtime_field/foo`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response.status).to.be(404); + }); + + it('returns 404 error on non-existing runtime field', async () => { + const response1 = await supertest + .delete(`${config.path}/${indexPattern.id}/runtime_field/test`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response1.status).to.be(404); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest + .delete(`${config.path}/${id}/runtime_field/foo`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/delete_runtime_field/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/delete_runtime_field/index.ts new file mode 100644 index 0000000000000..434c986417696 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/delete_runtime_field/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('delete_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/delete_runtime_field/main.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/delete_runtime_field/main.ts new file mode 100644 index 0000000000000..a93268adde3b5 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/delete_runtime_field/main.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const svlCommonApi = getService('svlCommonApi'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + configArray.forEach((config) => { + describe(config.name, () => { + it('can delete a runtime field', async () => { + const title = `basic_index*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + runtimeFieldMap: { + runtimeBar: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }, + }, + }); + + const response2 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(typeof response2.body[config.serviceKey].fields.runtimeBar).to.be('object'); + + const response3 = await supertest + .delete( + `${config.path}/${response1.body[config.serviceKey].id}/runtime_field/runtimeBar` + ) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response3.status).to.be(200); + + const response4 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(typeof response4.body[config.serviceKey].fields.runtimeBar).to.be('undefined'); + await supertest + .delete(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/get_runtime_field/errors.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/get_runtime_field/errors.ts new file mode 100644 index 0000000000000..19d329fe0f410 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/get_runtime_field/errors.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('errors', () => { + const basicIndex = '*asic_index'; + let indexPattern: any; + + configArray.forEach((config) => { + describe(config.name, () => { + before(async () => { + await esArchiver.load( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + + indexPattern = ( + await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + [config.serviceKey]: { + title: basicIndex, + }, + }) + ).body[config.serviceKey]; + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + + if (indexPattern) { + await supertest + .delete(`${config.path}/${indexPattern.id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + } + }); + + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .get(`${config.path}/${id}/runtime_field/foo`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response.status).to.be(404); + }); + + it('returns 404 error on non-existing runtime field', async () => { + const response1 = await supertest + .get(`${config.path}/${indexPattern.id}/runtime_field/sf`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response1.status).to.be(404); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest + .get(`${config.path}/${id}/runtime_field/foo`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/get_runtime_field/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/get_runtime_field/index.ts new file mode 100644 index 0000000000000..7f22ba407e651 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/get_runtime_field/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('get_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/get_runtime_field/main.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/get_runtime_field/main.ts new file mode 100644 index 0000000000000..638c4f99bd509 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/get_runtime_field/main.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const svlCommonApi = getService('svlCommonApi'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + configArray.forEach((config) => { + describe(config.name, () => { + it('can fetch a runtime field', async () => { + const title = `basic_index*`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + runtimeBar: { + type: 'keyword', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + + const response2 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field/runtimeFoo`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + const field = + config.serviceKey === 'index_pattern' ? response2.body.field : response2.body.fields[0]; + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey]).to.not.be.empty(); + expect(typeof field).to.be('object'); + expect(field.name).to.be('runtimeFoo'); + expect(field.type).to.be('string'); + expect(field.scripted).to.be(false); + expect(field.runtimeField.script.source).to.be("emit(doc['field_name'].value)"); + await supertest + .delete(`${config.path}/${response1.body[config.serviceKey].id}`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/index.ts new file mode 100644 index 0000000000000..5b95e3b39dcf4 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('runtime_fields_crud', () => { + loadTestFile(require.resolve('./create_runtime_field')); + loadTestFile(require.resolve('./get_runtime_field')); + loadTestFile(require.resolve('./delete_runtime_field')); + loadTestFile(require.resolve('./put_runtime_field')); + loadTestFile(require.resolve('./update_runtime_field')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/put_runtime_field/errors.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/put_runtime_field/errors.ts new file mode 100644 index 0000000000000..1141ca5db1ec0 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/put_runtime_field/errors.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const svlCommonApi = getService('svlCommonApi'); + + describe('errors', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + configArray.forEach((config) => { + describe(config.name, () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .put(`${config.path}/${id}/runtime_field`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response.status).to.be(404); + }); + + it('returns error on non-runtime field update attempt', async () => { + const title = `basic_index`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + }, + }); + + const response2 = await supertest + .put(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'bar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response2.status).to.be(400); + expect(response2.body.message).to.be('Only runtime fields can be updated'); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/put_runtime_field/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/put_runtime_field/index.ts new file mode 100644 index 0000000000000..85f3c93570a06 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/put_runtime_field/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('put_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/put_runtime_field/main.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/put_runtime_field/main.ts new file mode 100644 index 0000000000000..01cf160208898 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/put_runtime_field/main.ts @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const svlCommonApi = getService('svlCommonApi'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + configArray.forEach((config) => { + describe(config.name, () => { + it('can overwrite an existing field', async () => { + const title = `basic_index`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + runtimeBar: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + }, + }, + }); + + const response2 = await supertest + .put(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'runtimeFoo', + runtimeField: { + type: 'long', + script: { + source: "doc['field_name'].value", + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey]).to.not.be.empty(); + + const response3 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field/runtimeFoo`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + const field3 = + config.serviceKey === 'index_pattern' ? response3.body.field : response3.body.fields[0]; + + expect(response3.status).to.be(200); + expect(field3.type).to.be('number'); + + const response4 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field/runtimeBar`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + const field4 = + config.serviceKey === 'index_pattern' ? response4.body.field : response4.body.fields[0]; + + expect(response4.status).to.be(200); + expect(field4.type).to.be('string'); + }); + + it('can add a new runtime field', async () => { + const title = `basic_index`; + const response1 = await supertest + .post(config.path) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + }, + }, + }); + + await supertest + .put(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "doc['field_name'].value", + }, + }, + }); + + const response2 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field/runtimeBar`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + const field = + config.serviceKey === 'index_pattern' ? response2.body.field : response2.body.fields[0]; + + expect(response2.status).to.be(200); + expect(response2.body[config.serviceKey]).to.not.be.empty(); + expect(typeof field.runtimeField).to.be('object'); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/update_runtime_field/errors.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/update_runtime_field/errors.ts new file mode 100644 index 0000000000000..17e4711fdb9aa --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/update_runtime_field/errors.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('errors', () => { + configArray.forEach((config) => { + describe(config.name, () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .post(`${config.path}/${id}/runtime_field/foo`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + runtimeField: { + type: 'keyword', + script: { + source: "doc['something_new'].value", + }, + }, + }); + + expect(response.status).to.be(404); + }); + + it('returns error when field name is specified', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .post(`${config.path}/${id}/runtime_field/foo`) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'foo', + runtimeField: { + type: 'keyword', + script: { + source: "doc['something_new'].value", + }, + }, + }); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + "[request body.name]: a value wasn't expected to be present" + ); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/update_runtime_field/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/update_runtime_field/index.ts new file mode 100644 index 0000000000000..d60fcf7f047a1 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/update_runtime_field/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('update_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/update_runtime_field/main.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/update_runtime_field/main.ts new file mode 100644 index 0000000000000..385ae4f548440 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/runtime_fields_crud/update_runtime_field/main.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { INITIAL_REST_VERSION } from '@kbn/data-views-plugin/server/constants'; +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { configArray } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const svlCommonApi = getService('svlCommonApi'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + configArray.forEach((config) => { + describe(config.name, () => { + it('can update an existing field', async () => { + const title = `basic_index`; + const response1 = await supertest + .post(config.path) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + override: true, + [config.serviceKey]: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + runtimeBar: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + }, + }, + }); + + const response2 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field/runtimeFoo`) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + runtimeField: { + type: 'keyword', + script: { + source: "doc['something_new'].value", + }, + }, + }); + + expect(response2.status).to.be(200); + + const response3 = await supertest + .get(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field/runtimeFoo`) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + const field = + config.serviceKey === 'index_pattern' ? response3.body.field : response3.body.fields[0]; + + expect(response3.status).to.be(200); + expect(response3.body[config.serviceKey]).to.not.be.empty(); + expect(field.type).to.be('string'); + expect(field.runtimeField.type).to.be('keyword'); + expect(field.runtimeField.script.source).to.be("doc['something_new'].value"); + + // Partial update + const response4 = await supertest + .post(`${config.path}/${response1.body[config.serviceKey].id}/runtime_field/runtimeFoo`) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + runtimeField: { + script: { + source: "doc['partial_update'].value", + }, + }, + }); + + expect(response4.status).to.be(200); + const field2 = + config.serviceKey === 'index_pattern' ? response4.body.field : response4.body.fields[0]; + + expect(field2.runtimeField.script.source).to.be("doc['partial_update'].value"); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/swap_references/errors.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/swap_references/errors.ts new file mode 100644 index 0000000000000..0762f52bd0c44 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/swap_references/errors.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { INITIAL_REST_VERSION } from '@kbn/data-views-plugin/server/constants'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; +import { dataViewConfig } from '../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('errors', () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .get(`${dataViewConfig.path}/${id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response.status).to.be(404); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest + .get(`${dataViewConfig.path}/${id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/swap_references/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/swap_references/index.ts new file mode 100644 index 0000000000000..fc3c15d0d473a --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/swap_references/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('swap_references', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/data_views/swap_references/main.ts b/x-pack/test_serverless/api_integration/test_suites/common/data_views/swap_references/main.ts new file mode 100644 index 0000000000000..a2184a294e67a --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/data_views/swap_references/main.ts @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { + DATA_VIEW_SWAP_REFERENCES_PATH, + SPECIFIC_DATA_VIEW_PATH, + DATA_VIEW_PATH, +} from '@kbn/data-views-plugin/server'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { INITIAL_REST_VERSION } from '@kbn/data-views-plugin/server/constants'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + const title = 'logs-*'; + const prevDataViewId = '91200a00-9efd-11e7-acb3-3dab96693fab'; + const PREVIEW_PATH = `${DATA_VIEW_SWAP_REFERENCES_PATH}/_preview`; + let dataViewId = ''; + + describe('main', () => { + const kibanaServer = getService('kibanaServer'); + before(async () => { + const result = await supertest + .post(DATA_VIEW_PATH) + .send({ data_view: { title } }) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + dataViewId = result.body.data_view.id; + }); + after(async () => { + await supertest + .delete(SPECIFIC_DATA_VIEW_PATH.replace('{id}', dataViewId)) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + }); + beforeEach(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + afterEach(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + + it('can preview', async () => { + const res = await supertest + .post(PREVIEW_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fromId: prevDataViewId, + toId: dataViewId, + }); + expect(res).to.have.property('status', 200); + }); + + it('can preview specifying type', async () => { + const res = await supertest + .post(PREVIEW_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fromId: prevDataViewId, + fromType: 'index-pattern', + toId: dataViewId, + }); + expect(res).to.have.property('status', 200); + }); + + it('can save changes', async () => { + const res = await supertest + .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fromId: prevDataViewId, + toId: dataViewId, + }); + expect(res).to.have.property('status', 200); + expect(res.body.result.length).to.equal(1); + expect(res.body.result[0].id).to.equal('dd7caf20-9efd-11e7-acb3-3dab96693fab'); + expect(res.body.result[0].type).to.equal('visualization'); + }); + + it('can save changes and remove old saved object', async () => { + const res = await supertest + .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fromId: prevDataViewId, + toId: dataViewId, + delete: true, + }); + expect(res).to.have.property('status', 200); + expect(res.body.result.length).to.equal(1); + expect(res.body.deleteStatus.remainingRefs).to.equal(0); + expect(res.body.deleteStatus.deletePerformed).to.equal(true); + + const res2 = await supertest + .get(SPECIFIC_DATA_VIEW_PATH.replace('{id}', prevDataViewId)) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + + expect(res2).to.have.property('statusCode', 404); + }); + + describe('limit affected saved objects', () => { + beforeEach(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/management/saved_objects/relationships.json' + ); + }); + afterEach(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/management/saved_objects/relationships.json' + ); + }); + + it("won't delete if reference remains", async () => { + const res = await supertest + .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + fromId: '8963ca30-3224-11e8-a572-ffca06da1357', + toId: '91200a00-9efd-11e7-acb3-3dab96693fab', + forId: ['960372e0-3224-11e8-a572-ffca06da1357'], + delete: true, + }); + expect(res).to.have.property('status', 200); + expect(res.body.result.length).to.equal(1); + expect(res.body.deleteStatus.remainingRefs).to.equal(1); + expect(res.body.deleteStatus.deletePerformed).to.equal(false); + }); + + it('can limit by id', async () => { + // confirm this will find two items + const res = await supertest + .post(PREVIEW_PATH) + .send({ + fromId: '8963ca30-3224-11e8-a572-ffca06da1357', + toId: '91200a00-9efd-11e7-acb3-3dab96693fab', + }) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + expect(res).to.have.property('status', 200); + expect(res.body.result.length).to.equal(2); + + // limit to one item + const res2 = await supertest + .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .send({ + fromId: '8963ca30-3224-11e8-a572-ffca06da1357', + toId: '91200a00-9efd-11e7-acb3-3dab96693fab', + forId: ['960372e0-3224-11e8-a572-ffca06da1357'], + }) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + expect(res2).to.have.property('status', 200); + expect(res2.body.result.length).to.equal(1); + }); + + it('can limit by type', async () => { + // confirm this will find two items + const res = await supertest + .post(PREVIEW_PATH) + .send({ + fromId: '8963ca30-3224-11e8-a572-ffca06da1357', + toId: '91200a00-9efd-11e7-acb3-3dab96693fab', + }) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + expect(res).to.have.property('status', 200); + expect(res.body.result.length).to.equal(2); + + // limit to one item + const res2 = await supertest + .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .send({ + fromId: '8963ca30-3224-11e8-a572-ffca06da1357', + toId: '91200a00-9efd-11e7-acb3-3dab96693fab', + forType: 'search', + }) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()); + expect(res2).to.have.property('status', 200); + expect(res2.body.result.length).to.equal(1); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/index.ts index 4ea6b50bb8f2f..ae68f7ec7670c 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/index.ts @@ -28,5 +28,11 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./index_management')); loadTestFile(require.resolve('./alerting')); loadTestFile(require.resolve('./ingest_pipelines')); + loadTestFile(require.resolve('./data_view_field_editor')); + loadTestFile(require.resolve('./data_views')); + loadTestFile(require.resolve('./kql_telemetry')); + loadTestFile(require.resolve('./scripts_tests')); + loadTestFile(require.resolve('./search_oss')); + loadTestFile(require.resolve('./search_xpack')); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/common/kql_telemetry/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/kql_telemetry/index.ts new file mode 100644 index 0000000000000..07b76ff08e58c --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/kql_telemetry/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('KQL', () => { + loadTestFile(require.resolve('./kql_telemetry')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/kql_telemetry/kql_telemetry.ts b/x-pack/test_serverless/api_integration/test_suites/common/kql_telemetry/kql_telemetry.ts new file mode 100644 index 0000000000000..adbac6e89b548 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/kql_telemetry/kql_telemetry.ts @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { get } from 'lodash'; +import { ANALYTICS_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; +import { KQL_TELEMETRY_ROUTE_LATEST_VERSION } from '@kbn/data-plugin/common'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const es = getService('es'); + const svlCommonApi = getService('svlCommonApi'); + + describe('telemetry API', () => { + before(async () => { + // TODO: Clean `kql-telemetry` before running the tests + await kibanaServer.savedObjects.clean({ types: ['kql-telemetry'] }); + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + + it('should increment the opt *in* counter in the .kibana_analytics/kql-telemetry document', async () => { + await supertest + .post('/internal/kql_opt_in_stats') + .set('content-type', 'application/json') + .set(ELASTIC_HTTP_VERSION_HEADER, KQL_TELEMETRY_ROUTE_LATEST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ opt_in: true }) + .expect(200); + + return es + .search({ + index: ANALYTICS_SAVED_OBJECT_INDEX, + q: 'type:kql-telemetry', + }) + .then((response) => { + const kqlTelemetryDoc = get(response, 'hits.hits[0]._source.kql-telemetry'); + expect(kqlTelemetryDoc.optInCount).to.be(1); + }); + }); + + it('should increment the opt *out* counter in the .kibana_analytics/kql-telemetry document', async () => { + await supertest + .post('/internal/kql_opt_in_stats') + .set('content-type', 'application/json') + .set(ELASTIC_HTTP_VERSION_HEADER, KQL_TELEMETRY_ROUTE_LATEST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ opt_in: false }) + .expect(200); + + return es + .search({ + index: ANALYTICS_SAVED_OBJECT_INDEX, + q: 'type:kql-telemetry', + }) + .then((response) => { + const kqlTelemetryDoc = get(response, 'hits.hits[0]._source.kql-telemetry'); + expect(kqlTelemetryDoc.optOutCount).to.be(1); + }); + }); + + it('should report success when opt *in* is incremented successfully', () => { + return ( + supertest + .post('/internal/kql_opt_in_stats') + .set('content-type', 'application/json') + .set(ELASTIC_HTTP_VERSION_HEADER, KQL_TELEMETRY_ROUTE_LATEST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ opt_in: true }) + .expect('Content-Type', /json/) + .expect(200) + .then(({ body }) => { + expect(body.success).to.be(true); + }) + ); + }); + + it('should report success when opt *out* is incremented successfully', () => { + return ( + supertest + .post('/internal/kql_opt_in_stats') + .set('content-type', 'application/json') + .set(ELASTIC_HTTP_VERSION_HEADER, KQL_TELEMETRY_ROUTE_LATEST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ opt_in: false }) + .expect('Content-Type', /json/) + .expect(200) + .then(({ body }) => { + expect(body.success).to.be(true); + }) + ); + }); + + it('should only accept literal boolean values for the opt_in POST body param', function () { + return Promise.all([ + supertest + .post('/internal/kql_opt_in_stats') + .set('content-type', 'application/json') + .set(ELASTIC_HTTP_VERSION_HEADER, KQL_TELEMETRY_ROUTE_LATEST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ opt_in: 'notabool' }) + .expect(400), + supertest + .post('/internal/kql_opt_in_stats') + .set('content-type', 'application/json') + .set(ELASTIC_HTTP_VERSION_HEADER, KQL_TELEMETRY_ROUTE_LATEST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ opt_in: 0 }) + .expect(400), + supertest + .post('/internal/kql_opt_in_stats') + .set('content-type', 'application/json') + .set(ELASTIC_HTTP_VERSION_HEADER, KQL_TELEMETRY_ROUTE_LATEST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ opt_in: null }) + .expect(400), + supertest + .post('/internal/kql_opt_in_stats') + .set('content-type', 'application/json') + .set(ELASTIC_HTTP_VERSION_HEADER, KQL_TELEMETRY_ROUTE_LATEST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ opt_in: undefined }) + .expect(400), + supertest + .post('/internal/kql_opt_in_stats') + .set('content-type', 'application/json') + .set(ELASTIC_HTTP_VERSION_HEADER, KQL_TELEMETRY_ROUTE_LATEST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({}) + .expect(400), + ]); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/scripts_tests/index.js b/x-pack/test_serverless/api_integration/test_suites/common/scripts_tests/index.js new file mode 100644 index 0000000000000..d1eeb009a7cce --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/scripts_tests/index.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default function ({ loadTestFile }) { + // TODO: The `scripts` folder was renamed to `scripts_tests` because the folder + // name `scripts` triggers the `eslint@kbn/imports/no_boundary_crossing` rule + describe('scripts', () => { + loadTestFile(require.resolve('./languages')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/scripts_tests/languages.js b/x-pack/test_serverless/api_integration/test_suites/common/scripts_tests/languages.js new file mode 100644 index 0000000000000..832bf6df49188 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/scripts_tests/languages.js @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { SCRIPT_LANGUAGES_ROUTE_LATEST_VERSION } from '@kbn/data-plugin/common/constants'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const svlCommonApi = getService('svlCommonApi'); + + describe('Script Languages API', function getLanguages() { + it('should return 200 with an array of languages', () => + supertest + .get('/internal/scripts/languages') + .set(ELASTIC_HTTP_VERSION_HEADER, SCRIPT_LANGUAGES_ROUTE_LATEST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200) + .then((response) => { + expect(response.body).to.be.an('array'); + })); + + // eslint-disable-next-line jest/no-disabled-tests + it.skip('should only return langs enabled for inline scripting', () => + supertest + .get('/internal/scripts/languages') + .set(ELASTIC_HTTP_VERSION_HEADER, SCRIPT_LANGUAGES_ROUTE_LATEST_VERSION) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200) + .then((response) => { + expect(response.body).to.contain('expression'); + expect(response.body).to.contain('painless'); + expect(response.body).to.not.contain('groovy'); + })); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/search_oss/bsearch.ts b/x-pack/test_serverless/api_integration/test_suites/common/search_oss/bsearch.ts new file mode 100644 index 0000000000000..ba1f974a01608 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/search_oss/bsearch.ts @@ -0,0 +1,249 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import request from 'superagent'; +import { inflateResponse } from '@kbn/bfetch-plugin/public/streaming'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { BFETCH_ROUTE_VERSION_LATEST } from '@kbn/bfetch-plugin/common'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; +import { painlessErrReq } from './painless_err_req'; +import { verifyErrorResponse } from './verify_error'; + +function parseBfetchResponse(resp: request.Response, compressed: boolean = false) { + return resp.text + .trim() + .split('\n') + .map((item) => { + return JSON.parse(compressed ? inflateResponse(item) : item); + }); +} + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const svlCommonApi = getService('svlCommonApi'); + + describe('bsearch', () => { + describe('post', () => { + it('should return 200 a single response', async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + batch: [ + { + request: { + params: { + index: '.kibana', + body: { + query: { + match_all: {}, + }, + }, + }, + }, + options: { + strategy: 'es', + }, + }, + ], + }); + + const jsonBody = parseBfetchResponse(resp); + + expect(resp.status).to.be(200); + expect(jsonBody[0].id).to.be(0); + expect(jsonBody[0].result.isPartial).to.be(false); + expect(jsonBody[0].result.isRunning).to.be(false); + expect(jsonBody[0].result).to.have.property('rawResponse'); + }); + + it('should return 200 a single response from compressed', async () => { + const resp = await supertest + .post(`/internal/bsearch?compress=true`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + batch: [ + { + request: { + params: { + index: '.kibana', + body: { + query: { + match_all: {}, + }, + }, + }, + }, + options: { + strategy: 'es', + }, + }, + ], + }); + + const jsonBody = parseBfetchResponse(resp, true); + + expect(resp.status).to.be(200); + expect(jsonBody[0].id).to.be(0); + expect(jsonBody[0].result.isPartial).to.be(false); + expect(jsonBody[0].result.isRunning).to.be(false); + expect(jsonBody[0].result).to.have.property('rawResponse'); + }); + + it('should return a batch of successful responses', async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + batch: [ + { + request: { + params: { + index: '.kibana', + body: { + query: { + match_all: {}, + }, + }, + }, + }, + }, + { + request: { + params: { + index: '.kibana', + body: { + query: { + match_all: {}, + }, + }, + }, + }, + }, + ], + }); + + expect(resp.status).to.be(200); + const parsedResponse = parseBfetchResponse(resp); + expect(parsedResponse).to.have.length(2); + parsedResponse.forEach((responseJson) => { + expect(responseJson.result).to.have.property('isPartial'); + expect(responseJson.result).to.have.property('isRunning'); + expect(responseJson.result).to.have.property('rawResponse'); + }); + }); + + it('should return error for not found strategy', async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + batch: [ + { + request: { + params: { + index: '.kibana', + body: { + query: { + match_all: {}, + }, + }, + }, + }, + options: { + strategy: 'wtf', + }, + }, + ], + }); + + expect(resp.status).to.be(200); + parseBfetchResponse(resp).forEach((responseJson, i) => { + expect(responseJson.id).to.be(i); + verifyErrorResponse(responseJson.error, 404, 'Search strategy wtf not found'); + }); + }); + + it('should return 400 when index type is provided in "es" strategy', async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + batch: [ + { + request: { + index: '.kibana', + indexType: 'baad', + params: { + body: { + query: { + match_all: {}, + }, + }, + }, + }, + options: { + strategy: 'es', + }, + }, + ], + }); + + expect(resp.status).to.be(200); + parseBfetchResponse(resp).forEach((responseJson, i) => { + expect(responseJson.id).to.be(i); + verifyErrorResponse(responseJson.error, 400, 'Unsupported index pattern type baad'); + }); + }); + + describe('painless', () => { + before(async () => { + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + }); + + after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + }); + it('should return 400 "search_phase_execution_exception" for Painless error in "es" strategy', async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set(ELASTIC_HTTP_VERSION_HEADER, BFETCH_ROUTE_VERSION_LATEST) + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + batch: [ + { + request: painlessErrReq, + options: { + strategy: 'es', + }, + }, + ], + }); + + expect(resp.status).to.be(200); + parseBfetchResponse(resp).forEach((responseJson, i) => { + expect(responseJson.id).to.be(i); + verifyErrorResponse(responseJson.error, 400, 'search_phase_execution_exception', true); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/search_oss/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/search_oss/index.ts new file mode 100644 index 0000000000000..598493bfd2182 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/search_oss/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + // TODO: This `search` folder was renamed to `search_oss` to + // differentiate it from the x-pack `search` folder (now `search_xpack`) + describe('search', () => { + loadTestFile(require.resolve('./search')); + // TODO: Removed `sql_search` since + // SQL is not supported in Serverless + loadTestFile(require.resolve('./bsearch')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/search_oss/painless_err_req.ts b/x-pack/test_serverless/api_integration/test_suites/common/search_oss/painless_err_req.ts new file mode 100644 index 0000000000000..3e722770c5ae1 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/search_oss/painless_err_req.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const painlessErrReq = { + params: { + index: 'log*', + body: { + size: 500, + fields: ['*'], + script_fields: { + invalid_scripted_field: { + script: { + source: 'invalid', + lang: 'painless', + }, + }, + }, + stored_fields: ['*'], + query: { + bool: { + filter: [ + { + match_all: {}, + }, + { + range: { + '@timestamp': { + gte: '2015-01-19T12:27:55.047Z', + lte: '2021-01-19T12:27:55.047Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + }, + }, +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/common/search_oss/search.ts b/x-pack/test_serverless/api_integration/test_suites/common/search_oss/search.ts new file mode 100644 index 0000000000000..85a57a2c2d272 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/search_oss/search.ts @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; +import { painlessErrReq } from './painless_err_req'; +import { verifyErrorResponse } from './verify_error'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const svlCommonApi = getService('svlCommonApi'); + const kibanaServer = getService('kibanaServer'); + + describe('search', () => { + before(async () => { + // TODO: emptyKibanaIndex fails in Serverless with + // "index_not_found_exception: no such index [.kibana_ingest]", + // so it was switched to `savedObjects.cleanStandardList()` + await kibanaServer.savedObjects.cleanStandardList(); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + }); + + after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + }); + describe('post', () => { + it('should return 200 when correctly formatted searches are provided', async () => { + const resp = await supertest + .post(`/internal/search/es`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + params: { + body: { + query: { + match_all: {}, + }, + }, + }, + }) + .expect(200); + + expect(resp.status).to.be(200); + expect(resp.body.isPartial).to.be(false); + expect(resp.body.isRunning).to.be(false); + expect(resp.body).to.have.property('rawResponse'); + expect(resp.header).to.have.property(ELASTIC_HTTP_VERSION_HEADER, '1'); + }); + + it('should return 200 if terminated early', async () => { + const resp = await supertest + .post(`/internal/search/es`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + params: { + terminateAfter: 1, + index: 'log*', + size: 1000, + body: { + query: { + match_all: {}, + }, + }, + }, + }) + .expect(200); + + expect(resp.status).to.be(200); + expect(resp.body.isPartial).to.be(false); + expect(resp.body.isRunning).to.be(false); + expect(resp.body.rawResponse.terminated_early).to.be(true); + expect(resp.header).to.have.property(ELASTIC_HTTP_VERSION_HEADER, '1'); + }); + + it('should return 404 when if no strategy is provided', async () => { + const resp = await supertest + .post(`/internal/search`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + body: { + query: { + match_all: {}, + }, + }, + }) + .expect(404); + + verifyErrorResponse(resp.body, 404); + }); + + it('should return 404 when if unknown strategy is provided', async () => { + const resp = await supertest + .post(`/internal/search/banana`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + body: { + query: { + match_all: {}, + }, + }, + }) + .expect(404); + + verifyErrorResponse(resp.body, 404); + expect(resp.body.message).to.contain('banana not found'); + expect(resp.header).to.have.property(ELASTIC_HTTP_VERSION_HEADER, '1'); + }); + + it('should return 400 with illegal ES argument', async () => { + const resp = await supertest + .post(`/internal/search/es`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + params: { + timeout: 1, // This should be a time range string! + index: 'log*', + size: 1000, + body: { + query: { + match_all: {}, + }, + }, + }, + }) + .expect(400); + + verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true); + }); + + it('should return 400 with a bad body', async () => { + const resp = await supertest + .post(`/internal/search/es`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + params: { + body: { + index: 'nope nope', + bad_query: [], + }, + }, + }) + .expect(400); + + verifyErrorResponse(resp.body, 400, 'parsing_exception', true); + }); + + it('should return 400 for a painless error', async () => { + const resp = await supertest + .post(`/internal/search/es`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send(painlessErrReq) + .expect(400); + + verifyErrorResponse(resp.body, 400, 'search_phase_execution_exception', true); + }); + }); + + describe('delete', () => { + it('should return 404 when no search id provided', async () => { + const resp = await supertest + .delete(`/internal/search/es`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send() + .expect(404); + verifyErrorResponse(resp.body, 404); + }); + + it('should return 400 when trying a delete on a non supporting strategy', async () => { + const resp = await supertest + .delete(`/internal/search/es/123`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .send() + .expect(400); + verifyErrorResponse(resp.body, 400); + expect(resp.body.message).to.contain("Search strategy es doesn't support cancellations"); + expect(resp.header).to.have.property(ELASTIC_HTTP_VERSION_HEADER, '1'); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/search_oss/verify_error.ts b/x-pack/test_serverless/api_integration/test_suites/common/search_oss/verify_error.ts new file mode 100644 index 0000000000000..3523cefc8b33f --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/search_oss/verify_error.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +export const verifyErrorResponse = ( + r: any, + expectedCode: number, + message?: string, + shouldHaveAttrs?: boolean +) => { + expect(r.statusCode).to.be(expectedCode); + if (message) { + expect(r.message).to.include.string(message); + } + if (shouldHaveAttrs) { + expect(r).to.have.property('attributes'); + expect(r.attributes).to.have.property('root_cause'); + } else { + expect(r).not.to.have.property('attributes'); + } +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/common/search_xpack/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/search_xpack/index.ts new file mode 100644 index 0000000000000..fc433f4655977 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/search_xpack/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + // TODO: This `search` folder was renamed to `search_xpack` to + // differentiate it from the oss `search` folder (now `search_oss`) + describe('search', () => { + loadTestFile(require.resolve('./search')); + // TODO: Removed `session` since search + // sessions are not supported in Serverless + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/search_xpack/search.ts b/x-pack/test_serverless/api_integration/test_suites/common/search_xpack/search.ts new file mode 100644 index 0000000000000..1a4839cbae6fa --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/search_xpack/search.ts @@ -0,0 +1,501 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { parse as parseCookie } from 'tough-cookie'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { omit } from 'lodash'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; +import { verifyErrorResponse } from '../search_oss/verify_error'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const log = getService('log'); + const retry = getService('retry'); + const security = getService('security'); + // TODO: `supertestWithoutAuth` is typed as `any` in `x-pack/test/api_integration/apis/search/search.ts`, + // but within Serverless tests it's typed as `supertest.SuperTest`. This causes TS errors + // when accessing `loginResponse.headers`, so we cast it as `any` here to match the original tests. + const supertestNoAuth = getService('supertestWithoutAuth') as any; + const svlCommonApi = getService('svlCommonApi'); + + const shardDelayAgg = (delay: string) => ({ + aggs: { + delay: { + shard_delay: { + value: delay, + }, + }, + }, + }); + + async function markRequiresShardDelayAgg(testContext: Mocha.Context) { + const body = await es.info(); + if (!body.version.number.includes('SNAPSHOT')) { + log.debug('Skipping because this build does not have the required shard_delay agg'); + testContext.skip(); + } + } + + describe('search', () => { + before(async () => { + // ensure es not empty + await es.index({ + index: 'search-api-test', + id: 'search-api-test-doc', + body: { message: 'test doc' }, + refresh: 'wait_for', + }); + }); + after(async () => { + await es.indices.delete({ + index: 'search-api-test', + }); + }); + + describe('post', () => { + it('should return 200 with final response without search id if wait_for_completion_timeout is long enough', async function () { + const resp = await supertest + .post(`/internal/search/ese`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({ + params: { + body: { + query: { + match_all: {}, + }, + }, + wait_for_completion_timeout: '10s', + }, + }) + .expect(200); + + const { id } = resp.body; + expect(id).to.be(undefined); + expect(resp.body.isPartial).to.be(false); + expect(resp.body.isRunning).to.be(false); + expect(resp.body).to.have.property('rawResponse'); + }); + + it('should return 200 with search id and partial response if wait_for_completion_timeout is not long enough', async function () { + await markRequiresShardDelayAgg(this); + + const resp = await supertest + .post(`/internal/search/ese`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({ + params: { + body: { + query: { + match_all: {}, + }, + ...shardDelayAgg('3s'), + }, + wait_for_completion_timeout: '1ms', + }, + }) + .expect(200); + + const { id } = resp.body; + expect(id).not.to.be(undefined); + expect(resp.body.isPartial).to.be(true); + expect(resp.body.isRunning).to.be(true); + expect(resp.body).to.have.property('rawResponse'); + }); + + it('should retrieve results from completed search with search id', async function () { + await markRequiresShardDelayAgg(this); + + const resp = await supertest + .post(`/internal/search/ese`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({ + params: { + body: { + query: { + match_all: {}, + }, + ...shardDelayAgg('3s'), + }, + wait_for_completion_timeout: '1ms', + }, + }) + .expect(200); + + const { id } = resp.body; + expect(id).not.to.be(undefined); + expect(resp.body.isPartial).to.be(true); + expect(resp.body.isRunning).to.be(true); + + await new Promise((resolve) => setTimeout(resolve, 3000)); + + await retry.tryForTime(10000, async () => { + const resp2 = await supertest + .post(`/internal/search/ese/${id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({}) + .expect(200); + + expect(resp2.body.id).not.to.be(undefined); + expect(resp2.body.isPartial).to.be(false); + expect(resp2.body.isRunning).to.be(false); + + return true; + }); + }); + + it('should retrieve results from in-progress search with search id', async function () { + await markRequiresShardDelayAgg(this); + + const resp = await supertest + .post(`/internal/search/ese`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({ + params: { + body: { + query: { + match_all: {}, + }, + ...shardDelayAgg('10s'), + }, + wait_for_completion_timeout: '1ms', + }, + }) + .expect(200); + + const { id } = resp.body; + expect(id).not.to.be(undefined); + expect(resp.body.isPartial).to.be(true); + expect(resp.body.isRunning).to.be(true); + + const resp2 = await supertest + .post(`/internal/search/ese/${id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({}) + .expect(200); + + expect(resp2.body.id).not.to.be(undefined); + expect(resp2.body.isPartial).to.be(true); + expect(resp2.body.isRunning).to.be(true); + }); + + it('should fail without kbn-xref header', async () => { + const resp = await supertest + .post(`/internal/search/ese`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(omit(svlCommonApi.getInternalRequestHeader(), 'kbn-xsrf')) + .send({ + params: { + body: { + query: { + match_all: {}, + }, + }, + }, + }) + .expect(400); + + verifyErrorResponse(resp.body, 400, 'Request must contain a kbn-xsrf header.'); + }); + + it('should return 400 when unknown index type is provided', async () => { + const resp = await supertest + .post(`/internal/search/ese`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({ + indexType: 'baad', + params: { + body: { + query: { + match_all: {}, + }, + }, + }, + }) + .expect(400); + + verifyErrorResponse(resp.body, 400, 'Unknown indexType'); + }); + + it('should return 400 if invalid id is provided', async () => { + const resp = await supertest + .post(`/internal/search/ese/123`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({ + params: { + body: { + query: { + match_all: {}, + }, + }, + }, + }) + .expect(400); + + verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true); + }); + + it('should return 404 if unknown id is provided', async () => { + const resp = await supertest + .post( + `/internal/search/ese/FkxOb21iV1g2VGR1S2QzaWVtRU9fMVEbc3JWeWc1VHlUdDZ6MENxcXlYVG1Fdzo2NDg4` + ) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({ + params: { + body: { + query: { + match_all: {}, + }, + }, + }, + }) + .expect(404); + verifyErrorResponse(resp.body, 404, 'resource_not_found_exception', true); + }); + + it('should return 400 with a bad body', async () => { + const resp = await supertest + .post(`/internal/search/ese`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({ + params: { + body: { + index: 'nope nope', + bad_query: [], + }, + }, + }) + .expect(400); + + verifyErrorResponse(resp.body, 400, 'parsing_exception', true); + }); + + // TODO: Security works differently in Serverless so this test fails, + // we'll need to figure out how to test this properly in Serverless + it.skip('should return 403 for lack of privledges', async () => { + const username = 'no_access'; + const password = 't0pS3cr3t'; + + await security.user.create(username, { + password, + roles: ['test_shakespeare_reader'], + }); + + const loginResponse = await supertestNoAuth + .post('/internal/security/login') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { username, password }, + }) + .expect(200); + + const sessionCookie = parseCookie(loginResponse.headers['set-cookie'][0]); + + await supertestNoAuth + .post(`/internal/search/ese`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .set('Cookie', sessionCookie!.cookieString()) + .send({ + params: { + index: 'log*', + body: { + query: { + match_all: {}, + }, + }, + wait_for_completion_timeout: '10s', + }, + }) + .expect(403); + + await security.testUser.restoreDefaults(); + }); + }); + + // TODO: Removed rollup tests since rollups aren't supported in Serverless + + describe('delete', () => { + it('should return 404 when no search id provided', async () => { + await supertest + .delete(`/internal/search/ese`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send() + .expect(404); + }); + + it('should return 400 when trying a delete a bad id', async () => { + const resp = await supertest + .delete(`/internal/search/ese/123`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send() + .expect(400); + verifyErrorResponse(resp.body, 400, 'illegal_argument_exception', true); + }); + + it('should delete an in-progress search', async function () { + await markRequiresShardDelayAgg(this); + + const resp = await supertest + .post(`/internal/search/ese`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({ + params: { + body: { + query: { + match_all: {}, + }, + ...shardDelayAgg('10s'), + }, + wait_for_completion_timeout: '1ms', + }, + }) + .expect(200); + + const { id } = resp.body; + expect(id).not.to.be(undefined); + expect(resp.body.isPartial).to.be(true); + expect(resp.body.isRunning).to.be(true); + + await supertest + .delete(`/internal/search/ese/${id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send() + .expect(200); + + // try to re-fetch + await supertest + .post(`/internal/search/ese/${id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({}) + .expect(404); + }); + + // FLAKY: https://github.com/elastic/kibana/issues/164856 + it.skip('should delete a completed search', async function () { + await markRequiresShardDelayAgg(this); + + const resp = await supertest + .post(`/internal/search/ese`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({ + params: { + body: { + query: { + match_all: {}, + }, + ...shardDelayAgg('3s'), + }, + wait_for_completion_timeout: '1ms', + }, + }) + .expect(200); + + const { id } = resp.body; + expect(id).not.to.be(undefined); + expect(resp.body.isPartial).to.be(true); + expect(resp.body.isRunning).to.be(true); + + await new Promise((resolve) => setTimeout(resolve, 3000)); + + await retry.tryForTime(10000, async () => { + const resp2 = await supertest + .post(`/internal/search/ese/${id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({}) + .expect(200); + + expect(resp2.body.id).not.to.be(undefined); + expect(resp2.body.isPartial).to.be(false); + expect(resp2.body.isRunning).to.be(false); + + return true; + }); + + await supertest + .delete(`/internal/search/ese/${id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send() + .expect(200); + + // try to re-fetch + await supertest + .post(`/internal/search/ese/${id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + // TODO: API requests in Serverless require internal request headers + .set(svlCommonApi.getInternalRequestHeader()) + .set('kbn-xsrf', 'foo') + .send({}) + .expect(404); + }); + }); + }); +} diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index b986a6134525b..cf52b712de521 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -48,5 +48,8 @@ "@kbn/data-views-plugin", "@kbn/core-saved-objects-server", "@kbn/security-api-integration-helpers", + "@kbn/data-view-field-editor-plugin", + "@kbn/data-plugin", + "@kbn/bfetch-plugin", ] }