mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>
This commit is contained in:
parent
cee9be86c7
commit
3e6fe7c332
27 changed files with 895 additions and 55 deletions
|
@ -16,8 +16,9 @@ const legacyUrlAliasType: SavedObjectsType = {
|
|||
dynamic: false,
|
||||
properties: {
|
||||
sourceId: { type: 'keyword' },
|
||||
targetType: { type: 'keyword' },
|
||||
targetNamespace: { type: 'keyword' },
|
||||
targetType: { type: 'keyword' },
|
||||
targetId: { type: 'keyword' },
|
||||
resolveCounter: { type: 'long' },
|
||||
disabled: { type: 'boolean' },
|
||||
// other properties exist, but we aren't querying or aggregating on those, so we don't need to specify them (because we use `dynamic: false` above)
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { findLegacyUrlAliases } from './find_legacy_url_aliases';
|
||||
import type { findLegacyUrlAliases } from './legacy_url_aliases';
|
||||
import type * as InternalUtils from './internal_utils';
|
||||
|
||||
export const mockFindLegacyUrlAliases = jest.fn() as jest.MockedFunction<
|
||||
typeof findLegacyUrlAliases
|
||||
>;
|
||||
|
||||
jest.mock('./find_legacy_url_aliases', () => {
|
||||
jest.mock('./legacy_url_aliases', () => {
|
||||
return { findLegacyUrlAliases: mockFindLegacyUrlAliases };
|
||||
});
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
|
||||
import type { SavedObjectsSerializer } from '../../serialization';
|
||||
import type { SavedObject, SavedObjectsBaseOptions } from '../../types';
|
||||
import { findLegacyUrlAliases } from './find_legacy_url_aliases';
|
||||
import { findLegacyUrlAliases } from './legacy_url_aliases';
|
||||
import { getRootFields } from './included_fields';
|
||||
import {
|
||||
getObjectKey,
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import type { getErrorMessage } from '../../../../elasticsearch';
|
||||
|
||||
export const mockGetEsErrorMessage = jest.fn() as jest.MockedFunction<typeof getErrorMessage>;
|
||||
|
||||
jest.mock('../../../../elasticsearch', () => {
|
||||
return { getErrorMessage: mockGetEsErrorMessage };
|
||||
});
|
||||
|
||||
// Mock these functions to return empty results, as this simplifies test cases and we don't need to exercise alternate code paths for these
|
||||
jest.mock('@kbn/es-query', () => {
|
||||
return { nodeTypes: { function: { buildNode: jest.fn() } } };
|
||||
});
|
||||
jest.mock('../search_dsl', () => {
|
||||
return { getSearchDsl: jest.fn() };
|
||||
});
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { mockGetEsErrorMessage } from './delete_legacy_url_aliases.test.mock'; // Note: importing this file applies default mocks for other functions too
|
||||
|
||||
import { errors as EsErrors } from '@elastic/elasticsearch';
|
||||
|
||||
import { elasticsearchClientMock } from '../../../../elasticsearch/client/mocks';
|
||||
import { typeRegistryMock } from '../../../saved_objects_type_registry.mock';
|
||||
import { ALL_NAMESPACES_STRING } from '../utils';
|
||||
import { deleteLegacyUrlAliases } from './delete_legacy_url_aliases';
|
||||
import type { DeleteLegacyUrlAliasesParams } from './delete_legacy_url_aliases';
|
||||
|
||||
type SetupParams = Pick<
|
||||
DeleteLegacyUrlAliasesParams,
|
||||
'type' | 'id' | 'namespaces' | 'deleteBehavior'
|
||||
>;
|
||||
|
||||
describe('deleteLegacyUrlAliases', () => {
|
||||
function setup(setupParams: SetupParams) {
|
||||
return {
|
||||
mappings: { properties: {} }, // doesn't matter, only used as an argument to getSearchDsl which is mocked
|
||||
registry: typeRegistryMock.create(), // doesn't matter, only used as an argument to getSearchDsl which is mocked
|
||||
client: elasticsearchClientMock.createElasticsearchClient(),
|
||||
getIndexForType: jest.fn(), // doesn't matter
|
||||
...setupParams,
|
||||
};
|
||||
}
|
||||
|
||||
const type = 'obj-type';
|
||||
const id = 'obj-id';
|
||||
|
||||
it('throws an error if namespaces includes the "all namespaces" string', async () => {
|
||||
const namespaces = [ALL_NAMESPACES_STRING];
|
||||
const params = setup({ type, id, namespaces, deleteBehavior: 'inclusive' });
|
||||
|
||||
await expect(() => deleteLegacyUrlAliases(params)).rejects.toThrowError(
|
||||
`Failed to delete legacy URL aliases for ${type}/${id}: "namespaces" cannot include the * string`
|
||||
);
|
||||
expect(params.client.updateByQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws an error if updateByQuery fails', async () => {
|
||||
const namespaces = ['space-a', 'space-b'];
|
||||
const params = setup({ type, id, namespaces, deleteBehavior: 'inclusive' });
|
||||
const esError = new EsErrors.ResponseError(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
statusCode: 500,
|
||||
body: { error: { type: 'es_type', reason: 'es_reason' } },
|
||||
})
|
||||
);
|
||||
params.client.updateByQuery.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createErrorTransportRequestPromise(esError)
|
||||
);
|
||||
mockGetEsErrorMessage.mockClear();
|
||||
mockGetEsErrorMessage.mockReturnValue('Oh no!');
|
||||
|
||||
await expect(() => deleteLegacyUrlAliases(params)).rejects.toThrowError(
|
||||
`Failed to delete legacy URL aliases for ${type}/${id}: Oh no!`
|
||||
);
|
||||
expect(params.client.updateByQuery).toHaveBeenCalledTimes(1);
|
||||
expect(mockGetEsErrorMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockGetEsErrorMessage).toHaveBeenCalledWith(esError);
|
||||
});
|
||||
|
||||
describe('deleteBehavior "inclusive"', () => {
|
||||
const deleteBehavior = 'inclusive' as const;
|
||||
|
||||
it('when filtered namespaces is not empty, returns early', async () => {
|
||||
const namespaces = ['default'];
|
||||
const params = setup({ type, id, namespaces, deleteBehavior });
|
||||
|
||||
await deleteLegacyUrlAliases(params);
|
||||
expect(params.client.updateByQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('when filtered namespaces is not empty, calls updateByQuery with expected script params', async () => {
|
||||
const namespaces = ['space-a', 'default', 'space-b'];
|
||||
const params = setup({ type, id, namespaces, deleteBehavior });
|
||||
|
||||
await deleteLegacyUrlAliases(params);
|
||||
expect(params.client.updateByQuery).toHaveBeenCalledTimes(1);
|
||||
expect(params.client.updateByQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
script: expect.objectContaining({
|
||||
params: {
|
||||
namespaces: ['space-a', 'space-b'], // 'default' is filtered out
|
||||
matchTargetNamespaceOp: 'delete',
|
||||
notMatchTargetNamespaceOp: 'noop',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteBehavior "exclusive"', () => {
|
||||
const deleteBehavior = 'exclusive' as const;
|
||||
|
||||
it('when filtered namespaces is empty, calls updateByQuery with expected script params', async () => {
|
||||
const namespaces = ['default'];
|
||||
const params = setup({ type, id, namespaces, deleteBehavior });
|
||||
|
||||
await deleteLegacyUrlAliases(params);
|
||||
expect(params.client.updateByQuery).toHaveBeenCalledTimes(1);
|
||||
expect(params.client.updateByQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
script: expect.objectContaining({
|
||||
params: {
|
||||
namespaces: [], // 'default' is filtered out
|
||||
matchTargetNamespaceOp: 'noop',
|
||||
notMatchTargetNamespaceOp: 'delete',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('when filtered namespaces is not empty, calls updateByQuery with expected script params', async () => {
|
||||
const namespaces = ['space-a', 'default', 'space-b'];
|
||||
const params = setup({ type, id, namespaces, deleteBehavior });
|
||||
|
||||
await deleteLegacyUrlAliases(params);
|
||||
expect(params.client.updateByQuery).toHaveBeenCalledTimes(1);
|
||||
expect(params.client.updateByQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
script: expect.objectContaining({
|
||||
params: {
|
||||
namespaces: ['space-a', 'space-b'], // 'default' is filtered out
|
||||
matchTargetNamespaceOp: 'noop',
|
||||
notMatchTargetNamespaceOp: 'delete',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as esKuery from '@kbn/es-query';
|
||||
|
||||
import { getErrorMessage as getEsErrorMessage } from '../../../../elasticsearch';
|
||||
import type { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry';
|
||||
import type { IndexMapping } from '../../../mappings';
|
||||
import { LEGACY_URL_ALIAS_TYPE } from '../../../object_types';
|
||||
import type { RepositoryEsClient } from '../repository_es_client';
|
||||
import { getSearchDsl } from '../search_dsl';
|
||||
import { ALL_NAMESPACES_STRING, DEFAULT_NAMESPACE_STRING } from '../utils';
|
||||
|
||||
/** @internal */
|
||||
export interface DeleteLegacyUrlAliasesParams {
|
||||
mappings: IndexMapping;
|
||||
registry: ISavedObjectTypeRegistry;
|
||||
client: RepositoryEsClient;
|
||||
getIndexForType: (type: string) => string;
|
||||
/** The object type. */
|
||||
type: string;
|
||||
/** The object ID. */
|
||||
id: string;
|
||||
/**
|
||||
* The namespaces to include or exclude when searching for legacy URL alias targets (depends on the `deleteBehavior` parameter).
|
||||
* Note that using `namespaces: [], deleteBehavior: 'exclusive'` will delete all aliases for this object in all spaces.
|
||||
*/
|
||||
namespaces: string[];
|
||||
/**
|
||||
* If this is equal to 'inclusive', all aliases with a `targetNamespace` in the `namespaces` array will be deleted.
|
||||
* If this is equal to 'exclusive', all aliases with a `targetNamespace` _not_ in the `namespaces` array will be deleted.
|
||||
*/
|
||||
deleteBehavior: 'inclusive' | 'exclusive';
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes legacy URL aliases that point to a given object.
|
||||
*
|
||||
* Note that aliases are only created when an object is converted to become share-capable, and each targetId is deterministically generated
|
||||
* with uuidv5 -- this means that the chances of there actually being _multiple_ legacy URL aliases that target a given type/ID are slim to
|
||||
* none. However, we don't always know exactly what space an alias could be in (if an object exists in multiple spaces, or in all spaces),
|
||||
* so the most straightforward way for us to ensure that aliases are reliably deleted is to use updateByQuery, which is what this function
|
||||
* does.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export async function deleteLegacyUrlAliases(params: DeleteLegacyUrlAliasesParams) {
|
||||
const { mappings, registry, client, getIndexForType, type, id, namespaces, deleteBehavior } =
|
||||
params;
|
||||
|
||||
if (namespaces.includes(ALL_NAMESPACES_STRING)) {
|
||||
throwError(type, id, '"namespaces" cannot include the * string');
|
||||
}
|
||||
|
||||
// Legacy URL aliases cannot exist in the default space; filter that out
|
||||
const filteredNamespaces = namespaces.filter(
|
||||
(namespace) => namespace !== DEFAULT_NAMESPACE_STRING
|
||||
);
|
||||
if (!filteredNamespaces.length && deleteBehavior === 'inclusive') {
|
||||
// nothing to do, return early
|
||||
return;
|
||||
}
|
||||
|
||||
const { buildNode } = esKuery.nodeTypes.function;
|
||||
const match1 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.targetType`, type);
|
||||
const match2 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.targetId`, id);
|
||||
const kueryNode = buildNode('and', [match1, match2]);
|
||||
|
||||
try {
|
||||
await client.updateByQuery(
|
||||
{
|
||||
index: getIndexForType(LEGACY_URL_ALIAS_TYPE),
|
||||
refresh: false, // This could be called many times in succession, intentionally do not wait for a refresh
|
||||
body: {
|
||||
...getSearchDsl(mappings, registry, {
|
||||
type: LEGACY_URL_ALIAS_TYPE,
|
||||
kueryNode,
|
||||
}),
|
||||
script: {
|
||||
// Intentionally use one script source with variable params to take advantage of ES script caching
|
||||
source: `
|
||||
if (params['namespaces'].indexOf(ctx._source['${LEGACY_URL_ALIAS_TYPE}']['targetNamespace']) > -1) {
|
||||
ctx.op = params['matchTargetNamespaceOp'];
|
||||
} else {
|
||||
ctx.op = params['notMatchTargetNamespaceOp'];
|
||||
}
|
||||
`,
|
||||
params: {
|
||||
namespaces: filteredNamespaces,
|
||||
matchTargetNamespaceOp: deleteBehavior === 'inclusive' ? 'delete' : 'noop',
|
||||
notMatchTargetNamespaceOp: deleteBehavior === 'inclusive' ? 'noop' : 'delete',
|
||||
},
|
||||
lang: 'painless',
|
||||
},
|
||||
conflicts: 'proceed',
|
||||
},
|
||||
},
|
||||
{ ignore: [404] }
|
||||
);
|
||||
} catch (err) {
|
||||
const errorMessage = getEsErrorMessage(err);
|
||||
throwError(type, id, `${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
function throwError(type: string, id: string, message: string) {
|
||||
throw new Error(`Failed to delete legacy URL aliases for ${type}/${id}: ${message}`);
|
||||
}
|
|
@ -8,12 +8,12 @@
|
|||
|
||||
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
|
||||
|
||||
import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types';
|
||||
import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../../object_types';
|
||||
import type { CreatePointInTimeFinderFn, PointInTimeFinder } from '../point_in_time_finder';
|
||||
import { savedObjectsPointInTimeFinderMock } from '../point_in_time_finder.mock';
|
||||
import type { ISavedObjectsRepository } from '../repository';
|
||||
import { savedObjectsRepositoryMock } from '../repository.mock';
|
||||
import { findLegacyUrlAliases } from './find_legacy_url_aliases';
|
||||
import type { CreatePointInTimeFinderFn, PointInTimeFinder } from './point_in_time_finder';
|
||||
import { savedObjectsPointInTimeFinderMock } from './point_in_time_finder.mock';
|
||||
import type { ISavedObjectsRepository } from './repository';
|
||||
import { savedObjectsRepositoryMock } from './repository.mock';
|
||||
|
||||
describe('findLegacyUrlAliases', () => {
|
||||
let savedObjectsMock: jest.Mocked<ISavedObjectsRepository>;
|
|
@ -7,9 +7,9 @@
|
|||
*/
|
||||
|
||||
import * as esKuery from '@kbn/es-query';
|
||||
import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types';
|
||||
import { getObjectKey } from './internal_utils';
|
||||
import type { CreatePointInTimeFinderFn } from './point_in_time_finder';
|
||||
import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../../object_types';
|
||||
import { getObjectKey } from '../internal_utils';
|
||||
import type { CreatePointInTimeFinderFn } from '../point_in_time_finder';
|
||||
|
||||
interface FindLegacyUrlAliasesObject {
|
||||
type: string;
|
||||
|
@ -68,6 +68,7 @@ export async function findLegacyUrlAliases(
|
|||
|
||||
function createAliasKueryFilter(objects: Array<{ type: string; id: string }>) {
|
||||
const { buildNode } = esKuery.nodeTypes.function;
|
||||
// Note: these nodes include '.attributes' for type-level fields because these are eventually passed to `validateConvertFilterToKueryNode`, which requires it
|
||||
const kueryNodes = objects.reduce<unknown[]>((acc, { type, id }) => {
|
||||
const match1 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.targetType`, type);
|
||||
const match2 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.sourceId`, id);
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { findLegacyUrlAliases } from './find_legacy_url_aliases';
|
||||
|
||||
export { deleteLegacyUrlAliases } from './delete_legacy_url_aliases';
|
||||
export type { DeleteLegacyUrlAliasesParams } from './delete_legacy_url_aliases';
|
|
@ -6,14 +6,14 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { findLegacyUrlAliases } from './find_legacy_url_aliases';
|
||||
import type { findLegacyUrlAliases } from './legacy_url_aliases';
|
||||
import type * as InternalUtils from './internal_utils';
|
||||
|
||||
export const mockFindLegacyUrlAliases = jest.fn() as jest.MockedFunction<
|
||||
typeof findLegacyUrlAliases
|
||||
>;
|
||||
|
||||
jest.mock('./find_legacy_url_aliases', () => {
|
||||
jest.mock('./legacy_url_aliases', () => {
|
||||
return { findLegacyUrlAliases: mockFindLegacyUrlAliases };
|
||||
});
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import type {
|
|||
SavedObjectsRawDocSource,
|
||||
SavedObjectsSerializer,
|
||||
} from '../../serialization';
|
||||
import { findLegacyUrlAliases } from './find_legacy_url_aliases';
|
||||
import { findLegacyUrlAliases } from './legacy_url_aliases';
|
||||
import { Either, rawDocExistsInNamespaces } from './internal_utils';
|
||||
import { getObjectKey, isLeft, isRight } from './internal_utils';
|
||||
import type { CreatePointInTimeFinderFn } from './point_in_time_finder';
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
mockUpdateObjectsSpaces,
|
||||
mockGetCurrentTime,
|
||||
mockPreflightCheckForCreate,
|
||||
mockDeleteLegacyUrlAliases,
|
||||
} from './repository.test.mock';
|
||||
|
||||
import { SavedObjectsRepository } from './repository';
|
||||
|
@ -2394,9 +2395,11 @@ describe('SavedObjectsRepository', () => {
|
|||
const id = 'logstash-*';
|
||||
const namespace = 'foo-namespace';
|
||||
|
||||
const deleteSuccess = async (type, id, options) => {
|
||||
const deleteSuccess = async (type, id, options, internalOptions = {}) => {
|
||||
const { mockGetResponseValue } = internalOptions;
|
||||
if (registry.isMultiNamespace(type)) {
|
||||
const mockGetResponse = getMockGetResponse({ type, id }, options?.namespace);
|
||||
const mockGetResponse =
|
||||
mockGetResponseValue ?? getMockGetResponse({ type, id }, options?.namespace);
|
||||
client.get.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(mockGetResponse)
|
||||
);
|
||||
|
@ -2409,6 +2412,11 @@ describe('SavedObjectsRepository', () => {
|
|||
return result;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockDeleteLegacyUrlAliases.mockClear();
|
||||
mockDeleteLegacyUrlAliases.mockResolvedValue();
|
||||
});
|
||||
|
||||
describe('client calls', () => {
|
||||
it(`should use the ES delete action when not using a multi-namespace type`, async () => {
|
||||
await deleteSuccess(type, id);
|
||||
|
@ -2482,6 +2490,64 @@ describe('SavedObjectsRepository', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('legacy URL aliases', () => {
|
||||
it(`doesn't delete legacy URL aliases for single-namespace object types`, async () => {
|
||||
await deleteSuccess(type, id, { namespace });
|
||||
expect(mockDeleteLegacyUrlAliases).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// We intentionally do not include a test case for a multi-namespace object with a "not found" preflight result, because that throws
|
||||
// an error (without deleting aliases) and we already have a test case for that
|
||||
|
||||
it(`deletes legacy URL aliases for multi-namespace object types (all spaces)`, async () => {
|
||||
const internalOptions = {
|
||||
mockGetResponseValue: getMockGetResponse(
|
||||
{ type: MULTI_NAMESPACE_TYPE, id },
|
||||
ALL_NAMESPACES_STRING
|
||||
),
|
||||
};
|
||||
await deleteSuccess(MULTI_NAMESPACE_TYPE, id, { namespace, force: true }, internalOptions);
|
||||
expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MULTI_NAMESPACE_TYPE,
|
||||
id,
|
||||
namespaces: [],
|
||||
deleteBehavior: 'exclusive',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it(`deletes legacy URL aliases for multi-namespace object types (specific spaces)`, async () => {
|
||||
await deleteSuccess(MULTI_NAMESPACE_TYPE, id, { namespace }); // this function mocks a preflight response with the given namespace by default
|
||||
expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MULTI_NAMESPACE_TYPE,
|
||||
id,
|
||||
namespaces: [namespace],
|
||||
deleteBehavior: 'inclusive',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it(`logs a message when deleteLegacyUrlAliases returns an error`, async () => {
|
||||
client.get.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id, namespace })
|
||||
)
|
||||
);
|
||||
client.delete.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'deleted' })
|
||||
);
|
||||
mockDeleteLegacyUrlAliases.mockRejectedValueOnce(new Error('Oh no!'));
|
||||
await savedObjectsRepository.delete(MULTI_NAMESPACE_ISOLATED_TYPE, id, { namespace });
|
||||
expect(client.get).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
'Unable to delete aliases when deleting an object: Oh no!'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
const expectNotFoundError = async (type, id, options) => {
|
||||
await expect(savedObjectsRepository.delete(type, id, options)).rejects.toThrowError(
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { internalBulkResolve } from './internal_bulk_resolve';
|
|||
import type * as InternalUtils from './internal_utils';
|
||||
import type { preflightCheckForCreate } from './preflight_check_for_create';
|
||||
import type { updateObjectsSpaces } from './update_objects_spaces';
|
||||
import type { deleteLegacyUrlAliases } from './legacy_url_aliases';
|
||||
|
||||
export const mockCollectMultiNamespaceReferences = jest.fn() as jest.MockedFunction<
|
||||
typeof collectMultiNamespaceReferences
|
||||
|
@ -60,3 +61,10 @@ export const pointInTimeFinderMock = jest.fn();
|
|||
jest.doMock('./point_in_time_finder', () => ({
|
||||
PointInTimeFinder: pointInTimeFinderMock,
|
||||
}));
|
||||
|
||||
export const mockDeleteLegacyUrlAliases = jest.fn() as jest.MockedFunction<
|
||||
typeof deleteLegacyUrlAliases
|
||||
>;
|
||||
jest.mock('./legacy_url_aliases', () => ({
|
||||
deleteLegacyUrlAliases: mockDeleteLegacyUrlAliases,
|
||||
}));
|
||||
|
|
|
@ -100,6 +100,7 @@ import {
|
|||
preflightCheckForCreate,
|
||||
PreflightCheckForCreateObject,
|
||||
} from './preflight_check_for_create';
|
||||
import { deleteLegacyUrlAliases } from './legacy_url_aliases';
|
||||
|
||||
// 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.
|
||||
|
@ -717,6 +718,25 @@ export class SavedObjectsRepository {
|
|||
|
||||
const deleted = body.result === 'deleted';
|
||||
if (deleted) {
|
||||
const namespaces = preflightResult?.savedObjectNamespaces;
|
||||
if (namespaces) {
|
||||
// This is a multi-namespace object type, and it might have legacy URL aliases that need to be deleted.
|
||||
await deleteLegacyUrlAliases({
|
||||
mappings: this._mappings,
|
||||
registry: this._registry,
|
||||
client: this.client,
|
||||
getIndexForType: this.getIndexForType.bind(this),
|
||||
type,
|
||||
id,
|
||||
...(namespaces.includes(ALL_NAMESPACES_STRING)
|
||||
? { namespaces: [], deleteBehavior: 'exclusive' } // delete legacy URL aliases for this type/ID for all spaces
|
||||
: { namespaces, deleteBehavior: 'inclusive' }), // delete legacy URL aliases for this type/ID for these specific spaces
|
||||
}).catch((err) => {
|
||||
// The object has already been deleted, but we caught an error when attempting to delete aliases.
|
||||
// A consumer cannot attempt to delete the object again, so just log the error and swallow it.
|
||||
this._logger.error(`Unable to delete aliases when deleting an object: ${err.message}`);
|
||||
});
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
|
@ -1344,10 +1364,12 @@ export class SavedObjectsRepository {
|
|||
options?: SavedObjectsUpdateObjectsSpacesOptions
|
||||
) {
|
||||
return updateObjectsSpaces({
|
||||
mappings: this._mappings,
|
||||
registry: this._registry,
|
||||
allowedTypes: this._allowedTypes,
|
||||
client: this.client,
|
||||
serializer: this._serializer,
|
||||
logger: this._logger,
|
||||
getIndexForType: this.getIndexForType.bind(this),
|
||||
objects,
|
||||
spacesToAdd,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import type * as InternalUtils from './internal_utils';
|
||||
import type { deleteLegacyUrlAliases } from './legacy_url_aliases';
|
||||
|
||||
export const mockGetBulkOperationError = jest.fn() as jest.MockedFunction<
|
||||
typeof InternalUtils['getBulkOperationError']
|
||||
|
@ -27,3 +28,10 @@ jest.mock('./internal_utils', () => {
|
|||
rawDocExistsInNamespace: mockRawDocExistsInNamespace,
|
||||
};
|
||||
});
|
||||
|
||||
export const mockDeleteLegacyUrlAliases = jest.fn() as jest.MockedFunction<
|
||||
typeof deleteLegacyUrlAliases
|
||||
>;
|
||||
jest.mock('./legacy_url_aliases', () => ({
|
||||
deleteLegacyUrlAliases: mockDeleteLegacyUrlAliases,
|
||||
}));
|
||||
|
|
|
@ -10,12 +10,14 @@ import {
|
|||
mockGetBulkOperationError,
|
||||
mockGetExpectedVersionProperties,
|
||||
mockRawDocExistsInNamespace,
|
||||
mockDeleteLegacyUrlAliases,
|
||||
} from './update_objects_spaces.test.mock';
|
||||
|
||||
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
|
||||
import type { ElasticsearchClient } from 'src/core/server/elasticsearch';
|
||||
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
|
||||
|
||||
import { loggerMock } from '../../../logging/logger.mock';
|
||||
import { typeRegistryMock } from '../../saved_objects_type_registry.mock';
|
||||
import { SavedObjectsSerializer } from '../../serialization';
|
||||
import type {
|
||||
|
@ -23,6 +25,7 @@ import type {
|
|||
UpdateObjectsSpacesParams,
|
||||
} from './update_objects_spaces';
|
||||
import { updateObjectsSpaces } from './update_objects_spaces';
|
||||
import { ALL_NAMESPACES_STRING } from './utils';
|
||||
|
||||
type SetupParams = Partial<
|
||||
Pick<UpdateObjectsSpacesParams, 'objects' | 'spacesToAdd' | 'spacesToRemove' | 'options'>
|
||||
|
@ -53,6 +56,8 @@ beforeEach(() => {
|
|||
mockGetExpectedVersionProperties.mockReturnValue(EXPECTED_VERSION_PROPS);
|
||||
mockRawDocExistsInNamespace.mockReset();
|
||||
mockRawDocExistsInNamespace.mockReturnValue(true); // return true by default
|
||||
mockDeleteLegacyUrlAliases.mockReset();
|
||||
mockDeleteLegacyUrlAliases.mockResolvedValue(); // resolve an empty promise by default
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
|
@ -71,10 +76,12 @@ describe('#updateObjectsSpaces', () => {
|
|||
client = elasticsearchClientMock.createElasticsearchClient();
|
||||
const serializer = new SavedObjectsSerializer(registry);
|
||||
return {
|
||||
mappings: { properties: {} }, // doesn't matter, only used as an argument to deleteLegacyUrlAliases which is mocked
|
||||
registry,
|
||||
allowedTypes: [SHAREABLE_OBJ_TYPE, NON_SHAREABLE_OBJ_TYPE], // SHAREABLE_HIDDEN_OBJ_TYPE is excluded
|
||||
client,
|
||||
serializer,
|
||||
logger: loggerMock.create(),
|
||||
getIndexForType: (type: string) => `index-for-${type}`,
|
||||
objects,
|
||||
spacesToAdd,
|
||||
|
@ -382,6 +389,149 @@ describe('#updateObjectsSpaces', () => {
|
|||
expect(client.bulk).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy URL aliases', () => {
|
||||
it('does not delete aliases for objects that were not removed from any spaces', async () => {
|
||||
const space1 = 'space-to-add';
|
||||
const space2 = 'space-to-remove';
|
||||
const space3 = 'other-space';
|
||||
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed
|
||||
const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space3] }; // will be updated
|
||||
|
||||
const objects = [obj1, obj2];
|
||||
const spacesToAdd = [space1];
|
||||
const spacesToRemove = [space2];
|
||||
const params = setup({ objects, spacesToAdd, spacesToRemove });
|
||||
// this test case does not call mget
|
||||
mockBulkResults({ error: false }); // result for obj2
|
||||
|
||||
await updateObjectsSpaces(params);
|
||||
expect(client.bulk).toHaveBeenCalledTimes(1);
|
||||
expectBulkArgs({ action: 'update', object: { ...obj2, namespaces: [space3, space1] } });
|
||||
expect(mockDeleteLegacyUrlAliases).not.toHaveBeenCalled();
|
||||
expect(params.logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not delete aliases for objects that were removed from spaces but were also added to All Spaces (*)', async () => {
|
||||
const space2 = 'space-to-remove';
|
||||
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] };
|
||||
|
||||
const objects = [obj1];
|
||||
const spacesToAdd = [ALL_NAMESPACES_STRING];
|
||||
const spacesToRemove = [space2];
|
||||
const params = setup({ objects, spacesToAdd, spacesToRemove });
|
||||
// this test case does not call mget
|
||||
mockBulkResults({ error: false }); // result for obj1
|
||||
|
||||
await updateObjectsSpaces(params);
|
||||
expect(client.bulk).toHaveBeenCalledTimes(1);
|
||||
expectBulkArgs({
|
||||
action: 'update',
|
||||
object: { ...obj1, namespaces: [ALL_NAMESPACES_STRING] },
|
||||
});
|
||||
expect(mockDeleteLegacyUrlAliases).not.toHaveBeenCalled();
|
||||
expect(params.logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('deletes aliases for objects that were removed from specific spaces using "deleteBehavior: exclusive"', async () => {
|
||||
const space1 = 'space-to-remove';
|
||||
const space2 = 'another-space-to-remove';
|
||||
const space3 = 'other-space';
|
||||
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space3] }; // will not be changed
|
||||
const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1, space2, space3] }; // will be updated
|
||||
const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will be deleted
|
||||
|
||||
const objects = [obj1, obj2, obj3];
|
||||
const spacesToRemove = [space1, space2];
|
||||
const params = setup({ objects, spacesToRemove });
|
||||
// this test case does not call mget
|
||||
mockBulkResults({ error: false }, { error: false }); // result2 for obj2 and obj3
|
||||
|
||||
await updateObjectsSpaces(params);
|
||||
expect(client.bulk).toHaveBeenCalledTimes(1);
|
||||
expectBulkArgs(
|
||||
{ action: 'update', object: { ...obj2, namespaces: [space3] } },
|
||||
{ action: 'delete', object: obj3 }
|
||||
);
|
||||
expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledTimes(2);
|
||||
expect(mockDeleteLegacyUrlAliases).toHaveBeenNthCalledWith(
|
||||
1, // the first call resulted in an error which generated a log message (see assertion below)
|
||||
expect.objectContaining({
|
||||
type: obj2.type,
|
||||
id: obj2.id,
|
||||
namespaces: [space1, space2],
|
||||
deleteBehavior: 'inclusive',
|
||||
})
|
||||
);
|
||||
expect(mockDeleteLegacyUrlAliases).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
type: obj3.type,
|
||||
id: obj3.id,
|
||||
namespaces: [space1],
|
||||
deleteBehavior: 'inclusive',
|
||||
})
|
||||
);
|
||||
expect(params.logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('deletes aliases for objects that were removed from all spaces using "deleteBehavior: inclusive"', async () => {
|
||||
const space1 = 'space-to-add';
|
||||
const space2 = 'other-space';
|
||||
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; // will be updated to add space1
|
||||
const obj2 = {
|
||||
type: SHAREABLE_OBJ_TYPE,
|
||||
id: 'id-2',
|
||||
spaces: [space2, ALL_NAMESPACES_STRING], // will be updated to add space1 and remove *
|
||||
};
|
||||
|
||||
const objects = [obj1, obj2];
|
||||
const spacesToAdd = [space1];
|
||||
const spacesToRemove = [ALL_NAMESPACES_STRING];
|
||||
const params = setup({ objects, spacesToAdd, spacesToRemove });
|
||||
// this test case does not call mget
|
||||
mockBulkResults({ error: false }); // result for obj1
|
||||
|
||||
await updateObjectsSpaces(params);
|
||||
expect(client.bulk).toHaveBeenCalledTimes(1);
|
||||
expectBulkArgs(
|
||||
{ action: 'update', object: { ...obj1, namespaces: [space2, space1] } },
|
||||
{ action: 'update', object: { ...obj2, namespaces: [space2, space1] } }
|
||||
);
|
||||
expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledTimes(1);
|
||||
expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: obj2.type,
|
||||
id: obj2.id,
|
||||
namespaces: [space2, space1],
|
||||
deleteBehavior: 'exclusive',
|
||||
})
|
||||
);
|
||||
expect(params.logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs a message when deleteLegacyUrlAliases returns an error', async () => {
|
||||
const space1 = 'space-to-remove';
|
||||
const space2 = 'other-space';
|
||||
const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1, space2] }; // will be updated
|
||||
|
||||
const objects = [obj1];
|
||||
const spacesToRemove = [space1];
|
||||
const params = setup({ objects, spacesToRemove });
|
||||
// this test case does not call mget
|
||||
mockBulkResults({ error: false }); // result for obj1
|
||||
mockDeleteLegacyUrlAliases.mockRejectedValueOnce(new Error('Oh no!')); // result for deleting aliases for obj1
|
||||
|
||||
await updateObjectsSpaces(params);
|
||||
expect(client.bulk).toHaveBeenCalledTimes(1);
|
||||
expectBulkArgs({ action: 'update', object: { ...obj1, namespaces: [space2] } });
|
||||
expect(mockDeleteLegacyUrlAliases).toHaveBeenCalledTimes(1); // don't assert deleteLegacyUrlAliases args, we have tests for that above
|
||||
expect(params.logger.error).toHaveBeenCalledTimes(1);
|
||||
expect(params.logger.error).toHaveBeenCalledWith(
|
||||
'Unable to delete aliases when unsharing an object: Oh no!'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('returns expected results', () => {
|
||||
|
|
|
@ -6,9 +6,12 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import pMap from 'p-map';
|
||||
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import intersection from 'lodash/intersection';
|
||||
|
||||
import type { Logger } from '../../../logging';
|
||||
import type { IndexMapping } from '../../mappings';
|
||||
import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
|
||||
import type { SavedObjectsRawDocSource, SavedObjectsSerializer } from '../../serialization';
|
||||
import type {
|
||||
|
@ -28,6 +31,9 @@ import {
|
|||
} from './internal_utils';
|
||||
import { DEFAULT_REFRESH_SETTING } from './repository';
|
||||
import type { RepositoryEsClient } from './repository_es_client';
|
||||
import { ALL_NAMESPACES_STRING } from './utils';
|
||||
import type { DeleteLegacyUrlAliasesParams } from './legacy_url_aliases';
|
||||
import { deleteLegacyUrlAliases } from './legacy_url_aliases';
|
||||
|
||||
/**
|
||||
* An object that should have its spaces updated.
|
||||
|
@ -94,10 +100,12 @@ export interface SavedObjectsUpdateObjectsSpacesResponseObject {
|
|||
* @internal
|
||||
*/
|
||||
export interface UpdateObjectsSpacesParams {
|
||||
mappings: IndexMapping;
|
||||
registry: ISavedObjectTypeRegistry;
|
||||
allowedTypes: string[];
|
||||
client: RepositoryEsClient;
|
||||
serializer: SavedObjectsSerializer;
|
||||
logger: Logger;
|
||||
getIndexForType: (type: string) => string;
|
||||
objects: SavedObjectsUpdateObjectsSpacesObject[];
|
||||
spacesToAdd: string[];
|
||||
|
@ -105,15 +113,24 @@ export interface UpdateObjectsSpacesParams {
|
|||
options?: SavedObjectsUpdateObjectsSpacesOptions;
|
||||
}
|
||||
|
||||
type ObjectToDeleteAliasesFor = Pick<
|
||||
DeleteLegacyUrlAliasesParams,
|
||||
'type' | 'id' | 'namespaces' | 'deleteBehavior'
|
||||
>;
|
||||
|
||||
const MAX_CONCURRENT_ALIAS_DELETIONS = 10;
|
||||
|
||||
/**
|
||||
* Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace
|
||||
* type.
|
||||
*/
|
||||
export async function updateObjectsSpaces({
|
||||
mappings,
|
||||
registry,
|
||||
allowedTypes,
|
||||
client,
|
||||
serializer,
|
||||
logger,
|
||||
getIndexForType,
|
||||
objects,
|
||||
spacesToAdd,
|
||||
|
@ -190,8 +207,12 @@ export async function updateObjectsSpaces({
|
|||
const time = new Date().toISOString();
|
||||
let bulkOperationRequestIndexCounter = 0;
|
||||
const bulkOperationParams: estypes.BulkOperationContainer[] = [];
|
||||
const objectsToDeleteAliasesFor: ObjectToDeleteAliasesFor[] = [];
|
||||
const expectedBulkOperationResults: Array<
|
||||
Either<SavedObjectsUpdateObjectsSpacesResponseObject, Record<string, any>>
|
||||
Either<
|
||||
SavedObjectsUpdateObjectsSpacesResponseObject,
|
||||
{ type: string; id: string; updatedSpaces: string[]; esRequestIndex?: number }
|
||||
>
|
||||
> = expectedBulkGetResults.map((expectedBulkGetResult) => {
|
||||
if (isLeft(expectedBulkGetResult)) {
|
||||
return expectedBulkGetResult;
|
||||
|
@ -225,7 +246,7 @@ export async function updateObjectsSpaces({
|
|||
versionProperties = getExpectedVersionProperties(version);
|
||||
}
|
||||
|
||||
const { newSpaces, isUpdateRequired } = getNewSpacesArray(
|
||||
const { updatedSpaces, removedSpaces, isUpdateRequired } = analyzeSpaceChanges(
|
||||
currentSpaces,
|
||||
spacesToAdd,
|
||||
spacesToRemove
|
||||
|
@ -233,7 +254,7 @@ export async function updateObjectsSpaces({
|
|||
const expectedResult = {
|
||||
type,
|
||||
id,
|
||||
newSpaces,
|
||||
updatedSpaces,
|
||||
...(isUpdateRequired && { esRequestIndex: bulkOperationRequestIndexCounter++ }),
|
||||
};
|
||||
|
||||
|
@ -243,13 +264,24 @@ export async function updateObjectsSpaces({
|
|||
_index: getIndexForType(type),
|
||||
...versionProperties,
|
||||
};
|
||||
if (newSpaces.length) {
|
||||
const documentToSave = { updated_at: time, namespaces: newSpaces };
|
||||
if (updatedSpaces.length) {
|
||||
const documentToSave = { updated_at: time, namespaces: updatedSpaces };
|
||||
// @ts-expect-error BulkOperation.retry_on_conflict, BulkOperation.routing. BulkOperation.version, and BulkOperation.version_type are optional
|
||||
bulkOperationParams.push({ update: documentMetadata }, { doc: documentToSave });
|
||||
} else {
|
||||
bulkOperationParams.push({ delete: documentMetadata });
|
||||
}
|
||||
|
||||
if (removedSpaces.length && !updatedSpaces.includes(ALL_NAMESPACES_STRING)) {
|
||||
// The object is being removed from at least one space; make sure to delete aliases appropriately
|
||||
objectsToDeleteAliasesFor.push({
|
||||
type,
|
||||
id,
|
||||
...(removedSpaces.includes(ALL_NAMESPACES_STRING)
|
||||
? { namespaces: updatedSpaces, deleteBehavior: 'exclusive' }
|
||||
: { namespaces: removedSpaces, deleteBehavior: 'inclusive' }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { tag: 'Right', value: expectedResult };
|
||||
|
@ -260,6 +292,24 @@ export async function updateObjectsSpaces({
|
|||
? await client.bulk({ refresh, body: bulkOperationParams, require_alias: true })
|
||||
: undefined;
|
||||
|
||||
// Delete aliases if necessary, ensuring we don't have too many concurrent operations running.
|
||||
const mapper = async ({ type, id, namespaces, deleteBehavior }: ObjectToDeleteAliasesFor) =>
|
||||
deleteLegacyUrlAliases({
|
||||
mappings,
|
||||
registry,
|
||||
client,
|
||||
getIndexForType,
|
||||
type,
|
||||
id,
|
||||
namespaces,
|
||||
deleteBehavior,
|
||||
}).catch((err) => {
|
||||
// The object has already been unshared, but we caught an error when attempting to delete aliases.
|
||||
// A consumer cannot attempt to unshare the object again, so just log the error and swallow it.
|
||||
logger.error(`Unable to delete aliases when unsharing an object: ${err.message}`);
|
||||
});
|
||||
await pMap(objectsToDeleteAliasesFor, mapper, { concurrency: MAX_CONCURRENT_ALIAS_DELETIONS });
|
||||
|
||||
return {
|
||||
objects: expectedBulkOperationResults.map<SavedObjectsUpdateObjectsSpacesResponseObject>(
|
||||
(expectedResult) => {
|
||||
|
@ -267,7 +317,7 @@ export async function updateObjectsSpaces({
|
|||
return expectedResult.value;
|
||||
}
|
||||
|
||||
const { type, id, newSpaces, esRequestIndex } = expectedResult.value;
|
||||
const { type, id, updatedSpaces, esRequestIndex } = expectedResult.value;
|
||||
if (esRequestIndex !== undefined) {
|
||||
const response = bulkOperationResponse?.body.items[esRequestIndex] ?? {};
|
||||
const rawResponse = Object.values(response)[0] as any;
|
||||
|
@ -277,7 +327,7 @@ export async function updateObjectsSpaces({
|
|||
}
|
||||
}
|
||||
|
||||
return { id, type, spaces: newSpaces };
|
||||
return { id, type, spaces: updatedSpaces };
|
||||
}
|
||||
),
|
||||
};
|
||||
|
@ -289,17 +339,22 @@ function errorContent(error: DecoratedError) {
|
|||
}
|
||||
|
||||
/** Gets the remaining spaces for an object after adding new ones and removing old ones. */
|
||||
function getNewSpacesArray(
|
||||
function analyzeSpaceChanges(
|
||||
existingSpaces: string[],
|
||||
spacesToAdd: string[],
|
||||
spacesToRemove: string[]
|
||||
) {
|
||||
const addSet = new Set(spacesToAdd);
|
||||
const removeSet = new Set(spacesToRemove);
|
||||
const newSpaces = existingSpaces
|
||||
const removedSpaces: string[] = [];
|
||||
const updatedSpaces = existingSpaces
|
||||
.filter((x) => {
|
||||
addSet.delete(x);
|
||||
return !removeSet.delete(x);
|
||||
const removed = removeSet.delete(x);
|
||||
if (removed) {
|
||||
removedSpaces.push(x);
|
||||
}
|
||||
return !removed;
|
||||
})
|
||||
.concat(Array.from(addSet));
|
||||
|
||||
|
@ -307,5 +362,5 @@ function getNewSpacesArray(
|
|||
const isAnySpaceRemoved = removeSet.size < spacesToRemove.length;
|
||||
const isUpdateRequired = isAnySpaceAdded || isAnySpaceRemoved;
|
||||
|
||||
return { newSpaces, isUpdateRequired };
|
||||
return { updatedSpaces, removedSpaces, isUpdateRequired };
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -6,7 +6,9 @@
|
|||
*/
|
||||
|
||||
import { SuperTest } from 'supertest';
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import expect from '@kbn/expect';
|
||||
import type { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases';
|
||||
import { SPACES } from '../lib/spaces';
|
||||
import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils';
|
||||
|
@ -21,9 +23,13 @@ export interface DeleteTestCase extends TestCase {
|
|||
failure?: 400 | 403 | 404;
|
||||
}
|
||||
|
||||
const ALIAS_DELETE_INCLUSIVE = Object.freeze({ type: 'resolvetype', id: 'alias-match-newid' }); // exists in three specific spaces; deleting this should also delete the alias that targets it in space 1
|
||||
const ALIAS_DELETE_EXCLUSIVE = Object.freeze({ type: 'resolvetype', id: 'all_spaces' }); // exists in all spaces; deleting this should also delete the alias that targets it in space 1
|
||||
const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' });
|
||||
export const TEST_CASES: Record<string, DeleteTestCase> = Object.freeze({
|
||||
...CASES,
|
||||
ALIAS_DELETE_INCLUSIVE,
|
||||
ALIAS_DELETE_EXCLUSIVE,
|
||||
DOES_NOT_EXIST,
|
||||
});
|
||||
|
||||
|
@ -32,7 +38,7 @@ export const TEST_CASES: Record<string, DeleteTestCase> = Object.freeze({
|
|||
*/
|
||||
const createRequest = ({ type, id, force }: DeleteTestCase) => ({ type, id, force });
|
||||
|
||||
export function deleteTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>) {
|
||||
export function deleteTestSuiteFactory(es: Client, esArchiver: any, supertest: SuperTest<any>) {
|
||||
const expectSavedObjectForbidden = expectResponses.forbiddenTypes('delete');
|
||||
const expectResponseBody =
|
||||
(testCase: DeleteTestCase): ExpectResponseBody =>
|
||||
|
@ -47,6 +53,25 @@ export function deleteTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
|
|||
} else {
|
||||
// the success response for `delete` is an empty object
|
||||
expect(object).to.eql({});
|
||||
|
||||
// if we deleted an object that had an alias pointing to it, the alias should have been deleted as well
|
||||
await es.indices.refresh({ index: '.kibana' }); // alias deletion uses refresh: false, so we need to manually refresh the index before searching
|
||||
const searchResponse = await es.search({
|
||||
index: '.kibana',
|
||||
body: {
|
||||
size: 0,
|
||||
query: { terms: { type: ['legacy-url-alias'] } },
|
||||
track_total_hits: true,
|
||||
},
|
||||
});
|
||||
const expectAliasWasDeleted = !![ALIAS_DELETE_INCLUSIVE, ALIAS_DELETE_EXCLUSIVE].find(
|
||||
({ type, id }) => testCase.type === type && testCase.id === id
|
||||
);
|
||||
expect((searchResponse.hits.total as SearchTotalHits).value).to.eql(
|
||||
// Five aliases exist but only one should be deleted in each case (for the "inclusive" case, this asserts that the aliases
|
||||
// targeting that object in space x and space y were *not* deleted)
|
||||
expectAliasWasDeleted ? 4 : 5
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -51,6 +51,8 @@ const createTestCases = (spaceId: string) => {
|
|||
},
|
||||
{ ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) },
|
||||
CASES.NAMESPACE_AGNOSTIC,
|
||||
{ ...CASES.ALIAS_DELETE_INCLUSIVE, force: true },
|
||||
{ ...CASES.ALIAS_DELETE_EXCLUSIVE, force: true },
|
||||
{ ...CASES.DOES_NOT_EXIST, ...fail404() },
|
||||
];
|
||||
const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }];
|
||||
|
@ -61,8 +63,9 @@ const createTestCases = (spaceId: string) => {
|
|||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
const { addTests, createTestDefinitions } = deleteTestSuiteFactory(esArchiver, supertest);
|
||||
const { addTests, createTestDefinitions } = deleteTestSuiteFactory(es, esArchiver, supertest);
|
||||
const createTests = (spaceId: string) => {
|
||||
const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId);
|
||||
return {
|
||||
|
|
|
@ -45,6 +45,8 @@ const createTestCases = (spaceId: string) => [
|
|||
},
|
||||
{ ...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) },
|
||||
CASES.NAMESPACE_AGNOSTIC,
|
||||
{ ...CASES.ALIAS_DELETE_INCLUSIVE, force: true },
|
||||
{ ...CASES.ALIAS_DELETE_EXCLUSIVE, force: true },
|
||||
{ ...CASES.HIDDEN, ...fail404() },
|
||||
{ ...CASES.DOES_NOT_EXIST, ...fail404() },
|
||||
];
|
||||
|
@ -52,8 +54,9 @@ const createTestCases = (spaceId: string) => [
|
|||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
const { addTests, createTestDefinitions } = deleteTestSuiteFactory(esArchiver, supertest);
|
||||
const { addTests, createTestDefinitions } = deleteTestSuiteFactory(es, esArchiver, supertest);
|
||||
const createTests = (spaceId: string) => {
|
||||
const testCases = createTestCases(spaceId);
|
||||
return createTestDefinitions(testCases, false, { spaceId });
|
||||
|
|
|
@ -582,6 +582,94 @@
|
|||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "sharedtype:alias_delete_inclusive",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"sharedtype": {
|
||||
"title": "This is used to test that when an object is unshared from a space, inbound aliases for just those spaces are removed"
|
||||
},
|
||||
"type": "sharedtype",
|
||||
"namespaces": ["default", "space_1", "space_2"],
|
||||
"updated_at": "2017-09-21T18:59:16.270Z"
|
||||
},
|
||||
"type": "doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "legacy-url-alias:space_1:sharedtype:doesnt-matter",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"type": "legacy-url-alias",
|
||||
"updated_at": "2017-09-21T18:51:23.794Z",
|
||||
"legacy-url-alias": {
|
||||
"sourceId": "doesnt-matter",
|
||||
"targetNamespace": "space_1",
|
||||
"targetType": "sharedtype",
|
||||
"targetId": "alias_delete_inclusive"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "legacy-url-alias:space_2:sharedtype:doesnt-matter",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"type": "legacy-url-alias",
|
||||
"updated_at": "2017-09-21T18:51:23.794Z",
|
||||
"legacy-url-alias": {
|
||||
"sourceId": "doesnt-matter",
|
||||
"targetNamespace": "space_2",
|
||||
"targetType": "sharedtype",
|
||||
"targetId": "alias_delete_inclusive"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "sharedtype:alias_delete_exclusive",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"sharedtype": {
|
||||
"title": "This is used to test that when an object is unshared from all space, inbound aliases for all spaces are removed"
|
||||
},
|
||||
"type": "sharedtype",
|
||||
"namespaces": ["*"],
|
||||
"updated_at": "2017-09-21T18:59:16.270Z"
|
||||
},
|
||||
"type": "doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "legacy-url-alias:other_space:sharedtype:doesnt-matter",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"type": "legacy-url-alias",
|
||||
"updated_at": "2017-09-21T18:51:23.794Z",
|
||||
"legacy-url-alias": {
|
||||
"sourceId": "doesnt-matter",
|
||||
"targetNamespace": "other_space",
|
||||
"targetType": "sharedtype",
|
||||
"targetId": "alias_delete_exclusive"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
|
|
|
@ -38,6 +38,14 @@ export const MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES = Object.freeze({
|
|||
id: 'all_spaces',
|
||||
existingNamespaces: ['*'], // all current and future spaces
|
||||
}),
|
||||
ALIAS_DELETE_INCLUSIVE: Object.freeze({
|
||||
id: 'alias_delete_inclusive',
|
||||
existingNamespaces: ['default', 'space_1', 'space_2'], // each individual space
|
||||
}),
|
||||
ALIAS_DELETE_EXCLUSIVE: Object.freeze({
|
||||
id: 'alias_delete_exclusive',
|
||||
existingNamespaces: ['*'], // all current and future spaces
|
||||
}),
|
||||
DOES_NOT_EXIST: Object.freeze({
|
||||
id: 'does_not_exist',
|
||||
existingNamespaces: [] as string[],
|
||||
|
|
|
@ -53,8 +53,8 @@ export function deleteTestSuiteFactory(es: Client, esArchiver: any, supertest: S
|
|||
// @ts-expect-error @elastic/elasticsearch doesn't defined `count.buckets`.
|
||||
const buckets = response.aggregations?.count.buckets;
|
||||
|
||||
// The test fixture contains three legacy URL aliases:
|
||||
// (1) one for "space_1", (2) one for "space_2", and (3) one for "other_space", which is a non-existent space.
|
||||
// The test fixture contains six legacy URL aliases:
|
||||
// (1) two for "space_1", (2) two for "space_2", and (3) two for "other_space", which is a non-existent space.
|
||||
// Each test deletes "space_2", so the agg buckets should reflect that aliases (1) and (3) still exist afterwards.
|
||||
|
||||
// Space 2 deleted, all others should exist
|
||||
|
@ -75,26 +75,26 @@ export function deleteTestSuiteFactory(es: Client, esArchiver: any, supertest: S
|
|||
},
|
||||
},
|
||||
{
|
||||
doc_count: 6,
|
||||
doc_count: 7,
|
||||
key: 'space_1',
|
||||
countByType: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{ key: 'visualization', doc_count: 3 },
|
||||
{ key: 'legacy-url-alias', doc_count: 2 }, // aliases (1)
|
||||
{ key: 'dashboard', doc_count: 1 },
|
||||
{ key: 'index-pattern', doc_count: 1 },
|
||||
{ key: 'legacy-url-alias', doc_count: 1 }, // alias (1)
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
doc_count: 1,
|
||||
doc_count: 2,
|
||||
key: 'other_space',
|
||||
countByType: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [{ key: 'legacy-url-alias', doc_count: 1 }], // alias (3)
|
||||
buckets: [{ key: 'legacy-url-alias', doc_count: 2 }], // aliases (3)
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -110,8 +110,8 @@ export function deleteTestSuiteFactory(es: Client, esArchiver: any, supertest: S
|
|||
body: { query: { terms: { type: ['sharedtype'] } } },
|
||||
});
|
||||
const docs = multiNamespaceResponse.hits.hits;
|
||||
// Just 12 results, since spaces_2_only, conflict_1_space_2 and conflict_2_space_2 got deleted.
|
||||
expect(docs).length(12);
|
||||
// Just 14 results, since spaces_2_only, conflict_1_space_2 and conflict_2_space_2 got deleted.
|
||||
expect(docs).length(14);
|
||||
docs.forEach((doc) => () => {
|
||||
const containsSpace2 = doc?._source?.namespaces.includes('space_2');
|
||||
expect(containsSpace2).to.eql(false);
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import type { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { without, uniq } from 'lodash';
|
||||
import { SuperTest } from 'supertest';
|
||||
import {
|
||||
|
@ -35,6 +37,7 @@ export interface UpdateObjectsSpacesTestCase {
|
|||
objects: Array<{
|
||||
id: string;
|
||||
existingNamespaces: string[];
|
||||
expectAliasDifference?: number;
|
||||
failure?: 400 | 404;
|
||||
}>;
|
||||
spacesToAdd: string[];
|
||||
|
@ -54,7 +57,11 @@ const getTestTitle = ({ objects, spacesToAdd, spacesToRemove }: UpdateObjectsSpa
|
|||
return `{objects: [${objStr}], spacesToAdd: [${addStr}], spacesToRemove: [${remStr}]}`;
|
||||
};
|
||||
|
||||
export function updateObjectsSpacesTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>) {
|
||||
export function updateObjectsSpacesTestSuiteFactory(
|
||||
es: Client,
|
||||
esArchiver: any,
|
||||
supertest: SuperTest<any>
|
||||
) {
|
||||
const expectForbidden = expectResponses.forbiddenTypes('share_to_space');
|
||||
const expectResponseBody =
|
||||
(
|
||||
|
@ -68,7 +75,10 @@ export function updateObjectsSpacesTestSuiteFactory(esArchiver: any, supertest:
|
|||
} else {
|
||||
const { objects, spacesToAdd, spacesToRemove } = testCase;
|
||||
const apiResponse = response.body as SavedObjectsUpdateObjectsSpacesResponse;
|
||||
objects.forEach(({ id, existingNamespaces, failure }, i) => {
|
||||
|
||||
let hasRefreshed = false;
|
||||
for (let i = 0; i < objects.length; i++) {
|
||||
const { id, existingNamespaces, expectAliasDifference, failure } = objects[i];
|
||||
const object = apiResponse.objects[i];
|
||||
if (failure === 404) {
|
||||
const error = SavedObjectsErrorHelpers.createGenericNotFoundError(TYPE, id);
|
||||
|
@ -84,8 +94,28 @@ export function updateObjectsSpacesTestSuiteFactory(esArchiver: any, supertest:
|
|||
expect(result.type).to.eql(TYPE);
|
||||
expect(result.id).to.eql(id);
|
||||
expect(result.spaces.sort()).to.eql(expectedSpaces.sort());
|
||||
|
||||
if (expectAliasDifference !== undefined) {
|
||||
// if we deleted an object that had an alias pointing to it, the alias should have been deleted as well
|
||||
if (!hasRefreshed) {
|
||||
await es.indices.refresh({ index: '.kibana' }); // alias deletion uses refresh: false, so we need to manually refresh the index before searching
|
||||
hasRefreshed = true;
|
||||
}
|
||||
const searchResponse = await es.search({
|
||||
index: '.kibana',
|
||||
body: {
|
||||
size: 0,
|
||||
query: { terms: { type: ['legacy-url-alias'] } },
|
||||
track_total_hits: true,
|
||||
},
|
||||
});
|
||||
expect((searchResponse.hits.total as SearchTotalHits).value).to.eql(
|
||||
// Six aliases exist in the test fixtures
|
||||
6 + expectAliasDifference
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
const createTestDefinitions = (
|
||||
|
|
|
@ -28,6 +28,8 @@ const { fail404 } = testCaseFailures;
|
|||
|
||||
const createTestCases = (spaceId: string): UpdateObjectsSpacesTestCase[] => {
|
||||
const eachSpace = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID];
|
||||
// Note: we intentionally exclude ALIAS_DELETION test cases because they are already covered in spaces_only test suite, and there is no
|
||||
// authZ-specific logic that affects alias deletion, all of that happens at the Saved Objects Repository level.
|
||||
return [
|
||||
// Test case to check adding and removing all spaces ("*") to a saved object
|
||||
{
|
||||
|
@ -125,8 +127,10 @@ const calculateSingleSpaceAuthZ = (testCases: UpdateObjectsSpacesTestCase[], spa
|
|||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
const { addTests, createTestDefinitions } = updateObjectsSpacesTestSuiteFactory(
|
||||
es,
|
||||
esArchiver,
|
||||
supertest
|
||||
);
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
getTestScenarios,
|
||||
} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils';
|
||||
import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases';
|
||||
import type { UpdateObjectsSpacesTestCase } from '../../common/suites/update_objects_spaces';
|
||||
import { updateObjectsSpacesTestSuiteFactory } from '../../common/suites/update_objects_spaces';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
||||
|
@ -51,7 +52,55 @@ const createSinglePartTestCases = (spaceId: string) => {
|
|||
const createMultiPartTestCases = () => {
|
||||
const nonExistentSpace = 'does_not_exist'; // space that doesn't exist
|
||||
const eachSpace = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID];
|
||||
const group1 = [
|
||||
const group1: UpdateObjectsSpacesTestCase[] = [
|
||||
// These test cases ensure that aliases are deleted when objects are unshared.
|
||||
// For simplicity these are done separately, before the others.
|
||||
{
|
||||
objects: [
|
||||
{
|
||||
id: CASES.ALIAS_DELETE_INCLUSIVE.id,
|
||||
existingNamespaces: eachSpace,
|
||||
expectAliasDifference: -1, // one alias should have been deleted from space_2
|
||||
},
|
||||
],
|
||||
spacesToAdd: [],
|
||||
spacesToRemove: [SPACE_2_ID],
|
||||
},
|
||||
{
|
||||
objects: [
|
||||
{
|
||||
id: CASES.ALIAS_DELETE_INCLUSIVE.id,
|
||||
existingNamespaces: [DEFAULT_SPACE_ID, SPACE_1_ID],
|
||||
expectAliasDifference: -2, // one alias should have been deleted from space_1
|
||||
},
|
||||
],
|
||||
spacesToAdd: [],
|
||||
spacesToRemove: [SPACE_1_ID],
|
||||
},
|
||||
{
|
||||
objects: [
|
||||
{
|
||||
id: CASES.ALIAS_DELETE_INCLUSIVE.id,
|
||||
existingNamespaces: [DEFAULT_SPACE_ID],
|
||||
expectAliasDifference: -2, // no aliases can exist in the default space, so no aliases were deleted
|
||||
},
|
||||
],
|
||||
spacesToAdd: [],
|
||||
spacesToRemove: [DEFAULT_SPACE_ID],
|
||||
},
|
||||
{
|
||||
objects: [
|
||||
{
|
||||
id: CASES.ALIAS_DELETE_EXCLUSIVE.id,
|
||||
existingNamespaces: [SPACE_1_ID],
|
||||
expectAliasDifference: -3, // one alias should have been deleted from other_space
|
||||
},
|
||||
],
|
||||
spacesToAdd: [SPACE_1_ID],
|
||||
spacesToRemove: ['*'],
|
||||
},
|
||||
];
|
||||
const group2 = [
|
||||
// first, add this object to each space and remove it from nonExistentSpace
|
||||
// this will succeed even though the object already exists in the default space and it doesn't exist in nonExistentSpace
|
||||
{ objects: [CASES.DEFAULT_ONLY], spacesToAdd: eachSpace, spacesToRemove: [nonExistentSpace] },
|
||||
|
@ -87,7 +136,7 @@ const createMultiPartTestCases = () => {
|
|||
spacesToRemove: [SPACE_1_ID],
|
||||
},
|
||||
];
|
||||
const group2 = [
|
||||
const group3 = [
|
||||
// first, add this object to space_2 and remove it from space_1
|
||||
{
|
||||
objects: [CASES.DEFAULT_AND_SPACE_1],
|
||||
|
@ -111,15 +160,17 @@ const createMultiPartTestCases = () => {
|
|||
spacesToRemove: [],
|
||||
},
|
||||
];
|
||||
return [...group1, ...group2];
|
||||
return [...group1, ...group2, ...group3];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
const { addTests, createTestDefinitions } = updateObjectsSpacesTestSuiteFactory(
|
||||
es,
|
||||
esArchiver,
|
||||
supertest
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue