Add origin conflict checks when resolving import errors (#125241) (#125435)

(cherry picked from commit 5629decd9e)

Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2022-02-11 15:41:48 -05:00 committed by GitHub
parent 9d2091b3fd
commit 466ad82d4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 420 additions and 67 deletions

View file

@ -13,6 +13,7 @@ import {
SavedObjectReference,
SavedObject,
SavedObjectsImportFailure,
SavedObjectsImportRetry,
} from '../../types';
import { checkOriginConflicts } from './check_origin_conflicts';
import { savedObjectsClientMock } from '../../../mocks';
@ -65,6 +66,7 @@ describe('#checkOriginConflicts', () => {
ignoreRegularConflicts?: boolean;
importStateMap?: ImportStateMap;
pendingOverwrites?: Set<string>;
retries?: SavedObjectsImportRetry[];
}): CheckOriginConflictsParams => {
savedObjectsClient = savedObjectsClientMock.create();
find = savedObjectsClient.find;
@ -122,6 +124,24 @@ describe('#checkOriginConflicts', () => {
expect(find).not.toHaveBeenCalled();
});
test('does not execute searches for multi-namespace objects that have a retry with a destinationId specified', async () => {
const objects = [multiNsObj, multiNsObjWithOriginId];
const params = setupParams({
objects,
retries: [
{ type: multiNsObj.type, id: multiNsObj.id, destinationId: 'doesnt-matter' },
{
type: multiNsObjWithOriginId.type,
id: multiNsObjWithOriginId.id,
destinationId: 'doesnt-matter',
},
] as SavedObjectsImportRetry[],
});
await checkOriginConflicts(params);
expect(find).not.toHaveBeenCalled();
});
test('executes searches for multi-namespace objects', async () => {
const objects = [multiNsObj, otherObj, multiNsObjWithOriginId, otherObjWithOriginId];
const params1 = setupParams({ objects });
@ -205,6 +225,65 @@ describe('#checkOriginConflicts', () => {
expect(checkOriginConflictsResult).toEqual(expectedResult);
});
describe('retries', () => {
// retries are only defined when called from resolveSavedObjectsImportErrors
test('filters inexact matches of other retries ("retryDestinations" check)', async () => {
// obj1 and obj2 exist in this space
// try to import obj3 and obj4; simulating a scenario where they both share an origin, but obj3 is being retried with the
// destinationId of obj1, and obj2 is being retried without a destinationId
const obj1 = createObject(MULTI_NS_TYPE, 'id-1', 'some-originId');
const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'some-originId');
const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'some-originId');
const obj4 = createObject(MULTI_NS_TYPE, 'id-4', 'some-originId');
const objects = [obj3, obj4];
const params = setupParams({
objects,
importStateMap: new Map([
[`${obj3.type}:${obj3.id}`, {}],
[`${obj4.type}:${obj4.id}`, {}],
]),
pendingOverwrites: new Set([`${obj3.type}:${obj3.id}`]),
retries: [
{ type: obj3.type, id: obj3.id, destinationId: obj1.id, overwrite: true }, // if obj1 already exists, this would have had to have overwrite=true to pass the earlier call to checkConflicts without an error
{ type: obj4.type, id: obj4.id },
] as SavedObjectsImportRetry[],
});
// find is skipped for obj1 because it has a retry with a destinationId
mockFindResult(obj1, obj2); // find for obj4: the result is an inexact match with two destinations, but obj1 is matched by obj3 -- accordingly, obj4 has an inexact match to obj2
const checkOriginConflictsResult = await checkOriginConflicts(params);
const expectedResult = {
importStateMap: new Map(),
errors: [createConflictError(obj4, obj2.id)],
pendingOverwrites: new Set(), // does not capture obj3 because that would have been captured in pendingOverwrites for the checkConflicts function
};
expect(checkOriginConflictsResult).toEqual(expectedResult);
});
test('does not return a conflict error when a retry has overwrite=true', async () => {
// obj1 exists in this space
// try to import 2; simulating a scenario where they both share an origin
const obj1 = createObject(MULTI_NS_TYPE, 'id-1', 'some-originId');
const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'some-originId');
const objects = [obj2];
const params = setupParams({
objects,
importStateMap: new Map([[`${obj2.type}:${obj2.id}`, {}]]),
pendingOverwrites: new Set(), // obj2 wouldn't be included in pendingOverwrites from the earlier call to checkConflicts because obj2 doesn't exist
retries: [{ type: obj2.type, id: obj2.id, overwrite: true }] as SavedObjectsImportRetry[],
});
mockFindResult(obj1); // find for obj2: the result is an inexact match with one destination -- accordingly, obj2 has an inexact match to obj1
const checkOriginConflictsResult = await checkOriginConflicts(params);
const expectedResult = {
importStateMap: new Map([[`${obj2.type}:${obj2.id}`, { destinationId: obj1.id }]]),
errors: [],
pendingOverwrites: new Set([`${obj2.type}:${obj2.id}`]),
};
expect(checkOriginConflictsResult).toEqual(expectedResult);
});
});
describe('object result without a `importStateMap` entry (no match or exact match)', () => {
test('returns object when no match is detected (0 hits)', async () => {
// no objects exist in this space

View file

@ -8,8 +8,14 @@
import pMap from 'p-map';
import { v4 as uuidv4 } from 'uuid';
import { SavedObject, SavedObjectsClientContract, SavedObjectsImportFailure } from '../../types';
import type {
SavedObject,
SavedObjectsClientContract,
SavedObjectsImportFailure,
SavedObjectsImportRetry,
} from '../../types';
import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { getObjectKey } from '../../service/lib/internal_utils';
import type { ImportStateMap } from './types';
import { createOriginQuery } from './utils';
@ -21,11 +27,17 @@ interface CheckOriginConflictsParams {
ignoreRegularConflicts?: boolean;
importStateMap: ImportStateMap;
pendingOverwrites: Set<string>;
retries?: SavedObjectsImportRetry[];
}
type CheckOriginConflictParams = Omit<CheckOriginConflictsParams, 'objects' | 'importIdMap'> & {
type CheckOriginConflictParams = Omit<
CheckOriginConflictsParams,
'objects' | 'importIdMap' | 'retries'
> & {
object: SavedObject<{ title?: string }>;
objectIdsBeingImported: Set<string>;
retryMap: Map<string, SavedObjectsImportRetry>;
retryDestinations: Set<string>;
};
interface InexactMatch<T> {
@ -64,7 +76,7 @@ const transformObjectsToAmbiguousConflictFields = (
return a.id < b.id ? -1 : 1; // ascending
});
const getAmbiguousConflictSourceKey = <T>({ object }: InexactMatch<T>) =>
`${object.type}:${object.originId || object.id}`;
getObjectKey({ type: object.type, id: object.originId || object.id });
/**
* Make a search request for an import object to check if any objects of this type that match this object's `originId` or `id` exist in the
@ -85,12 +97,27 @@ const checkOriginConflict = async (
namespace,
objectIdsBeingImported,
pendingOverwrites,
retryMap,
retryDestinations,
} = params;
const { type, originId, id } = object;
if (!typeRegistry.isMultiNamespace(type) || pendingOverwrites.has(`${type}:${id}`)) {
const key = getObjectKey(object);
const retry = retryMap.get(key);
if (
!typeRegistry.isMultiNamespace(type) ||
pendingOverwrites.has(key) ||
!!retry?.destinationId
) {
// Skip the search request for non-multi-namespace types, since by definition they cannot have inexact matches or ambiguous conflicts.
// Also skip the search request for objects that we've already determined have an "exact match" conflict.
// Finally, skip the search request for objects that have specified a destinationId for a retry.
// The checkConflicts function is always called before this one. There are three situations where a retry would have a destinationId:
// 1. retry with overwrite=false, where the object already exists -> checkConflicts would return a conflict error
// 2. retry with overwrite=true, where the object already exists -> checkConflicts would add an entry to pendingOverwrites
// 3. retry where the object *doesn't* exist -> checkConflicts wouldn't return an error _or_ add an entry to pendingOverwrites
// Scenario (3) is why we check to see if there is a retry destinationId and skip the origin check in that case.
return { tag: 'right', value: object };
}
@ -111,9 +138,10 @@ const checkOriginConflict = async (
return { tag: 'right', value: object };
}
// This is an "inexact match" so far; filter the conflict destination(s) to exclude any that exactly match other objects we are importing.
const objects = savedObjects.filter(
(obj) => !objectIdsBeingImported.has(`${obj.type}:${obj.id}`)
);
const objects = savedObjects.filter((obj) => {
const destKey = getObjectKey(obj);
return !objectIdsBeingImported.has(destKey) && !retryDestinations.has(destKey);
});
const destinations = transformObjectsToAmbiguousConflictFields(objects);
if (destinations.length === 0) {
// No conflict destinations remain after filtering, so this is a "no match" result.
@ -140,16 +168,30 @@ const checkOriginConflict = async (
* will allow `createSavedObjects` to modify the ID before creating the object (thus ensuring a conflict during).
* B. Otherwise, this is an "ambiguous conflict" result; return an error.
*/
export async function checkOriginConflicts({ objects, ...params }: CheckOriginConflictsParams) {
export async function checkOriginConflicts({
objects,
retries = [],
...params
}: CheckOriginConflictsParams) {
const objectIdsBeingImported = new Set<string>();
for (const [key, { isOnlyReference }] of params.importStateMap.entries()) {
if (!isOnlyReference) {
objectIdsBeingImported.add(key);
}
}
const retryMap = retries.reduce(
(acc, cur) => acc.set(getObjectKey(cur), cur),
new Map<string, SavedObjectsImportRetry>()
);
const retryDestinations = retries.reduce((acc, cur) => {
if (cur.destinationId) {
acc.add(getObjectKey({ type: cur.type, id: cur.destinationId }));
}
return acc;
}, new Set<string>());
// Check each object for possible destination conflicts, ensuring we don't too many concurrent searches running.
const mapper = async (object: SavedObject<{ title?: string }>) =>
checkOriginConflict({ object, objectIdsBeingImported, ...params });
checkOriginConflict({ object, objectIdsBeingImported, retryMap, retryDestinations, ...params });
const checkOriginConflictResults = await pMap(objects, mapper, {
concurrency: MAX_CONCURRENT_SEARCHES,
});
@ -170,17 +212,18 @@ export async function checkOriginConflicts({ objects, ...params }: CheckOriginCo
if (!isLeft(result)) {
return;
}
const key = getAmbiguousConflictSourceKey(result.value);
const ambiguousConflictsSourceKey = getAmbiguousConflictSourceKey(result.value);
const sources = transformObjectsToAmbiguousConflictFields(
ambiguousConflictSourcesMap.get(key)!
ambiguousConflictSourcesMap.get(ambiguousConflictsSourceKey)!
);
const { object, destinations } = result.value;
const { type, id, attributes } = object;
if (sources.length === 1 && destinations.length === 1) {
// This is a simple "inexact match" result -- a single import object has a single destination conflict.
if (params.ignoreRegularConflicts) {
importStateMap.set(`${type}:${id}`, { destinationId: destinations[0].id });
pendingOverwrites.add(`${type}:${id}`);
const key = getObjectKey(object);
if (params.ignoreRegularConflicts || retryMap.get(key)?.overwrite) {
importStateMap.set(key, { destinationId: destinations[0].id });
pendingOverwrites.add(key);
} else {
const { title } = attributes;
errors.push({
@ -203,7 +246,7 @@ export async function checkOriginConflicts({ objects, ...params }: CheckOriginCo
if (sources.length > 1) {
// In the case of ambiguous source conflicts, don't treat them as errors; instead, regenerate the object ID and reset its origin
// (e.g., the same outcome as if `createNewCopies` was enabled for the entire import operation).
importStateMap.set(`${type}:${id}`, { destinationId: uuidv4(), omitOriginId: true });
importStateMap.set(getObjectKey(object), { destinationId: uuidv4(), omitOriginId: true });
return;
}
const { title } = attributes;

View file

@ -13,6 +13,7 @@ import type { collectSavedObjects } from './lib/collect_saved_objects';
import type { regenerateIds } from './lib/regenerate_ids';
import type { validateReferences } from './lib/validate_references';
import type { checkConflicts } from './lib/check_conflicts';
import type { checkOriginConflicts } from './lib/check_origin_conflicts';
import type { getImportStateMapForRetries } from './lib/get_import_state_map_for_retries';
import type { splitOverwrites } from './lib/split_overwrites';
import type { createSavedObjects } from './lib/create_saved_objects';
@ -55,6 +56,13 @@ jest.mock('./lib/check_conflicts', () => ({
checkConflicts: mockCheckConflicts,
}));
export const mockCheckOriginConflicts = jest.fn() as jest.MockedFunction<
typeof checkOriginConflicts
>;
jest.mock('./lib/check_origin_conflicts', () => ({
checkOriginConflicts: mockCheckOriginConflicts,
}));
export const mockGetImportStateMapForRetries = jest.fn() as jest.MockedFunction<
typeof getImportStateMapForRetries
>;

View file

@ -14,6 +14,7 @@ import {
mockRegenerateIds,
mockValidateReferences,
mockCheckConflicts,
mockCheckOriginConflicts,
mockGetImportStateMapForRetries,
mockSplitOverwrites,
mockCreateSavedObjects,
@ -56,7 +57,12 @@ describe('#importSavedObjectsFromStream', () => {
errors: [],
filteredObjects: [],
importStateMap: new Map(),
pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type
pendingOverwrites: new Set(),
});
mockCheckOriginConflicts.mockResolvedValue({
errors: [],
importStateMap: new Map(),
pendingOverwrites: new Set(),
});
mockGetImportStateMapForRetries.mockReturnValue(new Map());
mockSplitOverwrites.mockReturnValue({
@ -304,6 +310,32 @@ describe('#importSavedObjectsFromStream', () => {
expect(mockCheckConflicts).toHaveBeenCalledWith(checkConflictsParams);
});
test('checks origin conflicts', async () => {
const retries = [createRetry()];
const options = setupOptions({ retries });
const filteredObjects = [createObject()];
const importStateMap = new Map();
const pendingOverwrites = new Set<string>();
mockCheckConflicts.mockResolvedValue({
errors: [],
filteredObjects,
importStateMap,
pendingOverwrites,
});
await resolveSavedObjectsImportErrors(options);
const checkOriginConflictsParams = {
objects: filteredObjects,
savedObjectsClient,
typeRegistry,
namespace,
importStateMap,
pendingOverwrites,
retries,
};
expect(mockCheckOriginConflicts).toHaveBeenCalledWith(checkOriginConflictsParams);
});
test('gets import ID map for retries', async () => {
const retries = [createRetry()];
const createNewCopies = Symbol() as unknown as boolean;
@ -313,7 +345,7 @@ describe('#importSavedObjectsFromStream', () => {
errors: [],
filteredObjects,
importStateMap: new Map(),
pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type
pendingOverwrites: new Set(),
});
await resolveSavedObjectsImportErrors(options);
@ -357,28 +389,49 @@ describe('#importSavedObjectsFromStream', () => {
test('creates saved objects', async () => {
const options = setupOptions();
const errors = [createError(), createError(), createError()];
const errors = [createError(), createError(), createError(), createError()];
mockCollectSavedObjects.mockResolvedValue({
errors: [errors[0]],
collectedObjects: [], // doesn't matter
importStateMap: new Map(), // doesn't matter
importStateMap: new Map([
['a', {}],
['b', {}],
['c', {}],
['d', { isOnlyReference: true }],
]),
});
mockCheckReferenceOrigins.mockResolvedValue({
importStateMap: new Map([['d', { isOnlyReference: true, destinationId: 'newId-d' }]]),
});
mockValidateReferences.mockResolvedValue([errors[1]]);
mockCheckConflicts.mockResolvedValue({
errors: [errors[2]],
filteredObjects: [],
importStateMap: new Map([['foo', { destinationId: 'someId' }]]),
pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type
importStateMap: new Map([
['b', { destinationId: 'newId-b2' }],
['c', { destinationId: 'newId-c2' }],
]),
pendingOverwrites: new Set(),
});
mockCheckOriginConflicts.mockResolvedValue({
errors: [errors[3]],
importStateMap: new Map([['c', { destinationId: 'newId-c3' }]]),
pendingOverwrites: new Set(),
});
mockGetImportStateMapForRetries.mockReturnValue(
new Map([
['foo', { destinationId: 'newId' }],
['bar', { destinationId: 'anotherNewId' }],
['a', { destinationId: 'newId-a1' }],
['b', { destinationId: 'newId-b1' }],
['c', { destinationId: 'newId-c1' }],
])
);
// assert that the importStateMap is correctly composed of the results from the four modules
const importStateMap = new Map([
['foo', { destinationId: 'someId' }],
['bar', { destinationId: 'anotherNewId' }],
['a', { destinationId: 'newId-a1' }],
['b', { destinationId: 'newId-b2' }],
['c', { destinationId: 'newId-c3' }],
['d', { isOnlyReference: true, destinationId: 'newId-d' }],
]);
const objectsToOverwrite = [createObject()];
const objectsToNotOverwrite = [createObject()];
@ -421,6 +474,19 @@ describe('#importSavedObjectsFromStream', () => {
expect(mockRegenerateIds).toHaveBeenCalledWith(collectedObjects);
});
test('does not check origin conflicts', async () => {
const options = setupOptions({ createNewCopies: true });
const collectedObjects = [createObject()];
mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects,
importStateMap: new Map(), // doesn't matter
});
await resolveSavedObjectsImportErrors(options);
expect(mockCheckOriginConflicts).not.toHaveBeenCalled();
});
test('creates saved objects', async () => {
const options = setupOptions({ createNewCopies: true });
const errors = [createError(), createError(), createError()];
@ -428,42 +494,42 @@ describe('#importSavedObjectsFromStream', () => {
errors: [errors[0]],
collectedObjects: [], // doesn't matter
importStateMap: new Map([
['foo', {}],
['bar', {}],
['baz', {}],
['qux', { isOnlyReference: true }],
['a', {}],
['b', {}],
['c', {}],
['d', { isOnlyReference: true }],
]),
});
mockCheckReferenceOrigins.mockResolvedValue({
importStateMap: new Map([['qux', { isOnlyReference: true, destinationId: 'newId1' }]]),
importStateMap: new Map([['d', { isOnlyReference: true, destinationId: 'newId-d' }]]),
});
mockValidateReferences.mockResolvedValue([errors[1]]);
mockRegenerateIds.mockReturnValue(
new Map([
['foo', { destinationId: 'randomId1' }],
['bar', { destinationId: 'randomId2' }],
['baz', { destinationId: 'randomId3' }],
['a', { destinationId: 'randomId-a' }],
['b', { destinationId: 'randomId-b' }],
['c', { destinationId: 'randomId-c' }],
])
);
mockCheckConflicts.mockResolvedValue({
errors: [errors[2]],
filteredObjects: [],
importStateMap: new Map([['bar', { destinationId: 'someId' }]]),
pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type
importStateMap: new Map([['c', { destinationId: 'newId-c2' }]]),
pendingOverwrites: new Set(),
});
mockGetImportStateMapForRetries.mockReturnValue(
new Map([
['bar', { destinationId: 'newId2' }], // this is overridden by the checkConflicts result
['baz', { destinationId: 'newId3' }],
['b', { destinationId: 'newId-b1' }],
['c', { destinationId: 'newId-c1' }],
])
);
// assert that the importStateMap is correctly composed of the results from the five modules
const importStateMap = new Map([
['foo', { destinationId: 'randomId1' }],
['bar', { destinationId: 'someId' }],
['baz', { destinationId: 'newId3' }],
['qux', { isOnlyReference: true, destinationId: 'newId1' }],
['a', { destinationId: 'randomId-a' }],
['b', { destinationId: 'newId-b1' }],
['c', { destinationId: 'newId-c2' }],
['d', { isOnlyReference: true, destinationId: 'newId-d' }],
]);
const objectsToOverwrite = [createObject()];
const objectsToNotOverwrite = [createObject()];

View file

@ -27,6 +27,8 @@ import {
getImportStateMapForRetries,
checkConflicts,
executeImportHooks,
checkOriginConflicts,
ImportStateMap,
} from './lib';
/**
@ -150,6 +152,24 @@ export async function resolveSavedObjectsImportErrors({
};
const checkConflictsResult = await checkConflicts(checkConflictsParams);
errorAccumulator = [...errorAccumulator, ...checkConflictsResult.errors];
importStateMap = new Map([...importStateMap, ...checkConflictsResult.importStateMap]);
let originConflictsImportStateMap: ImportStateMap = new Map();
if (!createNewCopies) {
// If createNewCopies is *not* enabled, check multi-namespace object types for origin conflicts in this namespace
const checkOriginConflictsParams = {
objects: checkConflictsResult.filteredObjects,
savedObjectsClient,
typeRegistry,
namespace,
importStateMap,
pendingOverwrites: checkConflictsResult.pendingOverwrites,
retries,
};
const checkOriginConflictsResult = await checkOriginConflicts(checkOriginConflictsParams);
errorAccumulator = [...errorAccumulator, ...checkOriginConflictsResult.errors];
originConflictsImportStateMap = checkOriginConflictsResult.importStateMap;
}
// Check multi-namespace object types for regular conflicts and ambiguous conflicts
const getImportStateMapForRetriesParams = {
@ -161,7 +181,9 @@ export async function resolveSavedObjectsImportErrors({
importStateMap = new Map([
...importStateMap,
...importStateMapForRetries,
...checkConflictsResult.importStateMap, // this importStateMap takes precedence over the others
// the importStateMap entries from checkConflicts and checkOriginConflicts take precedence over the others
...checkConflictsResult.importStateMap,
...originConflictsImportStateMap,
]);
// Bulk create in two batches, overwrites and non-overwrites

View file

@ -767,3 +767,38 @@
"type": "doc"
}
}
{
"type": "doc",
"value": {
"id": "sharedtype:outbound-missing-reference-conflict-1",
"index": ".kibana",
"source": {
"sharedtype": {
"title": "This is used to test if an imported object with a missing reference will have an exact match conflict when the missing reference is replaced"
},
"type": "sharedtype",
"namespaces": ["*"],
"updated_at": "2017-09-21T18:59:16.270Z"
},
"type": "doc"
}
}
{
"type": "doc",
"value": {
"id": "sharedtype:outbound-missing-reference-conflict-2a",
"index": ".kibana",
"source": {
"originId": "outbound-missing-reference-conflict-2",
"sharedtype": {
"title": "This is used to test if an imported object with a missing reference will have an inexact match conflict when the missing reference is replaced"
},
"type": "sharedtype",
"namespaces": ["*"],
"updated_at": "2017-09-21T18:59:16.270Z"
},
"type": "doc"
}
}

View file

@ -106,3 +106,20 @@ export const CONFLICT_TEST_CASES: Record<string, CommonTestCase> = Object.freeze
expectedNamespaces: EACH_SPACE,
}),
});
/**
* These objects exist in the test data for all saved object test suites, but they are only used to test specific scenarios.
*/
export const OTHER_TEST_CASES: Record<string, CommonTestCase> = Object.freeze({
OUTBOUND_MISSING_REFERENCE_CONFLICT_1_OBJ: Object.freeze({
type: 'sharedtype',
id: 'outbound-missing-reference-conflict-1',
expectedNamespaces: [ALL_SPACES_ID],
}),
OUTBOUND_MISSING_REFERENCE_CONFLICT_2A_OBJ: Object.freeze({
type: 'sharedtype',
id: 'outbound-missing-reference-conflict-2a',
originId: 'outbound-missing-reference-conflict-2',
expectedNamespaces: [ALL_SPACES_ID],
}),
});

View file

@ -7,7 +7,11 @@
import expect from '@kbn/expect';
import { SuperTest } from 'supertest';
import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases';
import {
SAVED_OBJECT_TEST_CASES,
CONFLICT_TEST_CASES,
OTHER_TEST_CASES,
} from '../lib/saved_object_test_cases';
import { SPACES } from '../lib/spaces';
import { expectResponses, getUrlPrefix } from '../lib/saved_object_test_utils';
import { ExpectResponseBody, TestDefinition, TestSuite } from '../lib/types';
@ -41,13 +45,11 @@ export interface ExportTestCase {
};
}
// additional sharedtype objects that exist but do not have common test cases defined
const CID = 'conflict_';
const CONFLICT_1_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}1` });
const CONFLICT_2A_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}2a`, originId: `${CID}2` });
const CONFLICT_2B_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}2b`, originId: `${CID}2` });
const CONFLICT_3_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}3` });
const CONFLICT_4A_OBJ = Object.freeze({ type: 'sharedtype', id: `${CID}4a`, originId: `${CID}4` });
const CASES = {
...SAVED_OBJECT_TEST_CASES,
...CONFLICT_TEST_CASES,
...OTHER_TEST_CASES,
};
export const getTestCases = (spaceId?: string): { [key: string]: ExportTestCase } => ({
singleNamespaceObject: {
@ -86,10 +88,16 @@ export const getTestCases = (spaceId?: string): { [key: string]: ExportTestCase
? [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, CASES.MULTI_NAMESPACE_ONLY_SPACE_1]
: spaceId === SPACE_2_ID
? [CASES.MULTI_NAMESPACE_ONLY_SPACE_2]
: [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1]
)
.concat([CONFLICT_1_OBJ, CONFLICT_2A_OBJ, CONFLICT_2B_OBJ, CONFLICT_3_OBJ, CONFLICT_4A_OBJ])
.flat(),
: [CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1]),
...[
CASES.CONFLICT_1_OBJ,
CASES.CONFLICT_2A_OBJ,
CASES.CONFLICT_2B_OBJ,
CASES.CONFLICT_3_OBJ,
CASES.CONFLICT_4A_OBJ,
CASES.OUTBOUND_MISSING_REFERENCE_CONFLICT_1_OBJ,
CASES.OUTBOUND_MISSING_REFERENCE_CONFLICT_2A_OBJ,
],
],
},
...(spaceId !== SPACE_2_ID && {

View file

@ -8,7 +8,11 @@
import expect from '@kbn/expect';
import { SuperTest } from 'supertest';
import querystring from 'querystring';
import { SAVED_OBJECT_TEST_CASES, CONFLICT_TEST_CASES } from '../lib/saved_object_test_cases';
import {
SAVED_OBJECT_TEST_CASES,
CONFLICT_TEST_CASES,
OTHER_TEST_CASES,
} from '../lib/saved_object_test_cases';
import { SPACES, ALL_SPACES_ID } from '../lib/spaces';
import {
getUrlPrefix,
@ -44,6 +48,7 @@ export interface FindTestCase {
const TEST_CASES = [
...Object.values(SAVED_OBJECT_TEST_CASES),
...Object.values(CONFLICT_TEST_CASES),
...Object.values(OTHER_TEST_CASES),
];
export const getTestCases = (

View file

@ -37,6 +37,7 @@ export type ResolveImportErrorsTestSuite = TestSuite<ResolveImportErrorsTestDefi
export type FailureType = 'unsupported_type' | 'conflict';
export interface ResolveImportErrorsTestCase extends Omit<TestCase, 'failure'> {
originId?: string;
destinationId?: string;
expectedNewId?: string;
references?: SavedObjectReference[];
replaceReferences?: SavedObjectsImportRetry['replaceReferences'];
@ -61,34 +62,75 @@ export const TEST_CASES: Record<string, ResolveImportErrorsTestCase> = Object.fr
type: 'sharedtype',
id: `conflict_1a`,
originId: `conflict_1`,
destinationId: 'some-random-id',
expectedNewId: 'some-random-id',
}),
CONFLICT_1B_OBJ: Object.freeze({
type: 'sharedtype',
id: `conflict_1b`,
originId: `conflict_1`,
destinationId: 'another-random-id',
expectedNewId: 'another-random-id',
}),
CONFLICT_2C_OBJ: Object.freeze({
type: 'sharedtype',
id: `conflict_2c`,
originId: `conflict_2`,
destinationId: `conflict_2a`,
expectedNewId: `conflict_2a`,
}),
CONFLICT_2D_OBJ: Object.freeze({
type: 'sharedtype',
id: `conflict_2d`,
originId: `conflict_2`,
// destinationId is undefined on purpose
expectedNewId: `conflict_2b`, // since conflict_2c was matched with conflict_2a, this (conflict_2d) will result in a regular inexact match conflict with conflict_2b
}),
CONFLICT_3A_OBJ: Object.freeze({
type: 'sharedtype',
id: `conflict_3a`,
originId: `conflict_3`,
destinationId: `conflict_3`,
expectedNewId: `conflict_3`,
}),
CONFLICT_4_OBJ: Object.freeze({
type: 'sharedtype',
id: `conflict_4`,
destinationId: `conflict_4a`,
expectedNewId: `conflict_4a`,
}),
});
export const SPECIAL_TEST_CASES: Record<string, ResolveImportErrorsTestCase> = Object.freeze({
HIDDEN,
OUTBOUND_MISSING_REFERENCE_CONFLICT_1_OBJ: Object.freeze({
// This object has an exact match that already exists, *and* it has a reference to an index pattern that doesn't exist.
// We are choosing to replace the reference here, so Kibana should detect if there is a conflict and respond appropriately.
type: 'sharedtype',
id: 'outbound-missing-reference-conflict-1',
references: [{ name: '1', type: 'index-pattern', id: 'missing' }],
replaceReferences: [
{
type: 'index-pattern',
from: 'missing',
to: 'inbound-reference-origin-match-2b', // specific ID doesn't matter, just needs to be an index pattern that exists in all spaces
},
],
}),
OUTBOUND_MISSING_REFERENCE_CONFLICT_2_OBJ: Object.freeze({
// This object has an inexact match that already exists, *and* it has a reference to an index pattern that doesn't exist.
// We are choosing to replace the reference here, so Kibana should detect if there is a conflict and respond appropriately.
type: 'sharedtype',
id: 'outbound-missing-reference-conflict-2',
expectedNewId: `outbound-missing-reference-conflict-2a`,
references: [{ name: '1', type: 'index-pattern', id: 'missing' }],
replaceReferences: [
{
type: 'index-pattern',
from: 'missing',
to: 'inbound-reference-origin-match-2b', // specific ID doesn't matter, just needs to be an index pattern that exists in all spaces
},
],
}),
OUTBOUND_REFERENCE_ORIGIN_MATCH_1_OBJ: Object.freeze({
// This object does not already exist, but it has a reference to the originId of an index pattern that does exist.
// We use index patterns because they are one of the few reference types that are validated, so the import will fail if the reference
@ -125,7 +167,7 @@ const createRequest = (
type,
id,
originId,
expectedNewId,
destinationId,
references,
replaceReferences,
successParam,
@ -138,7 +180,7 @@ const createRequest = (
type,
id,
overwrite,
...(expectedNewId && { destinationId: expectedNewId }),
...(destinationId && { destinationId }),
...(replaceReferences && { replaceReferences }),
...(successParam === 'createNewCopy' && { createNewCopy: true }),
},

View file

@ -31,11 +31,15 @@ const newCopy = () => ({ successParam: 'createNewCopy' });
const createNewCopiesTestCases = () => {
// for each outcome, if failure !== undefined then we expect to receive
// an error; otherwise, we expect to receive a success result
const importable = Object.entries(CASES).map(([, val]) => ({
...val,
successParam: 'createNewCopies',
expectedNewId: uuidv4(),
}));
const importable = Object.values(CASES).map((testCase) => {
const newId = uuidv4();
return {
...testCase,
successParam: 'createNewCopies',
destinationId: newId,
expectedNewId: newId,
};
});
const nonImportable = [{ ...SPECIAL_TEST_CASES.HIDDEN, ...failUnsupportedType() }]; // unsupported_type is an "unresolvable" error
const all = [...importable, ...nonImportable];
return { importable, nonImportable, all };
@ -89,11 +93,21 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
// if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that
// `expectedDestinationId` already exists
{ ...CASES.CONFLICT_2C_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a'
{ ...CASES.CONFLICT_2D_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2b'
{ ...CASES.CONFLICT_3A_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3'
{ ...CASES.CONFLICT_4_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a'
];
const refOrigins = [
// These are in a separate group because they will result in a different 403 error for users who are unauthorized to read
{
...SPECIAL_TEST_CASES.OUTBOUND_MISSING_REFERENCE_CONFLICT_1_OBJ,
...failConflict(!overwrite),
},
{
...SPECIAL_TEST_CASES.OUTBOUND_MISSING_REFERENCE_CONFLICT_2_OBJ,
...failConflict(!overwrite),
...destinationId(),
},
{ ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_1_OBJ },
{ ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_2_OBJ },
];

View file

@ -30,11 +30,15 @@ const createNewCopiesTestCases = () => {
// for each outcome, if failureType !== undefined then we expect to receive
// an error; otherwise, we expect to receive a success result
return [
...Object.entries(CASES).map(([, val]) => ({
...val,
successParam: 'createNewCopies',
expectedNewId: uuidv4(),
})),
...Object.values(CASES).map((testCase) => {
const newId = uuidv4();
return {
...testCase,
successParam: 'createNewCopies',
destinationId: newId,
expectedNewId: newId,
};
}),
{ ...SPECIAL_TEST_CASES.HIDDEN, ...failUnsupportedType() }, // unsupported_type is an "unresolvable" error
// Other special test cases are excluded here for simplicity and consistency with the resolveImportErrors "spaces_and_security" test
// suite and the import test suites.
@ -86,8 +90,18 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
// if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that
// `expectedDestinationId` already exists
{ ...CASES.CONFLICT_2C_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a'
{ ...CASES.CONFLICT_2D_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2b'
{ ...CASES.CONFLICT_3A_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3'
{ ...CASES.CONFLICT_4_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a'
{
...SPECIAL_TEST_CASES.OUTBOUND_MISSING_REFERENCE_CONFLICT_1_OBJ,
...failConflict(!overwrite),
},
{
...SPECIAL_TEST_CASES.OUTBOUND_MISSING_REFERENCE_CONFLICT_2_OBJ,
...failConflict(!overwrite),
...destinationId(),
},
{ ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_1_OBJ },
{ ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_2_OBJ },
];