diff --git a/docs/user/plugins.asciidoc b/docs/user/plugins.asciidoc index 0bf407ac1ff90..8dc3d8b6e9e0d 100644 --- a/docs/user/plugins.asciidoc +++ b/docs/user/plugins.asciidoc @@ -81,7 +81,6 @@ Use it to create, edit and embed visualizations, and also to search inside an em * https://github.com/sw-jung/kibana_markdown_doc_view[Markdown Doc View] (sw-jung) - A plugin for custom doc view using markdown+handlebars template. * https://github.com/datasweet-fr/kibana-datasweet-formula[Datasweet Formula] (datasweet) - enables calculated metric on any standard Kibana visualization. -* https://github.com/pjhampton/kibana-prometheus-exporter[Prometheus Exporter] - exports the Kibana metrics in the prometheus format NOTE: To add your plugin to this page, open a {kib-repo}tree/{branch}/docs/plugins/known-plugins.asciidoc[pull request]. @@ -183,4 +182,4 @@ you must specify the path to that configuration file each time you use the `bin/ 0:: Success 64:: Unknown command or incorrect option parameter 74:: I/O error -70:: Other error +70:: Other error \ No newline at end of file diff --git a/package.json b/package.json index fadec309bd56c..c3fccbbcafc3c 100644 --- a/package.json +++ b/package.json @@ -1470,7 +1470,7 @@ "blob-polyfill": "^7.0.20220408", "callsites": "^3.1.0", "chance": "1.0.18", - "chromedriver": "^119.0.0", + "chromedriver": "^119.0.1", "clean-webpack-plugin": "^3.0.0", "cli-table3": "^0.6.1", "copy-webpack-plugin": "^6.0.2", diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.test.ts new file mode 100644 index 0000000000000..3faeee08048ef --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.test.ts @@ -0,0 +1,1048 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/no-shadow */ + +import { + pointInTimeFinderMock, + mockGetBulkOperationError, + mockGetCurrentTime, + mockPreflightCheckForCreate, + mockGetSearchDsl, +} from '../repository.test.mock'; + +import type { Payload } from '@hapi/boom'; + +import type { SavedObjectsBulkCreateObject } from '@kbn/core-saved-objects-api-server'; +import { + type SavedObjectsRawDoc, + type SavedObjectUnsanitizedDoc, + type SavedObjectReference, +} from '@kbn/core-saved-objects-server'; +import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server'; +import { SavedObjectsRepository } from '../repository'; +import { loggerMock } from '@kbn/logging-mocks'; +import { SavedObjectsSerializer } from '@kbn/core-saved-objects-base-server-internal'; +import { kibanaMigratorMock } from '../../mocks'; +import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; + +import { + CUSTOM_INDEX_TYPE, + NAMESPACE_AGNOSTIC_TYPE, + MULTI_NAMESPACE_TYPE, + MULTI_NAMESPACE_ISOLATED_TYPE, + HIDDEN_TYPE, + mockVersionProps, + mockTimestampFields, + mockTimestamp, + mappings, + mockVersion, + createRegistry, + createDocumentMigrator, + createSpySerializer, + bulkCreateSuccess, + getMockBulkCreateResponse, + expectErrorResult, + expectErrorInvalidType, + expectErrorConflict, + expectError, + createBadRequestErrorPayload, + expectCreateResult, + mockTimestampFieldsWithCreated, +} from '../../test_helpers/repository.test.common'; + +// BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository +// so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. + +interface ExpectedErrorResult { + type: string; + id: string; + error: Record; +} + +describe('SavedObjectsRepository', () => { + let client: ReturnType; + let repository: SavedObjectsRepository; + let migrator: ReturnType; + let logger: ReturnType; + let serializer: jest.Mocked; + + const registry = createRegistry(); + const documentMigrator = createDocumentMigrator(registry); + + const expectSuccess = ({ type, id }: { type: string; id: string }) => { + // @ts-expect-error TS is not aware of the extension + return expect.toBeDocumentWithoutError(type, id); + }; + + const expectMigrationArgs = (args: unknown, contains = true, n = 1) => { + const obj = contains ? expect.objectContaining(args) : expect.not.objectContaining(args); + expect(migrator.migrateDocument).toHaveBeenNthCalledWith( + n, + obj, + expect.objectContaining({ + allowDowngrade: expect.any(Boolean), + }) + ); + }; + + beforeEach(() => { + pointInTimeFinderMock.mockClear(); + client = elasticsearchClientMock.createElasticsearchClient(); + migrator = kibanaMigratorMock.create(); + documentMigrator.prepareMigrations(); + migrator.migrateDocument = jest.fn().mockImplementation(documentMigrator.migrate); + migrator.runMigrations = jest.fn().mockResolvedValue([{ status: 'skipped' }]); + logger = loggerMock.create(); + + // create a mock serializer "shim" so we can track function calls, but use the real serializer's implementation + serializer = createSpySerializer(registry); + + const allTypes = registry.getAllTypes().map((type) => type.name); + const allowedTypes = [...new Set(allTypes.filter((type) => !registry.isHidden(type)))]; + + // @ts-expect-error must use the private constructor to use the mocked serializer + repository = new SavedObjectsRepository({ + index: '.kibana-test', + mappings, + client, + migrator, + typeRegistry: registry, + serializer, + allowedTypes, + logger, + }); + + mockGetCurrentTime.mockReturnValue(mockTimestamp); + mockGetSearchDsl.mockClear(); + }); + + // Setup migration mock for creating an object + const mockMigrationVersion = { foo: '2.3.4' }; + const mockMigrateDocument = (doc: SavedObjectUnsanitizedDoc) => ({ + ...doc, + attributes: { + ...doc.attributes, + ...(doc.attributes?.title && { title: `${doc.attributes.title}!!` }), + }, + migrationVersion: mockMigrationVersion, + managed: doc.managed ?? false, + references: [{ name: 'search_0', type: 'search', id: '123' }], + }); + + describe('#bulkCreate', () => { + beforeEach(() => { + mockPreflightCheckForCreate.mockReset(); + mockPreflightCheckForCreate.mockImplementation(({ objects }) => { + return Promise.resolve(objects.map(({ type, id }) => ({ type, id }))); // respond with no errors by default + }); + }); + + const obj1 = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + references: [{ name: 'ref_0', type: 'test', id: '1' }], + managed: false, + }; + const obj2 = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Test Two' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + managed: false, + }; + const namespace = 'foo-namespace'; + + // bulk create calls have two objects for each source -- the action, and the source + const expectClientCallArgsAction = ( + objects: Array<{ type: string; id?: string; if_primary_term?: string; if_seq_no?: string }>, + { + method, + _index = expect.any(String), + getId = () => expect.any(String), + }: { method: string; _index?: string; getId?: (type: string, id?: string) => string } + ) => { + const body = []; + for (const { type, id, if_primary_term: ifPrimaryTerm, if_seq_no: ifSeqNo } of objects) { + body.push({ + [method]: { + _index, + _id: getId(type, id), + ...(ifPrimaryTerm && ifSeqNo + ? { if_primary_term: expect.any(Number), if_seq_no: expect.any(Number) } + : {}), + }, + }); + body.push(expect.any(Object)); + } + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }; + + const expectObjArgs = ( + { + type, + attributes, + references, + }: { type: string; attributes: unknown; references?: SavedObjectReference[] }, + overrides: Record = {} + ) => [ + expect.any(Object), + expect.objectContaining({ + [type]: attributes, + references, + type, + ...overrides, + ...mockTimestampFields, + }), + ]; + describe('client calls', () => { + it(`should use the ES bulk action by default`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2]); + expect(client.bulk).toHaveBeenCalledTimes(1); + }); + + it(`should use the preflightCheckForCreate action before bulk action for any types that are multi-namespace, when id is defined`, async () => { + const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }]; + await bulkCreateSuccess(client, repository, objects); + expect(client.bulk).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); + expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( + expect.objectContaining({ + objects: [ + { + type: MULTI_NAMESPACE_ISOLATED_TYPE, + id: obj2.id, + overwrite: false, + namespaces: ['default'], + }, + ], + }) + ); + }); + + it(`should use the ES create method if ID is undefined and overwrite=true`, async () => { + const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined })); + await bulkCreateSuccess(client, repository, objects, { overwrite: true }); + expectClientCallArgsAction(objects, { method: 'create' }); + }); + + it(`should use the ES create method if ID is undefined and overwrite=false`, async () => { + const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined })); + await bulkCreateSuccess(client, repository, objects); + expectClientCallArgsAction(objects, { method: 'create' }); + }); + + it(`should use the ES index method if ID is defined and overwrite=true`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2], { overwrite: true }); + expectClientCallArgsAction([obj1, obj2], { method: 'index' }); + }); + + it(`should use the ES index method with version if ID and version are defined and overwrite=true`, async () => { + await bulkCreateSuccess( + client, + repository, + [ + { + ...obj1, + version: mockVersion, + }, + obj2, + ], + { overwrite: true } + ); + + const obj1WithSeq = { + ...obj1, + managed: obj1.managed, + if_seq_no: mockVersionProps._seq_no, + if_primary_term: mockVersionProps._primary_term, + }; + + expectClientCallArgsAction([obj1WithSeq, obj2], { method: 'index' }); + }); + + it(`should use the ES create method if ID is defined and overwrite=false`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2]); + expectClientCallArgsAction([obj1, obj2], { method: 'create' }); + }); + + it(`should use the ES index method if ID is defined, overwrite=true and managed=true in a document`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2], { + overwrite: true, + managed: true, + }); + expectClientCallArgsAction([obj1, obj2], { method: 'index' }); + }); + + it(`should use the ES create method if ID is defined, overwrite=false and managed=true in a document`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2], { managed: true }); + expectClientCallArgsAction([obj1, obj2], { method: 'create' }); + }); + + it(`formats the ES request`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2]); + const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + // this test only ensures that the client accepts the managed field in a document + it(`formats the ES request with managed=true in a document`, async () => { + const obj1WithManagedTrue = { ...obj1, managed: true }; + const obj2WithManagedTrue = { ...obj2, managed: true }; + await bulkCreateSuccess(client, repository, [obj1WithManagedTrue, obj2WithManagedTrue]); + const body = [...expectObjArgs(obj1WithManagedTrue), ...expectObjArgs(obj2WithManagedTrue)]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + describe('originId', () => { + it(`returns error if originId is set for non-multi-namespace type`, async () => { + const result = await repository.bulkCreate([ + { ...obj1, originId: 'some-originId' }, + { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE, originId: 'some-originId' }, + ]); + expect(result.saved_objects).toEqual([ + expect.objectContaining({ id: obj1.id, type: obj1.type, error: expect.anything() }), + expect.objectContaining({ + id: obj2.id, + type: NAMESPACE_AGNOSTIC_TYPE, + error: expect.anything(), + }), + ]); + expect(client.bulk).not.toHaveBeenCalled(); + }); + + it(`defaults to no originId`, async () => { + const objects = [ + { ...obj1, type: MULTI_NAMESPACE_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, + ]; + + await bulkCreateSuccess(client, repository, objects); + const expected = expect.not.objectContaining({ originId: expect.anything() }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + describe('with existing originId', () => { + beforeEach(() => { + mockPreflightCheckForCreate.mockImplementation(({ objects }) => { + const existingDocument = { + _source: { originId: 'existing-originId' }, + } as SavedObjectsRawDoc; + return Promise.resolve( + objects.map(({ type, id }) => ({ type, id, existingDocument })) + ); + }); + }); + + it(`accepts custom originId for multi-namespace type`, async () => { + // The preflight result has `existing-originId`, but that is discarded + const objects = [ + { ...obj1, type: MULTI_NAMESPACE_TYPE, originId: 'some-originId' }, + { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE, originId: 'some-originId' }, + ]; + await bulkCreateSuccess(client, repository, objects); + const expected = expect.objectContaining({ originId: 'some-originId' }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + it(`accepts undefined originId`, async () => { + // The preflight result has `existing-originId`, but that is discarded + const objects = [ + { ...obj1, type: MULTI_NAMESPACE_TYPE, originId: undefined }, + { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE, originId: undefined }, + ]; + await bulkCreateSuccess(client, repository, objects); + const expected = expect.not.objectContaining({ originId: expect.anything() }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + it(`preserves existing originId if originId option is not set`, async () => { + const objects = [ + { ...obj1, type: MULTI_NAMESPACE_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, + ]; + await bulkCreateSuccess(client, repository, objects); + const expected = expect.objectContaining({ originId: 'existing-originId' }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + }); + }); + + it(`adds namespace to request body for any types that are single-namespace`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace }); + const expected = expect.objectContaining({ namespace }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + // this only ensures we don't override any other options + it(`adds managed=false to request body if declared for any types that are single-namespace`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace, managed: false }); + const expected = expect.objectContaining({ namespace, managed: false }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + // this only ensures we don't override any other options + it(`adds managed=true to request body if declared for any types that are single-namespace`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace, managed: true }); + const expected = expect.objectContaining({ namespace, managed: true }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace: 'default' }); + const expected = expect.not.objectContaining({ namespace: 'default' }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + it(`doesn't add namespace to request body for any types that are not single-namespace`, async () => { + const objects = [ + { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, + ]; + await bulkCreateSuccess(client, repository, objects, { namespace }); + const expected = expect.not.objectContaining({ namespace: expect.anything() }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + + it(`adds namespaces to request body for any types that are multi-namespace`, async () => { + const test = async (namespace?: string) => { + const objects = [obj1, obj2].map((x) => ({ ...x, type: MULTI_NAMESPACE_ISOLATED_TYPE })); + const [o1, o2] = objects; + mockPreflightCheckForCreate.mockResolvedValueOnce([ + { type: o1.type, id: o1.id! }, // first object does not have an existing document to overwrite + { + type: o2.type, + id: o2.id!, + existingDocument: { _id: o2.id!, _source: { namespaces: ['*'], type: o2.type } }, // second object does have an existing document to overwrite + }, + ]); + await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true }); + const expected1 = expect.objectContaining({ namespaces: [namespace ?? 'default'] }); + const expected2 = expect.objectContaining({ namespaces: ['*'] }); + const body = [expect.any(Object), expected1, expect.any(Object), expected2]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); + mockPreflightCheckForCreate.mockReset(); + }; + await test(undefined); + await test(namespace); + }); + + it(`adds initialNamespaces instead of namespace`, async () => { + const test = async (namespace?: string) => { + const ns2 = 'bar-namespace'; + const ns3 = 'baz-namespace'; + const objects = [ + { ...obj1, type: 'dashboard', initialNamespaces: [ns2] }, + { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, initialNamespaces: [ns2] }, + { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2, ns3] }, + ]; + const [o1, o2, o3] = objects; + mockPreflightCheckForCreate.mockResolvedValueOnce([ + // first object does not get passed in to preflightCheckForCreate at all + { type: o2.type, id: o2.id! }, // second object does not have an existing document to overwrite + { + type: o3.type, + id: o3.id!, + existingDocument: { + _id: o3.id!, + _source: { type: o3.type, namespaces: [namespace ?? 'default', 'something-else'] }, // third object does have an existing document to overwrite + }, + }, + ]); + await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true }); + const body = [ + { index: expect.objectContaining({ _id: `${ns2}:dashboard:${o1.id}` }) }, + expect.objectContaining({ namespace: ns2 }), + { + index: expect.objectContaining({ + _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${o2.id}`, + }), + }, + expect.objectContaining({ namespaces: [ns2] }), + { index: expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${o3.id}` }) }, + expect.objectContaining({ namespaces: [ns2, ns3] }), + ]; + expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( + expect.objectContaining({ + objects: [ + // assert that the initialNamespaces fields were passed into preflightCheckForCreate instead of the current namespace + { type: o2.type, id: o2.id, overwrite: true, namespaces: o2.initialNamespaces }, + { type: o3.type, id: o3.id, overwrite: true, namespaces: o3.initialNamespaces }, + ], + }) + ); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); + mockPreflightCheckForCreate.mockReset(); + }; + await test(undefined); + await test(namespace); + }); + + it(`normalizes initialNamespaces from 'default' to undefined`, async () => { + const test = async (namespace?: string) => { + const objects = [{ ...obj1, type: 'dashboard', initialNamespaces: ['default'] }]; + await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true }); + const body = [ + { index: expect.objectContaining({ _id: `dashboard:${obj1.id}` }) }, + expect.not.objectContaining({ namespace: 'default' }), + ]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); + }; + await test(undefined); + await test(namespace); + }); + + it(`doesn't add namespaces to request body for any types that are not multi-namespace`, async () => { + const test = async (namespace?: string) => { + const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; + await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true }); + const expected = expect.not.objectContaining({ namespaces: expect.anything() }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + client.bulk.mockClear(); + }; + await test(undefined); + await test(namespace); + }); + + it(`defaults to a refresh setting of wait_for`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2]); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ refresh: 'wait_for' }), + expect.anything() + ); + }); + + it(`should use default index`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2]); + expectClientCallArgsAction([obj1, obj2], { + method: 'create', + _index: '.kibana-test_8.0.0-testing', + }); + }); + + it(`should use custom index`, async () => { + await bulkCreateSuccess( + client, + repository, + [obj1, obj2].map((x) => ({ ...x, type: CUSTOM_INDEX_TYPE })) + ); + expectClientCallArgsAction([obj1, obj2], { + method: 'create', + _index: 'custom_8.0.0-testing', + }); + }); + + it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { + const getId = (type: string, id: string = '') => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) + await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace }); + expectClientCallArgsAction([obj1, obj2], { method: 'create', getId }); + }); + + it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { + const getId = (type: string, id: string = '') => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) + await bulkCreateSuccess(client, repository, [obj1, obj2]); + expectClientCallArgsAction([obj1, obj2], { method: 'create', getId }); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + const getId = (type: string, id: string = '') => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) + const objects = [ + { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, + ]; + await bulkCreateSuccess(client, repository, objects, { namespace }); + expectClientCallArgsAction(objects, { method: 'create', getId }); + }); + }); + + describe('errors', () => { + afterEach(() => { + mockGetBulkOperationError.mockReset(); + }); + + const obj3 = { + type: 'dashboard', + id: 'three', + attributes: { title: 'Test Three' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + }; + + const bulkCreateError = async ( + obj: SavedObjectsBulkCreateObject, + isBulkError: boolean | undefined, + expectedErrorResult: ExpectedErrorResult + ) => { + let response; + if (isBulkError) { + // mock the bulk error for only the second object + mockGetBulkOperationError.mockReturnValueOnce(undefined); + mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error as Payload); + response = getMockBulkCreateResponse([obj1, obj, obj2]); + } else { + response = getMockBulkCreateResponse([obj1, obj2]); + } + client.bulk.mockResponseOnce(response); + + const objects = [obj1, obj, obj2]; + const result = await repository.bulkCreate(objects); + expect(client.bulk).toHaveBeenCalled(); + const objCall = isBulkError ? expectObjArgs(obj) : []; + const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + expect(result).toEqual({ + saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)], + }); + }; + + it(`throws when options.namespace is '*'`, async () => { + await expect( + repository.bulkCreate([obj3], { namespace: ALL_NAMESPACES_STRING }) + ).rejects.toThrowError(createBadRequestErrorPayload('"options.namespace" cannot be "*"')); + }); + + it(`returns error when initialNamespaces is used with a space-agnostic object`, async () => { + const obj = { ...obj3, type: NAMESPACE_AGNOSTIC_TYPE, initialNamespaces: [] }; + await bulkCreateError( + obj, + undefined, + expectErrorResult( + obj, + createBadRequestErrorPayload( + '"initialNamespaces" cannot be used on space-agnostic types' + ) + ) + ); + }); + + it(`returns error when initialNamespaces is empty`, async () => { + const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [] }; + await bulkCreateError( + obj, + undefined, + expectErrorResult( + obj, + createBadRequestErrorPayload('"initialNamespaces" must be a non-empty array of strings') + ) + ); + }); + + it(`returns error when initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => { + const doTest = async (objType: string, initialNamespaces: string[]) => { + const obj = { ...obj3, type: objType, initialNamespaces }; + await bulkCreateError( + obj, + undefined, + expectErrorResult( + obj, + createBadRequestErrorPayload( + '"initialNamespaces" can only specify a single space when used with space-isolated types' + ) + ) + ); + }; + await doTest('dashboard', ['spacex', 'spacey']); + await doTest('dashboard', ['*']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']); + }); + + it(`returns error when type is invalid`, async () => { + const obj = { ...obj3, type: 'unknownType' }; + await bulkCreateError(obj, undefined, expectErrorInvalidType(obj)); + }); + + it(`returns error when type is hidden`, async () => { + const obj = { ...obj3, type: HIDDEN_TYPE }; + await bulkCreateError(obj, undefined, expectErrorInvalidType(obj)); + }); + + it(`returns error when there is a conflict from preflightCheckForCreate`, async () => { + const objects = [ + // only the second, third, and fourth objects are passed to preflightCheckForCreate and result in errors + obj1, + { ...obj1, type: MULTI_NAMESPACE_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_TYPE }, + { ...obj3, type: MULTI_NAMESPACE_TYPE }, + obj2, + ]; + const [o1, o2, o3, o4, o5] = objects; + mockPreflightCheckForCreate.mockResolvedValueOnce([ + // first and last objects do not get passed in to preflightCheckForCreate at all + { type: o2.type, id: o2.id!, error: { type: 'conflict' } }, + { + type: o3.type, + id: o3.id!, + error: { type: 'unresolvableConflict', metadata: { isNotOverwritable: true } }, + }, + { + type: o4.type, + id: o4.id!, + error: { type: 'aliasConflict', metadata: { spacesWithConflictingAliases: ['foo'] } }, + }, + ]); + const bulkResponse = getMockBulkCreateResponse([o1, o5]); + client.bulk.mockResponseOnce(bulkResponse); + + const options = { overwrite: true }; + const result = await repository.bulkCreate(objects, options); + expect(mockPreflightCheckForCreate).toHaveBeenCalled(); + expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( + expect.objectContaining({ + objects: [ + { type: o2.type, id: o2.id, overwrite: true, namespaces: ['default'] }, + { type: o3.type, id: o3.id, overwrite: true, namespaces: ['default'] }, + { type: o4.type, id: o4.id, overwrite: true, namespaces: ['default'] }, + ], + }) + ); + expect(client.bulk).toHaveBeenCalled(); + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body: [...expectObjArgs(o1), ...expectObjArgs(o5)] }), + expect.anything() + ); + expect(result).toEqual({ + saved_objects: [ + expectSuccess(o1), + expectErrorConflict(o2), + expectErrorConflict(o3, { metadata: { isNotOverwritable: true } }), + expectErrorConflict(o4, { metadata: { spacesWithConflictingAliases: ['foo'] } }), + expectSuccess(o5), + ], + }); + }); + + it(`returns bulk error`, async () => { + const expectedErrorResult = { + type: obj3.type, + id: obj3.id, + error: { error: 'Oh no, a bulk error!' }, + }; + await bulkCreateError(obj3, true, expectedErrorResult); + }); + + it(`returns errors for any bulk objects with invalid schemas`, async () => { + const response = getMockBulkCreateResponse([obj3]); + client.bulk.mockResponseOnce(response); + + const result = await repository.bulkCreate([ + obj3, + // @ts-expect-error - Title should be a string and is intentionally malformed for testing + { ...obj3, id: 'three-again', attributes: { title: 123 } }, + ]); + expect(client.bulk).toHaveBeenCalledTimes(1); // only called once for the valid object + expect(result.saved_objects).toEqual([ + expect.objectContaining(obj3), + expect.objectContaining({ + error: new Error( + '[attributes.title]: expected value of type [string] but got [number]: Bad Request' + ), + id: 'three-again', + type: 'dashboard', + }), + ]); + }); + }); + + describe('migration', () => { + it(`migrates the docs and serializes the migrated docs`, async () => { + migrator.migrateDocument.mockImplementation(mockMigrateDocument); + const modifiedObj1 = { ...obj1, coreMigrationVersion: '8.0.0' }; + await bulkCreateSuccess(client, repository, [modifiedObj1, obj2]); + const docs = [modifiedObj1, obj2].map((x) => ({ ...x, ...mockTimestampFieldsWithCreated })); + expectMigrationArgs(docs[0], true, 1); + expectMigrationArgs(docs[1], true, 2); + + const migratedDocs = docs.map((x) => migrator.migrateDocument(x)); + expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(1, migratedDocs[0]); + expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(2, migratedDocs[1]); + }); + + it(`adds namespace to body when providing namespace for single-namespace type`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace }); + expectMigrationArgs({ namespace }, true, 1); + expectMigrationArgs({ namespace }, true, 2); + }); + + it(`doesn't add namespace to body when providing no namespace for single-namespace type`, async () => { + await bulkCreateSuccess(client, repository, [obj1, obj2]); + expectMigrationArgs({ namespace: expect.anything() }, false, 1); + expectMigrationArgs({ namespace: expect.anything() }, false, 2); + }); + + it(`doesn't add namespace to body when not using single-namespace type`, async () => { + const objects = [ + { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, + { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, + ]; + await bulkCreateSuccess(client, repository, objects, { namespace }); + expectMigrationArgs({ namespace: expect.anything() }, false, 1); + expectMigrationArgs({ namespace: expect.anything() }, false, 2); + }); + + it(`adds namespaces to body when providing namespace for multi-namespace type`, async () => { + const objects = [obj1, obj2].map((obj) => ({ + ...obj, + type: MULTI_NAMESPACE_ISOLATED_TYPE, + })); + await bulkCreateSuccess(client, repository, objects, { namespace }); + expectMigrationArgs({ namespaces: [namespace] }, true, 1); + expectMigrationArgs({ namespaces: [namespace] }, true, 2); + }); + + it(`adds default namespaces to body when providing no namespace for multi-namespace type`, async () => { + const objects = [obj1, obj2].map((obj) => ({ + ...obj, + type: MULTI_NAMESPACE_ISOLATED_TYPE, + })); + await bulkCreateSuccess(client, repository, objects); + expectMigrationArgs({ namespaces: ['default'] }, true, 1); + expectMigrationArgs({ namespaces: ['default'] }, true, 2); + }); + + it(`doesn't add namespaces to body when not using multi-namespace type`, async () => { + const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; + await bulkCreateSuccess(client, repository, objects); + expectMigrationArgs({ namespaces: expect.anything() }, false, 1); + expectMigrationArgs({ namespaces: expect.anything() }, false, 2); + }); + }); + + describe('returns', () => { + it(`formats the ES response`, async () => { + const result = await bulkCreateSuccess(client, repository, [obj1, obj2]); + expect(result).toEqual({ + saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)), + }); + }); + + it.todo(`should return objects in the same order regardless of type`); + + it(`handles a mix of successful creates and errors`, async () => { + const obj = { + type: 'unknownType', + id: 'three', + attributes: {}, + }; + const objects = [obj1, obj, obj2]; + const response = getMockBulkCreateResponse([obj1, obj2]); + client.bulk.mockResponseOnce(response); + const result = await repository.bulkCreate(objects); + expect(client.bulk).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + saved_objects: [expectCreateResult(obj1), expectError(obj), expectCreateResult(obj2)], + }); + }); + + it(`a deserialized saved object`, async () => { + // Test for fix to https://github.com/elastic/kibana/issues/65088 where + // we returned raw ID's when an object without an id was created. + const namespace = 'myspace'; + // FIXME: this test is based on a gigantic hack to have the bulk operation return the source + // of the document when it actually does not, forcing to cast to any as BulkResponse + // does not contains _source + const response = getMockBulkCreateResponse([obj1, obj2], namespace) as any; + client.bulk.mockResponseOnce(response); + + // Bulk create one object with id unspecified, and one with id specified + const result = await repository.bulkCreate([{ ...obj1, id: undefined }, obj2], { + namespace, + }); + + // Assert that both raw docs from the ES response are deserialized + expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith( + 1, + { + ...response.items[0].create, + _source: { + ...response.items[0].create._source, + namespaces: response.items[0].create._source.namespaces, + coreMigrationVersion: expect.any(String), + typeMigrationVersion: '1.1.1', + }, + _id: expect.stringMatching( + /^myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/ + ), + }, + expect.any(Object) + ); + expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith( + 2, + { + ...response.items[1].create, + _source: { + ...response.items[1].create._source, + namespaces: response.items[1].create._source.namespaces, + coreMigrationVersion: expect.any(String), + typeMigrationVersion: '1.1.1', + }, + }, + expect.any(Object) + ); + + // Assert that ID's are deserialized to remove the type and namespace + expect(result.saved_objects[0].id).toEqual( + expect.stringMatching(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/) + ); + expect(result.saved_objects[1].id).toEqual(obj2.id); + + // Assert that managed is not changed + expect(result.saved_objects[0].managed).toBeFalsy(); + expect(result.saved_objects[1].managed).toEqual(obj2.managed); + }); + + it(`sets managed=false if not already set`, async () => { + const obj1WithoutManaged = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + references: [{ name: 'ref_0', type: 'test', id: '1' }], + }; + const obj2WithoutManaged = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Test Two' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + }; + const result = await bulkCreateSuccess(client, repository, [ + obj1WithoutManaged, + obj2WithoutManaged, + ]); + expect(result).toEqual({ + saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)), + }); + }); + + it(`sets managed=false only on documents without managed already set`, async () => { + const objWithoutManaged = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + references: [{ name: 'ref_0', type: 'test', id: '1' }], + }; + const result = await bulkCreateSuccess(client, repository, [objWithoutManaged, obj2]); + expect(result).toEqual({ + saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)), + }); + }); + + it(`sets managed=true if provided as an override`, async () => { + const obj1WithoutManaged = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + references: [{ name: 'ref_0', type: 'test', id: '1' }], + }; + const obj2WithoutManaged = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Test Two' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + }; + const result = await bulkCreateSuccess( + client, + repository, + [obj1WithoutManaged, obj2WithoutManaged], + { managed: true } + ); + expect(result).toEqual({ + saved_objects: [ + { ...obj1WithoutManaged, managed: true }, + { ...obj2WithoutManaged, managed: true }, + ].map((x) => expectCreateResult(x)), + }); + }); + + it(`sets managed=false if provided as an override`, async () => { + const obj1WithoutManaged = { + type: 'config', + id: '6.0.0-alpha1', + attributes: { title: 'Test One' }, + references: [{ name: 'ref_0', type: 'test', id: '1' }], + }; + const obj2WithoutManaged = { + type: 'index-pattern', + id: 'logstash-*', + attributes: { title: 'Test Two' }, + references: [{ name: 'ref_0', type: 'test', id: '2' }], + }; + const result = await bulkCreateSuccess( + client, + repository, + [obj1WithoutManaged, obj2WithoutManaged], + { managed: false } + ); + expect(result).toEqual({ + saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)), + }); + }); + }); + }); +}); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts index 1084ad3e58966..3547d653e3de4 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts @@ -32,7 +32,6 @@ import type { SavedObjectsIncrementCounterOptions, SavedObjectsCreatePointInTimeFinderDependencies, SavedObjectsCreatePointInTimeFinderOptions, - SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, SavedObjectsCreateOptions, SavedObjectsDeleteOptions, @@ -86,13 +85,10 @@ import { getMockMgetResponse, type TypeIdTuple, createSpySerializer, - bulkCreateSuccess, - getMockBulkCreateResponse, bulkGet, expectErrorResult, expectErrorInvalidType, expectErrorNotFound, - expectErrorConflict, expectError, generateIndexPatternSearchResults, findSuccess, @@ -105,7 +101,6 @@ import { createUnsupportedTypeErrorPayload, createConflictErrorPayload, createGenericNotFoundErrorPayload, - expectCreateResult, mockTimestampFieldsWithCreated, getMockEsBulkDeleteResponse, bulkDeleteSuccess, @@ -193,917 +188,6 @@ describe('SavedObjectsRepository', () => { references: [{ name: 'search_0', type: 'search', id: '123' }], }); - describe('#bulkCreate', () => { - beforeEach(() => { - mockPreflightCheckForCreate.mockReset(); - mockPreflightCheckForCreate.mockImplementation(({ objects }) => { - return Promise.resolve(objects.map(({ type, id }) => ({ type, id }))); // respond with no errors by default - }); - }); - - const obj1 = { - type: 'config', - id: '6.0.0-alpha1', - attributes: { title: 'Test One' }, - references: [{ name: 'ref_0', type: 'test', id: '1' }], - managed: false, - }; - const obj2 = { - type: 'index-pattern', - id: 'logstash-*', - attributes: { title: 'Test Two' }, - references: [{ name: 'ref_0', type: 'test', id: '2' }], - managed: false, - }; - const namespace = 'foo-namespace'; - - // bulk create calls have two objects for each source -- the action, and the source - const expectClientCallArgsAction = ( - objects: Array<{ type: string; id?: string; if_primary_term?: string; if_seq_no?: string }>, - { - method, - _index = expect.any(String), - getId = () => expect.any(String), - }: { method: string; _index?: string; getId?: (type: string, id?: string) => string } - ) => { - const body = []; - for (const { type, id, if_primary_term: ifPrimaryTerm, if_seq_no: ifSeqNo } of objects) { - body.push({ - [method]: { - _index, - _id: getId(type, id), - ...(ifPrimaryTerm && ifSeqNo - ? { if_primary_term: expect.any(Number), if_seq_no: expect.any(Number) } - : {}), - }, - }); - body.push(expect.any(Object)); - } - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }; - - const expectObjArgs = ( - { - type, - attributes, - references, - }: { type: string; attributes: unknown; references?: SavedObjectReference[] }, - overrides: Record = {} - ) => [ - expect.any(Object), - expect.objectContaining({ - [type]: attributes, - references, - type, - ...overrides, - ...mockTimestampFields, - }), - ]; - describe('client calls', () => { - it(`should use the ES bulk action by default`, async () => { - await bulkCreateSuccess(client, repository, [obj1, obj2]); - expect(client.bulk).toHaveBeenCalledTimes(1); - }); - - it(`should use the preflightCheckForCreate action before bulk action for any types that are multi-namespace, when id is defined`, async () => { - const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }]; - await bulkCreateSuccess(client, repository, objects); - expect(client.bulk).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1); - expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( - expect.objectContaining({ - objects: [ - { - type: MULTI_NAMESPACE_ISOLATED_TYPE, - id: obj2.id, - overwrite: false, - namespaces: ['default'], - }, - ], - }) - ); - }); - - it(`should use the ES create method if ID is undefined and overwrite=true`, async () => { - const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined })); - await bulkCreateSuccess(client, repository, objects, { overwrite: true }); - expectClientCallArgsAction(objects, { method: 'create' }); - }); - - it(`should use the ES create method if ID is undefined and overwrite=false`, async () => { - const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined })); - await bulkCreateSuccess(client, repository, objects); - expectClientCallArgsAction(objects, { method: 'create' }); - }); - - it(`should use the ES index method if ID is defined and overwrite=true`, async () => { - await bulkCreateSuccess(client, repository, [obj1, obj2], { overwrite: true }); - expectClientCallArgsAction([obj1, obj2], { method: 'index' }); - }); - - it(`should use the ES index method with version if ID and version are defined and overwrite=true`, async () => { - await bulkCreateSuccess( - client, - repository, - [ - { - ...obj1, - version: mockVersion, - }, - obj2, - ], - { overwrite: true } - ); - - const obj1WithSeq = { - ...obj1, - managed: obj1.managed, - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - - expectClientCallArgsAction([obj1WithSeq, obj2], { method: 'index' }); - }); - - it(`should use the ES create method if ID is defined and overwrite=false`, async () => { - await bulkCreateSuccess(client, repository, [obj1, obj2]); - expectClientCallArgsAction([obj1, obj2], { method: 'create' }); - }); - - it(`should use the ES index method if ID is defined, overwrite=true and managed=true in a document`, async () => { - await bulkCreateSuccess(client, repository, [obj1, obj2], { - overwrite: true, - managed: true, - }); - expectClientCallArgsAction([obj1, obj2], { method: 'index' }); - }); - - it(`should use the ES create method if ID is defined, overwrite=false and managed=true in a document`, async () => { - await bulkCreateSuccess(client, repository, [obj1, obj2], { managed: true }); - expectClientCallArgsAction([obj1, obj2], { method: 'create' }); - }); - - it(`formats the ES request`, async () => { - await bulkCreateSuccess(client, repository, [obj1, obj2]); - const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - // this test only ensures that the client accepts the managed field in a document - it(`formats the ES request with managed=true in a document`, async () => { - const obj1WithManagedTrue = { ...obj1, managed: true }; - const obj2WithManagedTrue = { ...obj2, managed: true }; - await bulkCreateSuccess(client, repository, [obj1WithManagedTrue, obj2WithManagedTrue]); - const body = [...expectObjArgs(obj1WithManagedTrue), ...expectObjArgs(obj2WithManagedTrue)]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - describe('originId', () => { - it(`returns error if originId is set for non-multi-namespace type`, async () => { - const result = await repository.bulkCreate([ - { ...obj1, originId: 'some-originId' }, - { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE, originId: 'some-originId' }, - ]); - expect(result.saved_objects).toEqual([ - expect.objectContaining({ id: obj1.id, type: obj1.type, error: expect.anything() }), - expect.objectContaining({ - id: obj2.id, - type: NAMESPACE_AGNOSTIC_TYPE, - error: expect.anything(), - }), - ]); - expect(client.bulk).not.toHaveBeenCalled(); - }); - - it(`defaults to no originId`, async () => { - const objects = [ - { ...obj1, type: MULTI_NAMESPACE_TYPE }, - { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, - ]; - - await bulkCreateSuccess(client, repository, objects); - const expected = expect.not.objectContaining({ originId: expect.anything() }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - describe('with existing originId', () => { - beforeEach(() => { - mockPreflightCheckForCreate.mockImplementation(({ objects }) => { - const existingDocument = { - _source: { originId: 'existing-originId' }, - } as SavedObjectsRawDoc; - return Promise.resolve( - objects.map(({ type, id }) => ({ type, id, existingDocument })) - ); - }); - }); - - it(`accepts custom originId for multi-namespace type`, async () => { - // The preflight result has `existing-originId`, but that is discarded - const objects = [ - { ...obj1, type: MULTI_NAMESPACE_TYPE, originId: 'some-originId' }, - { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE, originId: 'some-originId' }, - ]; - await bulkCreateSuccess(client, repository, objects); - const expected = expect.objectContaining({ originId: 'some-originId' }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - it(`accepts undefined originId`, async () => { - // The preflight result has `existing-originId`, but that is discarded - const objects = [ - { ...obj1, type: MULTI_NAMESPACE_TYPE, originId: undefined }, - { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE, originId: undefined }, - ]; - await bulkCreateSuccess(client, repository, objects); - const expected = expect.not.objectContaining({ originId: expect.anything() }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - it(`preserves existing originId if originId option is not set`, async () => { - const objects = [ - { ...obj1, type: MULTI_NAMESPACE_TYPE }, - { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, - ]; - await bulkCreateSuccess(client, repository, objects); - const expected = expect.objectContaining({ originId: 'existing-originId' }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - }); - }); - - it(`adds namespace to request body for any types that are single-namespace`, async () => { - await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace }); - const expected = expect.objectContaining({ namespace }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - // this only ensures we don't override any other options - it(`adds managed=false to request body if declared for any types that are single-namespace`, async () => { - await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace, managed: false }); - const expected = expect.objectContaining({ namespace, managed: false }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - // this only ensures we don't override any other options - it(`adds managed=true to request body if declared for any types that are single-namespace`, async () => { - await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace, managed: true }); - const expected = expect.objectContaining({ namespace, managed: true }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - it(`normalizes options.namespace from 'default' to undefined`, async () => { - await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace: 'default' }); - const expected = expect.not.objectContaining({ namespace: 'default' }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - it(`doesn't add namespace to request body for any types that are not single-namespace`, async () => { - const objects = [ - { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, - { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, - ]; - await bulkCreateSuccess(client, repository, objects, { namespace }); - const expected = expect.not.objectContaining({ namespace: expect.anything() }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - }); - - it(`adds namespaces to request body for any types that are multi-namespace`, async () => { - const test = async (namespace?: string) => { - const objects = [obj1, obj2].map((x) => ({ ...x, type: MULTI_NAMESPACE_ISOLATED_TYPE })); - const [o1, o2] = objects; - mockPreflightCheckForCreate.mockResolvedValueOnce([ - { type: o1.type, id: o1.id! }, // first object does not have an existing document to overwrite - { - type: o2.type, - id: o2.id!, - existingDocument: { _id: o2.id!, _source: { namespaces: ['*'], type: o2.type } }, // second object does have an existing document to overwrite - }, - ]); - await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true }); - const expected1 = expect.objectContaining({ namespaces: [namespace ?? 'default'] }); - const expected2 = expect.objectContaining({ namespaces: ['*'] }); - const body = [expect.any(Object), expected1, expect.any(Object), expected2]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - client.bulk.mockClear(); - mockPreflightCheckForCreate.mockReset(); - }; - await test(undefined); - await test(namespace); - }); - - it(`adds initialNamespaces instead of namespace`, async () => { - const test = async (namespace?: string) => { - const ns2 = 'bar-namespace'; - const ns3 = 'baz-namespace'; - const objects = [ - { ...obj1, type: 'dashboard', initialNamespaces: [ns2] }, - { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, initialNamespaces: [ns2] }, - { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2, ns3] }, - ]; - const [o1, o2, o3] = objects; - mockPreflightCheckForCreate.mockResolvedValueOnce([ - // first object does not get passed in to preflightCheckForCreate at all - { type: o2.type, id: o2.id! }, // second object does not have an existing document to overwrite - { - type: o3.type, - id: o3.id!, - existingDocument: { - _id: o3.id!, - _source: { type: o3.type, namespaces: [namespace ?? 'default', 'something-else'] }, // third object does have an existing document to overwrite - }, - }, - ]); - await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true }); - const body = [ - { index: expect.objectContaining({ _id: `${ns2}:dashboard:${o1.id}` }) }, - expect.objectContaining({ namespace: ns2 }), - { - index: expect.objectContaining({ - _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${o2.id}`, - }), - }, - expect.objectContaining({ namespaces: [ns2] }), - { index: expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${o3.id}` }) }, - expect.objectContaining({ namespaces: [ns2, ns3] }), - ]; - expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( - expect.objectContaining({ - objects: [ - // assert that the initialNamespaces fields were passed into preflightCheckForCreate instead of the current namespace - { type: o2.type, id: o2.id, overwrite: true, namespaces: o2.initialNamespaces }, - { type: o3.type, id: o3.id, overwrite: true, namespaces: o3.initialNamespaces }, - ], - }) - ); - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - client.bulk.mockClear(); - mockPreflightCheckForCreate.mockReset(); - }; - await test(undefined); - await test(namespace); - }); - - it(`normalizes initialNamespaces from 'default' to undefined`, async () => { - const test = async (namespace?: string) => { - const objects = [{ ...obj1, type: 'dashboard', initialNamespaces: ['default'] }]; - await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true }); - const body = [ - { index: expect.objectContaining({ _id: `dashboard:${obj1.id}` }) }, - expect.not.objectContaining({ namespace: 'default' }), - ]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - client.bulk.mockClear(); - }; - await test(undefined); - await test(namespace); - }); - - it(`doesn't add namespaces to request body for any types that are not multi-namespace`, async () => { - const test = async (namespace?: string) => { - const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; - await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true }); - const expected = expect.not.objectContaining({ namespaces: expect.anything() }); - const body = [expect.any(Object), expected, expect.any(Object), expected]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - client.bulk.mockClear(); - }; - await test(undefined); - await test(namespace); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await bulkCreateSuccess(client, repository, [obj1, obj2]); - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ refresh: 'wait_for' }), - expect.anything() - ); - }); - - it(`should use default index`, async () => { - await bulkCreateSuccess(client, repository, [obj1, obj2]); - expectClientCallArgsAction([obj1, obj2], { - method: 'create', - _index: '.kibana-test_8.0.0-testing', - }); - }); - - it(`should use custom index`, async () => { - await bulkCreateSuccess( - client, - repository, - [obj1, obj2].map((x) => ({ ...x, type: CUSTOM_INDEX_TYPE })) - ); - expectClientCallArgsAction([obj1, obj2], { - method: 'create', - _index: 'custom_8.0.0-testing', - }); - }); - - it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - const getId = (type: string, id: string = '') => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) - await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace }); - expectClientCallArgsAction([obj1, obj2], { method: 'create', getId }); - }); - - it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - const getId = (type: string, id: string = '') => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) - await bulkCreateSuccess(client, repository, [obj1, obj2]); - expectClientCallArgsAction([obj1, obj2], { method: 'create', getId }); - }); - - it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - const getId = (type: string, id: string = '') => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) - const objects = [ - { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, - { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, - ]; - await bulkCreateSuccess(client, repository, objects, { namespace }); - expectClientCallArgsAction(objects, { method: 'create', getId }); - }); - }); - - describe('errors', () => { - afterEach(() => { - mockGetBulkOperationError.mockReset(); - }); - - const obj3 = { - type: 'dashboard', - id: 'three', - attributes: { title: 'Test Three' }, - references: [{ name: 'ref_0', type: 'test', id: '2' }], - }; - - const bulkCreateError = async ( - obj: SavedObjectsBulkCreateObject, - isBulkError: boolean | undefined, - expectedErrorResult: ExpectedErrorResult - ) => { - let response; - if (isBulkError) { - // mock the bulk error for only the second object - mockGetBulkOperationError.mockReturnValueOnce(undefined); - mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error as Payload); - response = getMockBulkCreateResponse([obj1, obj, obj2]); - } else { - response = getMockBulkCreateResponse([obj1, obj2]); - } - client.bulk.mockResponseOnce(response); - - const objects = [obj1, obj, obj2]; - const result = await repository.bulkCreate(objects); - expect(client.bulk).toHaveBeenCalled(); - const objCall = isBulkError ? expectObjArgs(obj) : []; - const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body }), - expect.anything() - ); - expect(result).toEqual({ - saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)], - }); - }; - - it(`throws when options.namespace is '*'`, async () => { - await expect( - repository.bulkCreate([obj3], { namespace: ALL_NAMESPACES_STRING }) - ).rejects.toThrowError(createBadRequestErrorPayload('"options.namespace" cannot be "*"')); - }); - - it(`returns error when initialNamespaces is used with a space-agnostic object`, async () => { - const obj = { ...obj3, type: NAMESPACE_AGNOSTIC_TYPE, initialNamespaces: [] }; - await bulkCreateError( - obj, - undefined, - expectErrorResult( - obj, - createBadRequestErrorPayload( - '"initialNamespaces" cannot be used on space-agnostic types' - ) - ) - ); - }); - - it(`returns error when initialNamespaces is empty`, async () => { - const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [] }; - await bulkCreateError( - obj, - undefined, - expectErrorResult( - obj, - createBadRequestErrorPayload('"initialNamespaces" must be a non-empty array of strings') - ) - ); - }); - - it(`returns error when initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => { - const doTest = async (objType: string, initialNamespaces: string[]) => { - const obj = { ...obj3, type: objType, initialNamespaces }; - await bulkCreateError( - obj, - undefined, - expectErrorResult( - obj, - createBadRequestErrorPayload( - '"initialNamespaces" can only specify a single space when used with space-isolated types' - ) - ) - ); - }; - await doTest('dashboard', ['spacex', 'spacey']); - await doTest('dashboard', ['*']); - await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']); - await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']); - }); - - it(`returns error when type is invalid`, async () => { - const obj = { ...obj3, type: 'unknownType' }; - await bulkCreateError(obj, undefined, expectErrorInvalidType(obj)); - }); - - it(`returns error when type is hidden`, async () => { - const obj = { ...obj3, type: HIDDEN_TYPE }; - await bulkCreateError(obj, undefined, expectErrorInvalidType(obj)); - }); - - it(`returns error when there is a conflict from preflightCheckForCreate`, async () => { - const objects = [ - // only the second, third, and fourth objects are passed to preflightCheckForCreate and result in errors - obj1, - { ...obj1, type: MULTI_NAMESPACE_TYPE }, - { ...obj2, type: MULTI_NAMESPACE_TYPE }, - { ...obj3, type: MULTI_NAMESPACE_TYPE }, - obj2, - ]; - const [o1, o2, o3, o4, o5] = objects; - mockPreflightCheckForCreate.mockResolvedValueOnce([ - // first and last objects do not get passed in to preflightCheckForCreate at all - { type: o2.type, id: o2.id!, error: { type: 'conflict' } }, - { - type: o3.type, - id: o3.id!, - error: { type: 'unresolvableConflict', metadata: { isNotOverwritable: true } }, - }, - { - type: o4.type, - id: o4.id!, - error: { type: 'aliasConflict', metadata: { spacesWithConflictingAliases: ['foo'] } }, - }, - ]); - const bulkResponse = getMockBulkCreateResponse([o1, o5]); - client.bulk.mockResponseOnce(bulkResponse); - - const options = { overwrite: true }; - const result = await repository.bulkCreate(objects, options); - expect(mockPreflightCheckForCreate).toHaveBeenCalled(); - expect(mockPreflightCheckForCreate).toHaveBeenCalledWith( - expect.objectContaining({ - objects: [ - { type: o2.type, id: o2.id, overwrite: true, namespaces: ['default'] }, - { type: o3.type, id: o3.id, overwrite: true, namespaces: ['default'] }, - { type: o4.type, id: o4.id, overwrite: true, namespaces: ['default'] }, - ], - }) - ); - expect(client.bulk).toHaveBeenCalled(); - expect(client.bulk).toHaveBeenCalledWith( - expect.objectContaining({ body: [...expectObjArgs(o1), ...expectObjArgs(o5)] }), - expect.anything() - ); - expect(result).toEqual({ - saved_objects: [ - expectSuccess(o1), - expectErrorConflict(o2), - expectErrorConflict(o3, { metadata: { isNotOverwritable: true } }), - expectErrorConflict(o4, { metadata: { spacesWithConflictingAliases: ['foo'] } }), - expectSuccess(o5), - ], - }); - }); - - it(`returns bulk error`, async () => { - const expectedErrorResult = { - type: obj3.type, - id: obj3.id, - error: { error: 'Oh no, a bulk error!' }, - }; - await bulkCreateError(obj3, true, expectedErrorResult); - }); - - it(`returns errors for any bulk objects with invalid schemas`, async () => { - const response = getMockBulkCreateResponse([obj3]); - client.bulk.mockResponseOnce(response); - - const result = await repository.bulkCreate([ - obj3, - // @ts-expect-error - Title should be a string and is intentionally malformed for testing - { ...obj3, id: 'three-again', attributes: { title: 123 } }, - ]); - expect(client.bulk).toHaveBeenCalledTimes(1); // only called once for the valid object - expect(result.saved_objects).toEqual([ - expect.objectContaining(obj3), - expect.objectContaining({ - error: new Error( - '[attributes.title]: expected value of type [string] but got [number]: Bad Request' - ), - id: 'three-again', - type: 'dashboard', - }), - ]); - }); - }); - - describe('migration', () => { - it(`migrates the docs and serializes the migrated docs`, async () => { - migrator.migrateDocument.mockImplementation(mockMigrateDocument); - const modifiedObj1 = { ...obj1, coreMigrationVersion: '8.0.0' }; - await bulkCreateSuccess(client, repository, [modifiedObj1, obj2]); - const docs = [modifiedObj1, obj2].map((x) => ({ ...x, ...mockTimestampFieldsWithCreated })); - expectMigrationArgs(docs[0], true, 1); - expectMigrationArgs(docs[1], true, 2); - - const migratedDocs = docs.map((x) => migrator.migrateDocument(x)); - expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(1, migratedDocs[0]); - expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(2, migratedDocs[1]); - }); - - it(`adds namespace to body when providing namespace for single-namespace type`, async () => { - await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace }); - expectMigrationArgs({ namespace }, true, 1); - expectMigrationArgs({ namespace }, true, 2); - }); - - it(`doesn't add namespace to body when providing no namespace for single-namespace type`, async () => { - await bulkCreateSuccess(client, repository, [obj1, obj2]); - expectMigrationArgs({ namespace: expect.anything() }, false, 1); - expectMigrationArgs({ namespace: expect.anything() }, false, 2); - }); - - it(`doesn't add namespace to body when not using single-namespace type`, async () => { - const objects = [ - { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, - { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }, - ]; - await bulkCreateSuccess(client, repository, objects, { namespace }); - expectMigrationArgs({ namespace: expect.anything() }, false, 1); - expectMigrationArgs({ namespace: expect.anything() }, false, 2); - }); - - it(`adds namespaces to body when providing namespace for multi-namespace type`, async () => { - const objects = [obj1, obj2].map((obj) => ({ - ...obj, - type: MULTI_NAMESPACE_ISOLATED_TYPE, - })); - await bulkCreateSuccess(client, repository, objects, { namespace }); - expectMigrationArgs({ namespaces: [namespace] }, true, 1); - expectMigrationArgs({ namespaces: [namespace] }, true, 2); - }); - - it(`adds default namespaces to body when providing no namespace for multi-namespace type`, async () => { - const objects = [obj1, obj2].map((obj) => ({ - ...obj, - type: MULTI_NAMESPACE_ISOLATED_TYPE, - })); - await bulkCreateSuccess(client, repository, objects); - expectMigrationArgs({ namespaces: ['default'] }, true, 1); - expectMigrationArgs({ namespaces: ['default'] }, true, 2); - }); - - it(`doesn't add namespaces to body when not using multi-namespace type`, async () => { - const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }]; - await bulkCreateSuccess(client, repository, objects); - expectMigrationArgs({ namespaces: expect.anything() }, false, 1); - expectMigrationArgs({ namespaces: expect.anything() }, false, 2); - }); - }); - - describe('returns', () => { - it(`formats the ES response`, async () => { - const result = await bulkCreateSuccess(client, repository, [obj1, obj2]); - expect(result).toEqual({ - saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)), - }); - }); - - it.todo(`should return objects in the same order regardless of type`); - - it(`handles a mix of successful creates and errors`, async () => { - const obj = { - type: 'unknownType', - id: 'three', - attributes: {}, - }; - const objects = [obj1, obj, obj2]; - const response = getMockBulkCreateResponse([obj1, obj2]); - client.bulk.mockResponseOnce(response); - const result = await repository.bulkCreate(objects); - expect(client.bulk).toHaveBeenCalledTimes(1); - expect(result).toEqual({ - saved_objects: [expectCreateResult(obj1), expectError(obj), expectCreateResult(obj2)], - }); - }); - - it(`a deserialized saved object`, async () => { - // Test for fix to https://github.com/elastic/kibana/issues/65088 where - // we returned raw ID's when an object without an id was created. - const namespace = 'myspace'; - // FIXME: this test is based on a gigantic hack to have the bulk operation return the source - // of the document when it actually does not, forcing to cast to any as BulkResponse - // does not contains _source - const response = getMockBulkCreateResponse([obj1, obj2], namespace) as any; - client.bulk.mockResponseOnce(response); - - // Bulk create one object with id unspecified, and one with id specified - const result = await repository.bulkCreate([{ ...obj1, id: undefined }, obj2], { - namespace, - }); - - // Assert that both raw docs from the ES response are deserialized - expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith( - 1, - { - ...response.items[0].create, - _source: { - ...response.items[0].create._source, - namespaces: response.items[0].create._source.namespaces, - coreMigrationVersion: expect.any(String), - typeMigrationVersion: '1.1.1', - }, - _id: expect.stringMatching( - /^myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/ - ), - }, - expect.any(Object) - ); - expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith( - 2, - { - ...response.items[1].create, - _source: { - ...response.items[1].create._source, - namespaces: response.items[1].create._source.namespaces, - coreMigrationVersion: expect.any(String), - typeMigrationVersion: '1.1.1', - }, - }, - expect.any(Object) - ); - - // Assert that ID's are deserialized to remove the type and namespace - expect(result.saved_objects[0].id).toEqual( - expect.stringMatching(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/) - ); - expect(result.saved_objects[1].id).toEqual(obj2.id); - - // Assert that managed is not changed - expect(result.saved_objects[0].managed).toBeFalsy(); - expect(result.saved_objects[1].managed).toEqual(obj2.managed); - }); - - it(`sets managed=false if not already set`, async () => { - const obj1WithoutManaged = { - type: 'config', - id: '6.0.0-alpha1', - attributes: { title: 'Test One' }, - references: [{ name: 'ref_0', type: 'test', id: '1' }], - }; - const obj2WithoutManaged = { - type: 'index-pattern', - id: 'logstash-*', - attributes: { title: 'Test Two' }, - references: [{ name: 'ref_0', type: 'test', id: '2' }], - }; - const result = await bulkCreateSuccess(client, repository, [ - obj1WithoutManaged, - obj2WithoutManaged, - ]); - expect(result).toEqual({ - saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)), - }); - }); - - it(`sets managed=false only on documents without managed already set`, async () => { - const objWithoutManaged = { - type: 'config', - id: '6.0.0-alpha1', - attributes: { title: 'Test One' }, - references: [{ name: 'ref_0', type: 'test', id: '1' }], - }; - const result = await bulkCreateSuccess(client, repository, [objWithoutManaged, obj2]); - expect(result).toEqual({ - saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)), - }); - }); - - it(`sets managed=true if provided as an override`, async () => { - const obj1WithoutManaged = { - type: 'config', - id: '6.0.0-alpha1', - attributes: { title: 'Test One' }, - references: [{ name: 'ref_0', type: 'test', id: '1' }], - }; - const obj2WithoutManaged = { - type: 'index-pattern', - id: 'logstash-*', - attributes: { title: 'Test Two' }, - references: [{ name: 'ref_0', type: 'test', id: '2' }], - }; - const result = await bulkCreateSuccess( - client, - repository, - [obj1WithoutManaged, obj2WithoutManaged], - { managed: true } - ); - expect(result).toEqual({ - saved_objects: [ - { ...obj1WithoutManaged, managed: true }, - { ...obj2WithoutManaged, managed: true }, - ].map((x) => expectCreateResult(x)), - }); - }); - - it(`sets managed=false if provided as an override`, async () => { - const obj1WithoutManaged = { - type: 'config', - id: '6.0.0-alpha1', - attributes: { title: 'Test One' }, - references: [{ name: 'ref_0', type: 'test', id: '1' }], - }; - const obj2WithoutManaged = { - type: 'index-pattern', - id: 'logstash-*', - attributes: { title: 'Test Two' }, - references: [{ name: 'ref_0', type: 'test', id: '2' }], - }; - const result = await bulkCreateSuccess( - client, - repository, - [obj1WithoutManaged, obj2WithoutManaged], - { managed: false } - ); - expect(result).toEqual({ - saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)), - }); - }); - }); - }); - describe('#bulkGet', () => { const obj1: SavedObject = { type: 'config', diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts index 7434f46ca35be..79f964f8c4271 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts @@ -40,7 +40,12 @@ export default function ({ getService }: FtrProviderContext) { body: Record | undefined; } - describe('When attempting to call an endpoint api', function () { + // Flaky: + // https://github.com/elastic/kibana/issues/171655 + // https://github.com/elastic/kibana/issues/171656 + // https://github.com/elastic/kibana/issues/171647 + // https://github.com/elastic/kibana/issues/171648 + describe.skip('When attempting to call an endpoint api', function () { targetTags(this, ['@ess', '@serverless']); let indexedData: IndexedHostsAndAlertsResponse; diff --git a/yarn.lock b/yarn.lock index 8e9c85066ff03..84ac80b1f08a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8390,10 +8390,10 @@ "@tanstack/query-core" "4.29.11" use-sync-external-store "^1.2.0" -"@testim/chrome-version@^1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.3.tgz#fbb68696899d7b8c1b9b891eded9c04fe2cd5529" - integrity sha512-g697J3WxV/Zytemz8aTuKjTGYtta9+02kva3C1xc7KXB8GdbfE1akGJIsZLyY/FSh2QrnE+fiB7vmWU3XNcb6A== +"@testim/chrome-version@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.4.tgz#86e04e677cd6c05fa230dd15ac223fa72d1d7090" + integrity sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g== "@testing-library/dom@^8.0.0": version "8.19.0" @@ -11578,7 +11578,7 @@ axios@^0.26.0: dependencies: follow-redirects "^1.14.8" -axios@^1.3.4, axios@^1.4.0, axios@^1.6.0: +axios@^1.3.4, axios@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102" integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg== @@ -12850,18 +12850,18 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^119.0.0: - version "119.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-119.0.0.tgz#f250c442e72266f3e28d2b1eebc6a671a8629340" - integrity sha512-3TmabGT7xg57/Jbsg6B/Kqk3HaSbCP1ZHkR5zNft5vT/IWKjZCAGTH9waMI+i5KHSEiMH0zOw/WF98l+1Npkpw== +chromedriver@^119.0.1: + version "119.0.1" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-119.0.1.tgz#064f3650790ccea055e9bfd95c600f5ea60295e9" + integrity sha512-lpCFFLaXPpvElTaUOWKdP74pFb/sJhWtWqMjn7Ju1YriWn8dT5JBk84BGXMPvZQs70WfCYWecxdMmwfIu1Mupg== dependencies: - "@testim/chrome-version" "^1.1.3" - axios "^1.4.0" - compare-versions "^6.0.0" + "@testim/chrome-version" "^1.1.4" + axios "^1.6.0" + compare-versions "^6.1.0" extract-zip "^2.0.1" https-proxy-agent "^5.0.1" proxy-from-env "^1.1.0" - tcp-port-used "^1.0.1" + tcp-port-used "^1.0.2" chromium-bidi@0.4.28: version "0.4.28" @@ -13270,7 +13270,7 @@ compare-versions@3.5.1: resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393" integrity sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg== -compare-versions@^6.0.0: +compare-versions@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a" integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg== @@ -14371,13 +14371,6 @@ debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, de dependencies: ms "2.1.2" -debug@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.0.tgz#373687bffa678b38b1cd91f861b63850035ddc87" - integrity sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg== - dependencies: - ms "^2.1.1" - debug@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" @@ -14385,6 +14378,13 @@ debug@4.1.1: dependencies: ms "^2.1.1" +debug@4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -18858,10 +18858,10 @@ io-ts@^2.0.5: resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.0.5.tgz#e6e3db9df8b047f9cbd6b69e7d2ad3e6437a0b13" integrity sha512-pL7uUptryanI5Glv+GUv7xh+aLBjxGEDmLwmEYNSx0yOD3djK0Nw5Bt0N6BAkv9LadOUU7QKpRsLcqnTh3UlLA== -ip-regex@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" - integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= +ip-regex@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" + integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== ip@^1.1.8: version "1.1.8" @@ -19421,7 +19421,7 @@ is-url-superb@^4.0.0: resolved "https://registry.yarnpkg.com/is-url-superb/-/is-url-superb-4.0.0.tgz#b54d1d2499bb16792748ac967aa3ecb41a33a8c2" integrity sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA== -is-url@^1.2.2, is-url@^1.2.4: +is-url@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== @@ -19495,14 +19495,14 @@ is-yarn-global@^0.3.0: resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== -is2@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is2/-/is2-2.0.1.tgz#8ac355644840921ce435d94f05d3a94634d3481a" - integrity sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA== +is2@^2.0.6: + version "2.0.9" + resolved "https://registry.yarnpkg.com/is2/-/is2-2.0.9.tgz#ff63b441f90de343fa8fac2125ee170da8e8240d" + integrity sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g== dependencies: deep-is "^0.1.3" - ip-regex "^2.1.0" - is-url "^1.2.2" + ip-regex "^4.1.0" + is-url "^1.2.4" isarray@0.0.1: version "0.0.1" @@ -28596,13 +28596,13 @@ tcomb@^3.0.0, tcomb@^3.2.17: resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-3.2.29.tgz#32404fe9456d90c2cf4798682d37439f1ccc386c" integrity sha512-di2Hd1DB2Zfw6StGv861JoAF5h/uQVu/QJp2g8KVbtfKnoHdBQl5M32YWq6mnSYBQ1vFFrns5B1haWJL7rKaOQ== -tcp-port-used@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/tcp-port-used/-/tcp-port-used-1.0.1.tgz#46061078e2d38c73979a2c2c12b5a674e6689d70" - integrity sha512-rwi5xJeU6utXoEIiMvVBMc9eJ2/ofzB+7nLOdnZuFTmNCLqRiQh2sMG9MqCxHU/69VC/Fwp5dV9306Qd54ll1Q== +tcp-port-used@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tcp-port-used/-/tcp-port-used-1.0.2.tgz#9652b7436eb1f4cfae111c79b558a25769f6faea" + integrity sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA== dependencies: - debug "4.1.0" - is2 "2.0.1" + debug "4.3.1" + is2 "^2.0.6" teex@^1.0.1: version "1.0.1"