mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Moves SOR bulkCreate unit tests to dedicated file (#171431)
This commit is contained in:
parent
540f2b632e
commit
d8ef2d0fb1
2 changed files with 1048 additions and 916 deletions
File diff suppressed because it is too large
Load diff
|
@ -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<string, unknown> = {}
|
||||
) => [
|
||||
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<unknown> = {
|
||||
type: 'config',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue