(handler: RequestHandler) => RequestHandler
;
// @public
-export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise;
+export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, typeRegistry, namespace, trueCopy, }: SavedObjectsResolveImportErrorsOptions): Promise;
// @public
export type ResponseError = string | Error | {
@@ -1811,14 +1811,14 @@ export type SafeRouteMethod = 'get' | 'options';
// @public (undocumented)
export interface SavedObject {
attributes: T;
+ // Warning: (ae-forgotten-export) The symbol "SavedObjectError" needs to be exported by the entry point index.d.ts
+ //
// (undocumented)
- error?: {
- message: string;
- statusCode: number;
- };
+ error?: SavedObjectError;
id: string;
migrationVersion?: SavedObjectsMigrationVersion;
namespaces?: string[];
+ originId?: string;
references: SavedObjectReference[];
type: string;
updated_at?: string;
@@ -1885,6 +1885,7 @@ export interface SavedObjectsBulkCreateObject {
// (undocumented)
id?: string;
migrationVersion?: SavedObjectsMigrationVersion;
+ originId?: string;
// (undocumented)
references?: SavedObjectReference[];
// (undocumented)
@@ -1930,6 +1931,24 @@ export interface SavedObjectsBulkUpdateResponse {
saved_objects: Array>;
}
+// @public (undocumented)
+export interface SavedObjectsCheckConflictsObject {
+ // (undocumented)
+ id: string;
+ // (undocumented)
+ type: string;
+}
+
+// @public (undocumented)
+export interface SavedObjectsCheckConflictsResponse {
+ // (undocumented)
+ errors: Array<{
+ id: string;
+ type: string;
+ error: SavedObjectError;
+ }>;
+}
+
// @public (undocumented)
export class SavedObjectsClient {
// @internal
@@ -1938,6 +1957,7 @@ export class SavedObjectsClient {
bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>;
bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>;
bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>;
+ checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise;
create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>;
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>;
@@ -2014,6 +2034,7 @@ export interface SavedObjectsCoreFieldMapping {
export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
id?: string;
migrationVersion?: SavedObjectsMigrationVersion;
+ originId?: string;
overwrite?: boolean;
// (undocumented)
references?: SavedObjectReference[];
@@ -2132,6 +2153,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions {
// (undocumented)
perPage?: number;
preference?: string;
+ rawSearchFields?: string[];
search?: string;
searchFields?: string[];
// (undocumented)
@@ -2159,8 +2181,22 @@ export interface SavedObjectsFindResult extends SavedObject {
score: number;
}
+// @public
+export interface SavedObjectsImportAmbiguousConflictError {
+ // (undocumented)
+ destinations: Array<{
+ id: string;
+ title?: string;
+ updatedAt?: string;
+ }>;
+ // (undocumented)
+ type: 'ambiguous_conflict';
+}
+
// @public
export interface SavedObjectsImportConflictError {
+ // (undocumented)
+ destinationId?: string;
// (undocumented)
type: 'conflict';
}
@@ -2168,7 +2204,7 @@ export interface SavedObjectsImportConflictError {
// @public
export interface SavedObjectsImportError {
// (undocumented)
- error: SavedObjectsImportConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError;
+ error: SavedObjectsImportConflictError | SavedObjectsImportAmbiguousConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError;
// (undocumented)
id: string;
// (undocumented)
@@ -2197,10 +2233,13 @@ export interface SavedObjectsImportMissingReferencesError {
export interface SavedObjectsImportOptions {
namespace?: string;
objectLimit: number;
+ // @deprecated (undocumented)
overwrite: boolean;
readStream: Readable;
savedObjectsClient: SavedObjectsClientContract;
- supportedTypes: string[];
+ // @deprecated (undocumented)
+ trueCopy: boolean;
+ typeRegistry: ISavedObjectTypeRegistry;
}
// @public
@@ -2211,10 +2250,13 @@ export interface SavedObjectsImportResponse {
success: boolean;
// (undocumented)
successCount: number;
+ // (undocumented)
+ successResults?: SavedObjectsImportSuccess[];
}
// @public
export interface SavedObjectsImportRetry {
+ destinationId?: string;
// (undocumented)
id: string;
// (undocumented)
@@ -2225,6 +2267,19 @@ export interface SavedObjectsImportRetry {
from: string;
to: string;
}>;
+ // @deprecated (undocumented)
+ trueCopy?: boolean;
+ // (undocumented)
+ type: string;
+}
+
+// @public
+export interface SavedObjectsImportSuccess {
+ destinationId?: string;
+ // (undocumented)
+ id: string;
+ // @deprecated (undocumented)
+ trueCopy?: boolean;
// (undocumented)
type: string;
}
@@ -2330,6 +2385,7 @@ export class SavedObjectsRepository {
bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>;
bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>;
bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>;
+ checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise;
create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>;
// Warning: (ae-forgotten-export) The symbol "KibanaMigrator" needs to be exported by the entry point index.d.ts
//
@@ -2339,16 +2395,9 @@ export class SavedObjectsRepository {
deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise;
deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise<{}>;
// (undocumented)
- find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, preference, }: SavedObjectsFindOptions): Promise>;
+ find({ search, defaultSearchOperator, searchFields, rawSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, preference, }: SavedObjectsFindOptions): Promise>;
get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>;
- incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise<{
- id: string;
- type: string;
- updated_at: string;
- references: any;
- version: string;
- attributes: any;
- }>;
+ incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise;
update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
}
@@ -2365,7 +2414,9 @@ export interface SavedObjectsResolveImportErrorsOptions {
readStream: Readable;
retries: SavedObjectsImportRetry[];
savedObjectsClient: SavedObjectsClientContract;
- supportedTypes: string[];
+ // @deprecated (undocumented)
+ trueCopy: boolean;
+ typeRegistry: ISavedObjectTypeRegistry;
}
// @internal @deprecated (undocumented)
diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts
index 04aaacc3cf31a..9abc093c74fb3 100644
--- a/src/core/types/saved_objects.ts
+++ b/src/core/types/saved_objects.ts
@@ -86,10 +86,7 @@ export interface SavedObject {
version?: string;
/** Timestamp of the last time this document had been updated. */
updated_at?: string;
- error?: {
- message: string;
- statusCode: number;
- };
+ error?: SavedObjectError;
/** {@inheritdoc SavedObjectAttributes} */
attributes: T;
/** {@inheritdoc SavedObjectReference} */
@@ -98,4 +95,18 @@ export interface SavedObject {
migrationVersion?: SavedObjectsMigrationVersion;
/** Namespace(s) that this saved object exists in. This attribute is only used for multi-namespace saved object types. */
namespaces?: string[];
+ /**
+ * The ID of the saved object this originated from. This is set if this object's `id` was regenerated; that can happen during migration
+ * from a legacy single-namespace type, or during import. It is only set during migration or create operations. This is used during import
+ * to ensure that ID regeneration is deterministic, so saved objects will be overwritten if they are imported multiple times into a given
+ * space.
+ */
+ originId?: string;
+}
+
+export interface SavedObjectError {
+ error: string;
+ message: string;
+ statusCode: number;
+ metadata?: Record;
}
diff --git a/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts
index c1a153b800550..cd35d4d726400 100644
--- a/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts
+++ b/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts
@@ -19,6 +19,7 @@
import {
SavedObjectsImportConflictError,
+ SavedObjectsImportAmbiguousConflictError,
SavedObjectsImportUnknownError,
SavedObjectsImportMissingReferencesError,
} from 'src/core/public';
@@ -35,7 +36,7 @@ describe('processImportResponse()', () => {
expect(result.importCount).toBe(0);
});
- test('conflict errors get added to failedImports', () => {
+ test('conflict errors get added to failedImports and result in idle status', () => {
const response = {
success: false,
successCount: 0,
@@ -63,9 +64,41 @@ describe('processImportResponse()', () => {
},
]
`);
+ expect(result.status).toBe('idle');
});
- test('unknown errors get added to failedImports', () => {
+ test('ambiguous conflict errors get added to failedImports and result in idle status', () => {
+ const response = {
+ success: false,
+ successCount: 0,
+ errors: [
+ {
+ type: 'a',
+ id: '1',
+ error: {
+ type: 'ambiguous_conflict',
+ } as SavedObjectsImportAmbiguousConflictError,
+ },
+ ],
+ };
+ const result = processImportResponse(response);
+ expect(result.failedImports).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "error": Object {
+ "type": "ambiguous_conflict",
+ },
+ "obj": Object {
+ "id": "1",
+ "type": "a",
+ },
+ },
+ ]
+ `);
+ expect(result.status).toBe('idle');
+ });
+
+ test('unknown errors get added to failedImports and result in success status', () => {
const response = {
success: false,
successCount: 0,
@@ -93,9 +126,10 @@ describe('processImportResponse()', () => {
},
]
`);
+ expect(result.status).toBe('success');
});
- test('missing references get added to failedImports', () => {
+ test('missing references get added to failedImports and result in idle status', () => {
const response = {
success: false,
successCount: 0,
@@ -135,5 +169,6 @@ describe('processImportResponse()', () => {
},
]
`);
+ expect(result.status).toBe('idle');
});
});
diff --git a/src/plugins/saved_objects_management/public/lib/process_import_response.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.ts
index 4725000aa9d55..ece8c924f8885 100644
--- a/src/plugins/saved_objects_management/public/lib/process_import_response.ts
+++ b/src/plugins/saved_objects_management/public/lib/process_import_response.ts
@@ -20,6 +20,7 @@
import {
SavedObjectsImportResponse,
SavedObjectsImportConflictError,
+ SavedObjectsImportAmbiguousConflictError,
SavedObjectsImportUnsupportedTypeError,
SavedObjectsImportMissingReferencesError,
SavedObjectsImportUnknownError,
@@ -30,6 +31,7 @@ export interface FailedImport {
obj: Pick;
error:
| SavedObjectsImportConflictError
+ | SavedObjectsImportAmbiguousConflictError
| SavedObjectsImportUnsupportedTypeError
| SavedObjectsImportMissingReferencesError
| SavedObjectsImportUnknownError;
@@ -48,6 +50,9 @@ export interface ProcessedImportResponse {
conflictedSearchDocs: undefined;
}
+const isConflict = ({ type }: FailedImport['error']) =>
+ type === 'conflict' || type === 'ambiguous_conflict';
+
export function processImportResponse(
response: SavedObjectsImportResponse
): ProcessedImportResponse {
@@ -80,8 +85,7 @@ export function processImportResponse(
// Import won't be successful in the scenario unmatched references exist, import API returned errors of type unknown or import API
// returned errors of type missing_references.
status:
- unmatchedReferences.size === 0 &&
- !failedImports.some((issue) => issue.error.type === 'conflict')
+ unmatchedReferences.size === 0 && !failedImports.some((issue) => isConflict(issue.error))
? 'success'
: 'idle',
importCount: response.successCount,
diff --git a/test/api_integration/apis/saved_objects/import.js b/test/api_integration/apis/saved_objects/import.js
index fbacfe458d976..fe0849eb7bcab 100644
--- a/test/api_integration/apis/saved_objects/import.js
+++ b/test/api_integration/apis/saved_objects/import.js
@@ -40,6 +40,11 @@ export default function ({ getService }) {
expect(resp.body).to.eql({
success: true,
successCount: 3,
+ successResults: [
+ { type: 'index-pattern', id: '91200a00-9efd-11e7-acb3-3dab96693fab' },
+ { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab' },
+ { type: 'dashboard', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab' },
+ ],
});
});
});
@@ -108,6 +113,11 @@ export default function ({ getService }) {
expect(resp.body).to.eql({
success: true,
successCount: 3,
+ successResults: [
+ { type: 'index-pattern', id: '91200a00-9efd-11e7-acb3-3dab96693fab' },
+ { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab' },
+ { type: 'dashboard', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab' },
+ ],
});
});
});
diff --git a/test/api_integration/apis/saved_objects/migrations.js b/test/api_integration/apis/saved_objects/migrations.js
index d0ff4cc06c57e..4a18617cc2143 100644
--- a/test/api_integration/apis/saved_objects/migrations.js
+++ b/test/api_integration/apis/saved_objects/migrations.js
@@ -292,13 +292,10 @@ export default ({ getService }) => {
);
// It only created the original and the dest
- assert.deepEqual(
- _.pluck(
- await callCluster('cat.indices', { index: '.migration-c*', format: 'json' }),
- 'index'
- ).sort(),
- ['.migration-c_1', '.migration-c_2']
- );
+ const indices = (await callCluster('cat.indices', { index: '.migration-c*', format: 'json' }))
+ .map(({ index }) => index)
+ .sort();
+ assert.deepEqual(indices, ['.migration-c_1', '.migration-c_2']);
// The docs in the original index are unchanged
assert.deepEqual(await fetchDocs({ callCluster, index: `${index}_1` }), [
diff --git a/test/api_integration/apis/saved_objects/resolve_import_errors.js b/test/api_integration/apis/saved_objects/resolve_import_errors.js
index aacfcd4382fac..093bfc74c60d4 100644
--- a/test/api_integration/apis/saved_objects/resolve_import_errors.js
+++ b/test/api_integration/apis/saved_objects/resolve_import_errors.js
@@ -72,6 +72,11 @@ export default function ({ getService }) {
expect(resp.body).to.eql({
success: true,
successCount: 3,
+ successResults: [
+ { type: 'index-pattern', id: '91200a00-9efd-11e7-acb3-3dab96693fab' },
+ { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab' },
+ { type: 'dashboard', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab' },
+ ],
});
});
});
@@ -234,7 +239,15 @@ export default function ({ getService }) {
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
.expect(200)
.then((resp) => {
- expect(resp.body).to.eql({ success: true, successCount: 3 });
+ expect(resp.body).to.eql({
+ success: true,
+ successCount: 3,
+ successResults: [
+ { type: 'index-pattern', id: '91200a00-9efd-11e7-acb3-3dab96693fab' },
+ { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab' },
+ { type: 'dashboard', id: 'be3733a0-9efe-11e7-acb3-3dab96693fab' },
+ ],
+ });
});
});
@@ -254,7 +267,13 @@ export default function ({ getService }) {
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
.expect(200)
.then((resp) => {
- expect(resp.body).to.eql({ success: true, successCount: 1 });
+ expect(resp.body).to.eql({
+ success: true,
+ successCount: 1,
+ successResults: [
+ { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab' },
+ ],
+ });
});
});
@@ -298,6 +317,7 @@ export default function ({ getService }) {
expect(resp.body).to.eql({
success: true,
successCount: 1,
+ successResults: [{ type: 'visualization', id: '1' }],
});
});
await supertest
diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts
index eea19bb1aa7dd..d08c715f7dbfa 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts
@@ -42,6 +42,19 @@ beforeEach(() => {
afterEach(() => jest.clearAllMocks());
+describe('#checkConflicts', () => {
+ it('redirects request to underlying base client', async () => {
+ const objects = [{ type: 'foo', id: 'bar' }];
+ const options = { namespace: 'some-namespace' };
+ const mockedResponse = { errors: [] };
+ mockBaseClient.checkConflicts.mockResolvedValue(mockedResponse);
+
+ await expect(wrapper.checkConflicts(objects, options)).resolves.toEqual(mockedResponse);
+ expect(mockBaseClient.checkConflicts).toHaveBeenCalledTimes(1);
+ expect(mockBaseClient.checkConflicts).toHaveBeenCalledWith(objects, options);
+ });
+});
+
describe('#create', () => {
it('redirects request to underlying base client if type is not registered', async () => {
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts
index bdc2b6cb2e667..e1819367b4c58 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts
@@ -13,6 +13,7 @@ import {
SavedObjectsBulkUpdateObject,
SavedObjectsBulkResponse,
SavedObjectsBulkUpdateResponse,
+ SavedObjectsCheckConflictsObject,
SavedObjectsClientContract,
SavedObjectsCreateOptions,
SavedObjectsFindOptions,
@@ -51,6 +52,13 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
private getDescriptorNamespace = (type: string, namespace?: string) =>
this.options.baseTypeRegistry.isSingleNamespace(type) ? namespace : undefined;
+ public async checkConflicts(
+ objects: SavedObjectsCheckConflictsObject[] = [],
+ options?: SavedObjectsBaseOptions
+ ) {
+ return await this.options.baseClient.checkConflicts(objects, options);
+ }
+
public async create(
type: string,
attributes: T = {} as T,
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts
index b623295c5e060..f1c22b7b38194 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts
@@ -9,7 +9,11 @@ import { AssetType } from '../../../types';
import * as Registry from '../registry';
type ArchiveAsset = Pick;
-type SavedObjectToBe = Required & { type: AssetType };
+type SavedObjectToBe = Required<
+ Pick
+> & {
+ type: AssetType;
+};
export async function getObject(key: string) {
const buffer = Registry.getAsset(key);
diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts
index c646cd95228f0..01a9504c62fed 100644
--- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts
+++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts
@@ -61,7 +61,7 @@ const expectGeneralError = async (fn: Function, args: Record) => {
* Requires that function args are passed in as key/value pairs
* The argument properties must be in the correct order to be spread properly
*/
-const expectForbiddenError = async (fn: Function, args: Record) => {
+const expectForbiddenError = async (fn: Function, args: Record, action?: string) => {
clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation(
getMockCheckPrivilegesFailure
);
@@ -84,7 +84,7 @@ const expectForbiddenError = async (fn: Function, args: Record) =>
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1);
expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
USERNAME,
- ACTION,
+ action ?? ACTION,
types,
spaceIds,
missing,
@@ -93,7 +93,7 @@ const expectForbiddenError = async (fn: Function, args: Record) =>
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
};
-const expectSuccess = async (fn: Function, args: Record) => {
+const expectSuccess = async (fn: Function, args: Record, action?: string) => {
const result = await fn.bind(client)(...Object.values(args));
const getCalls = (clientOpts.actions.savedObject.get as jest.MockedFunction<
SavedObjectActions['get']
@@ -106,7 +106,7 @@ const expectSuccess = async (fn: Function, args: Record) => {
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1);
expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(
USERNAME,
- ACTION,
+ action ?? ACTION,
types,
spaceIds,
args
@@ -474,6 +474,40 @@ describe('#bulkUpdate', () => {
});
});
+describe('#checkConflicts', () => {
+ const obj1 = Object.freeze({ type: 'foo', id: 'foo-id' });
+ const obj2 = Object.freeze({ type: 'bar', id: 'bar-id' });
+ const options = Object.freeze({ namespace: 'some-ns' });
+
+ test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => {
+ const objects = [obj1, obj2];
+ await expectGeneralError(client.checkConflicts, { objects });
+ });
+
+ test(`throws decorated ForbiddenError when unauthorized`, async () => {
+ const objects = [obj1, obj2];
+ await expectForbiddenError(client.checkConflicts, { objects, options }, 'checkConflicts');
+ });
+
+ test(`returns result of baseClient.create when authorized`, async () => {
+ const apiCallReturnValue = Symbol();
+ clientOpts.baseClient.checkConflicts.mockResolvedValue(apiCallReturnValue as any);
+
+ const objects = [obj1, obj2];
+ const result = await expectSuccess(
+ client.checkConflicts,
+ { objects, options },
+ 'checkConflicts'
+ );
+ expect(result).toBe(apiCallReturnValue);
+ });
+
+ test(`checks privileges for user, actions, and namespace`, async () => {
+ const objects = [obj1, obj2];
+ await expectPrivilegeCheck(client.checkConflicts, { objects, options });
+ });
+});
+
describe('#create', () => {
const type = 'foo';
const attributes = { some_attr: 's' };
diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts
index 969344afae5e3..8cebcf6140ca7 100644
--- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts
+++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts
@@ -9,6 +9,7 @@ import {
SavedObjectsBulkCreateObject,
SavedObjectsBulkGetObject,
SavedObjectsBulkUpdateObject,
+ SavedObjectsCheckConflictsObject,
SavedObjectsClientContract,
SavedObjectsCreateOptions,
SavedObjectsFindOptions,
@@ -77,6 +78,18 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra
return await this.redactSavedObjectNamespaces(savedObject);
}
+ public async checkConflicts(
+ objects: SavedObjectsCheckConflictsObject[] = [],
+ options: SavedObjectsBaseOptions = {}
+ ) {
+ const types = this.getUniqueObjectTypes(objects);
+ const args = { objects, options };
+ await this.ensureAuthorized(types, 'bulk_create', options.namespace, args, 'checkConflicts');
+
+ const response = await this.baseClient.checkConflicts(objects, options);
+ return response;
+ }
+
public async bulkCreate(
objects: Array>,
options: SavedObjectsBaseOptions = {}
diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts
index 9679dd8c52523..d02e21f9c222d 100644
--- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts
+++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts
@@ -7,10 +7,11 @@ import {
SavedObjectsImportResponse,
SavedObjectsImportOptions,
SavedObjectsExportOptions,
+ SavedObjectsImportSuccess,
} from 'src/core/server';
import { copySavedObjectsToSpacesFactory } from './copy_to_spaces';
import { Readable } from 'stream';
-import { coreMock, savedObjectsTypeRegistryMock, httpServerMock } from 'src/core/server/mocks';
+import { coreMock, httpServerMock } from 'src/core/server/mocks';
jest.mock('../../../../../../src/core/server', () => {
return {
@@ -53,34 +54,6 @@ describe('copySavedObjectsToSpaces', () => {
const setup = (setupOpts: SetupOpts) => {
const coreStart = coreMock.createStart();
- const typeRegistry = savedObjectsTypeRegistryMock.create();
- typeRegistry.getAllTypes.mockReturnValue([
- {
- name: 'dashboard',
- namespaceType: 'single',
- hidden: false,
- mappings: { properties: {} },
- },
- {
- name: 'visualization',
- namespaceType: 'single',
- hidden: false,
- mappings: { properties: {} },
- },
- {
- name: 'globaltype',
- namespaceType: 'agnostic',
- hidden: false,
- mappings: { properties: {} },
- },
- ]);
-
- typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) =>
- typeRegistry.getAllTypes().some((t) => t.name === type && t.namespaceType === 'agnostic')
- );
-
- coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry);
-
(exportSavedObjectsToStream as jest.Mock).mockImplementation(
async (opts: SavedObjectsExportOptions) => {
return (
@@ -104,6 +77,9 @@ describe('copySavedObjectsToSpaces', () => {
const response: SavedObjectsImportResponse = {
success: true,
successCount: setupOpts.objects.length,
+ successResults: [
+ ('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess,
+ ],
};
return Promise.resolve(response);
@@ -156,6 +132,7 @@ describe('copySavedObjectsToSpaces', () => {
id: 'my-dashboard',
},
],
+ trueCopy: false,
});
expect(result).toMatchInlineSnapshot(`
@@ -164,11 +141,17 @@ describe('copySavedObjectsToSpaces', () => {
"errors": undefined,
"success": true,
"successCount": 3,
+ "successResults": Array [
+ "Some success(es) occurred!",
+ ],
},
"destination2": Object {
"errors": undefined,
"success": true,
"successCount": 3,
+ "successResults": Array [
+ "Some success(es) occurred!",
+ ],
},
}
`);
@@ -192,6 +175,7 @@ describe('copySavedObjectsToSpaces', () => {
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
+ "checkConflicts": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"deleteFromNamespaces": [MockFunction],
@@ -258,6 +242,7 @@ describe('copySavedObjectsToSpaces', () => {
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
+ "checkConflicts": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"deleteFromNamespaces": [MockFunction],
@@ -266,10 +251,19 @@ describe('copySavedObjectsToSpaces', () => {
"get": [MockFunction],
"update": [MockFunction],
},
- "supportedTypes": Array [
- "dashboard",
- "visualization",
- ],
+ "trueCopy": false,
+ "typeRegistry": Object {
+ "getAllTypes": [MockFunction],
+ "getImportableAndExportableTypes": [MockFunction],
+ "getIndex": [MockFunction],
+ "getType": [MockFunction],
+ "isHidden": [MockFunction],
+ "isImportableAndExportable": [MockFunction],
+ "isMultiNamespace": [MockFunction],
+ "isNamespaceAgnostic": [MockFunction],
+ "isSingleNamespace": [MockFunction],
+ "registerType": [MockFunction],
+ },
},
],
Array [
@@ -323,6 +317,7 @@ describe('copySavedObjectsToSpaces', () => {
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
+ "checkConflicts": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"deleteFromNamespaces": [MockFunction],
@@ -331,10 +326,19 @@ describe('copySavedObjectsToSpaces', () => {
"get": [MockFunction],
"update": [MockFunction],
},
- "supportedTypes": Array [
- "dashboard",
- "visualization",
- ],
+ "trueCopy": false,
+ "typeRegistry": Object {
+ "getAllTypes": [MockFunction],
+ "getImportableAndExportableTypes": [MockFunction],
+ "getIndex": [MockFunction],
+ "getType": [MockFunction],
+ "isHidden": [MockFunction],
+ "isImportableAndExportable": [MockFunction],
+ "isMultiNamespace": [MockFunction],
+ "isNamespaceAgnostic": [MockFunction],
+ "isSingleNamespace": [MockFunction],
+ "registerType": [MockFunction],
+ },
},
],
]
@@ -370,6 +374,7 @@ describe('copySavedObjectsToSpaces', () => {
return Promise.resolve({
success: true,
successCount: 3,
+ successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess],
});
},
});
@@ -394,6 +399,7 @@ describe('copySavedObjectsToSpaces', () => {
id: 'my-dashboard',
},
],
+ trueCopy: false,
}
);
@@ -410,11 +416,17 @@ describe('copySavedObjectsToSpaces', () => {
"errors": undefined,
"success": true,
"successCount": 3,
+ "successResults": Array [
+ "Some success(es) occurred!",
+ ],
},
"non-existent-space": Object {
"errors": undefined,
"success": true,
"successCount": 3,
+ "successResults": Array [
+ "Some success(es) occurred!",
+ ],
},
}
`);
@@ -472,6 +484,7 @@ describe('copySavedObjectsToSpaces', () => {
id: 'my-dashboard',
},
],
+ trueCopy: false,
}
)
).rejects.toThrowErrorMatchingInlineSnapshot(
diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts
index dca6f2a6206ab..4f2c83ba1d548 100644
--- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts
+++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts
@@ -12,7 +12,6 @@ import {
} from '../../../../../../src/core/server';
import { spaceIdToNamespace } from '../utils/namespace';
import { CopyOptions, CopyResponse } from './types';
-import { getEligibleTypes } from './lib/get_eligible_types';
import { createReadableStreamFromArray } from './lib/readable_stream_from_array';
import { createEmptyFailureResponse } from './lib/create_empty_failure_response';
import { readStreamToCompletion } from './lib/read_stream_to_completion';
@@ -27,8 +26,6 @@ export function copySavedObjectsToSpacesFactory(
const savedObjectsClient = getScopedClient(request, COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS);
- const eligibleTypes = getEligibleTypes(getTypeRegistry());
-
const exportRequestedObjects = async (
sourceSpaceId: string,
options: Pick
@@ -56,13 +53,15 @@ export function copySavedObjectsToSpacesFactory(
objectLimit: getImportExportObjectLimit(),
overwrite: options.overwrite,
savedObjectsClient,
- supportedTypes: eligibleTypes,
+ typeRegistry: getTypeRegistry(),
readStream: objectsStream,
+ trueCopy: options.trueCopy,
});
return {
success: importResponse.success,
successCount: importResponse.successCount,
+ successResults: importResponse.successResults,
errors: importResponse.errors,
};
} catch (error) {
diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts
index 7bb4c61ed51a0..0778c379c53b9 100644
--- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts
+++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts
@@ -7,8 +7,9 @@ import {
SavedObjectsImportResponse,
SavedObjectsResolveImportErrorsOptions,
SavedObjectsExportOptions,
+ SavedObjectsImportSuccess,
} from 'src/core/server';
-import { coreMock, savedObjectsTypeRegistryMock, httpServerMock } from 'src/core/server/mocks';
+import { coreMock, httpServerMock } from 'src/core/server/mocks';
import { Readable } from 'stream';
import { resolveCopySavedObjectsToSpacesConflictsFactory } from './resolve_copy_conflicts';
@@ -53,34 +54,6 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
const setup = (setupOpts: SetupOpts) => {
const coreStart = coreMock.createStart();
- const typeRegistry = savedObjectsTypeRegistryMock.create();
- typeRegistry.getAllTypes.mockReturnValue([
- {
- name: 'dashboard',
- namespaceType: 'single',
- hidden: false,
- mappings: { properties: {} },
- },
- {
- name: 'visualization',
- namespaceType: 'single',
- hidden: false,
- mappings: { properties: {} },
- },
- {
- name: 'globaltype',
- namespaceType: 'agnostic',
- hidden: false,
- mappings: { properties: {} },
- },
- ]);
-
- typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) =>
- typeRegistry.getAllTypes().some((t) => t.name === type && t.namespaceType === 'agnostic')
- );
-
- coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry);
-
(exportSavedObjectsToStream as jest.Mock).mockImplementation(
async (opts: SavedObjectsExportOptions) => {
return (
@@ -105,6 +78,9 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
const response: SavedObjectsImportResponse = {
success: true,
successCount: setupOpts.objects.length,
+ successResults: [
+ ('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess,
+ ],
};
return response;
@@ -172,6 +148,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
},
],
},
+ trueCopy: false,
});
expect(result).toMatchInlineSnapshot(`
@@ -180,11 +157,17 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
"errors": undefined,
"success": true,
"successCount": 3,
+ "successResults": Array [
+ "Some success(es) occurred!",
+ ],
},
"destination2": Object {
"errors": undefined,
"success": true,
"successCount": 3,
+ "successResults": Array [
+ "Some success(es) occurred!",
+ ],
},
}
`);
@@ -208,6 +191,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
+ "checkConflicts": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"deleteFromNamespaces": [MockFunction],
@@ -281,6 +265,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
+ "checkConflicts": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"deleteFromNamespaces": [MockFunction],
@@ -289,10 +274,19 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
"get": [MockFunction],
"update": [MockFunction],
},
- "supportedTypes": Array [
- "dashboard",
- "visualization",
- ],
+ "trueCopy": false,
+ "typeRegistry": Object {
+ "getAllTypes": [MockFunction],
+ "getImportableAndExportableTypes": [MockFunction],
+ "getIndex": [MockFunction],
+ "getType": [MockFunction],
+ "isHidden": [MockFunction],
+ "isImportableAndExportable": [MockFunction],
+ "isMultiNamespace": [MockFunction],
+ "isNamespaceAgnostic": [MockFunction],
+ "isSingleNamespace": [MockFunction],
+ "registerType": [MockFunction],
+ },
},
],
Array [
@@ -353,6 +347,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
+ "checkConflicts": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"deleteFromNamespaces": [MockFunction],
@@ -361,10 +356,19 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
"get": [MockFunction],
"update": [MockFunction],
},
- "supportedTypes": Array [
- "dashboard",
- "visualization",
- ],
+ "trueCopy": false,
+ "typeRegistry": Object {
+ "getAllTypes": [MockFunction],
+ "getImportableAndExportableTypes": [MockFunction],
+ "getIndex": [MockFunction],
+ "getType": [MockFunction],
+ "isHidden": [MockFunction],
+ "isImportableAndExportable": [MockFunction],
+ "isMultiNamespace": [MockFunction],
+ "isNamespaceAgnostic": [MockFunction],
+ "isSingleNamespace": [MockFunction],
+ "registerType": [MockFunction],
+ },
},
],
]
@@ -400,6 +404,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
return Promise.resolve({
success: true,
successCount: 3,
+ successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess],
});
},
});
@@ -443,6 +448,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
},
],
},
+ trueCopy: false,
});
expect(result).toMatchInlineSnapshot(`
@@ -458,11 +464,17 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
"errors": undefined,
"success": true,
"successCount": 3,
+ "successResults": Array [
+ "Some success(es) occurred!",
+ ],
},
"non-existent-space": Object {
"errors": undefined,
"success": true,
"successCount": 3,
+ "successResults": Array [
+ "Some success(es) occurred!",
+ ],
},
}
`);
@@ -496,6 +508,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
includeReferences: true,
objects: [],
retries: {},
+ trueCopy: false,
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Something went wrong while reading this stream"`
diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts
index a355d19b305a3..d8bbb3e2c2644 100644
--- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts
+++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts
@@ -5,14 +5,13 @@
*/
import { Readable } from 'stream';
-import { SavedObject, CoreStart, KibanaRequest } from 'src/core/server';
+import { SavedObject, CoreStart, KibanaRequest, SavedObjectsImportRetry } from 'src/core/server';
import {
exportSavedObjectsToStream,
resolveSavedObjectsImportErrors,
} from '../../../../../../src/core/server';
import { spaceIdToNamespace } from '../utils/namespace';
import { CopyOptions, ResolveConflictsOptions, CopyResponse } from './types';
-import { getEligibleTypes } from './lib/get_eligible_types';
import { createEmptyFailureResponse } from './lib/create_empty_failure_response';
import { readStreamToCompletion } from './lib/read_stream_to_completion';
import { createReadableStreamFromArray } from './lib/readable_stream_from_array';
@@ -27,8 +26,6 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory(
const savedObjectsClient = getScopedClient(request, COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS);
- const eligibleTypes = getEligibleTypes(getTypeRegistry());
-
const exportRequestedObjects = async (
sourceSpaceId: string,
options: Pick
@@ -47,26 +44,24 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory(
const resolveConflictsForSpace = async (
spaceId: string,
objectsStream: Readable,
- retries: Array<{
- type: string;
- id: string;
- overwrite: boolean;
- replaceReferences: Array<{ type: string; from: string; to: string }>;
- }>
+ retries: SavedObjectsImportRetry[],
+ trueCopy: boolean
) => {
try {
const importResponse = await resolveSavedObjectsImportErrors({
namespace: spaceIdToNamespace(spaceId),
objectLimit: getImportExportObjectLimit(),
savedObjectsClient,
- supportedTypes: eligibleTypes,
+ typeRegistry: getTypeRegistry(),
readStream: objectsStream,
retries,
+ trueCopy,
});
return {
success: importResponse.success,
successCount: importResponse.successCount,
+ successResults: importResponse.successResults,
errors: importResponse.errors,
};
} catch (error) {
@@ -93,7 +88,8 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory(
response[spaceId] = await resolveConflictsForSpace(
spaceId,
createReadableStreamFromArray(exportedSavedObjects),
- retries
+ retries,
+ options.trueCopy
);
}
diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts
index 1bbe5aa6625b0..4301d3790ce60 100644
--- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts
+++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/types.ts
@@ -5,26 +5,33 @@
*/
import { Payload } from 'boom';
-import { SavedObjectsImportError } from 'src/core/server';
+import {
+ SavedObjectsImportSuccess,
+ SavedObjectsImportError,
+ SavedObjectsImportRetry,
+} from 'src/core/server';
export interface CopyOptions {
objects: Array<{ type: string; id: string }>;
overwrite: boolean;
includeReferences: boolean;
+ trueCopy: boolean;
}
export interface ResolveConflictsOptions {
objects: Array<{ type: string; id: string }>;
includeReferences: boolean;
retries: {
- [spaceId: string]: Array<{ type: string; id: string; overwrite: boolean }>;
+ [spaceId: string]: Array>;
};
+ trueCopy: boolean;
}
export interface CopyResponse {
[spaceId: string]: {
success: boolean;
successCount: number;
+ successResults?: SavedObjectsImportSuccess[];
errors?: Array;
};
}
diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts
index 034d212a33035..ce93591f492f1 100644
--- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts
+++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts
@@ -43,41 +43,6 @@ export const createMockSavedObjectsService = (spaces: any[] = []) => {
const { savedObjects } = coreMock.createStart();
const typeRegistry = savedObjectsTypeRegistryMock.create();
- typeRegistry.getAllTypes.mockReturnValue([
- {
- name: 'visualization',
- namespaceType: 'single',
- hidden: false,
- mappings: { properties: {} },
- },
- {
- name: 'dashboard',
- namespaceType: 'single',
- hidden: false,
- mappings: { properties: {} },
- },
- {
- name: 'index-pattern',
- namespaceType: 'single',
- hidden: false,
- mappings: { properties: {} },
- },
- {
- name: 'globalType',
- namespaceType: 'agnostic',
- hidden: false,
- mappings: { properties: {} },
- },
- {
- name: 'space',
- namespaceType: 'agnostic',
- hidden: true,
- mappings: { properties: {} },
- },
- ]);
- typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) =>
- typeRegistry.getAllTypes().some((t) => t.name === type && t.namespaceType === 'agnostic')
- );
savedObjects.getTypeRegistry.mockReturnValue(typeRegistry);
savedObjects.getScopedClient.mockReturnValue(mockSavedObjectsClientContract);
diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts
index b604554cbc59a..77f1751f8a104 100644
--- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts
+++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts
@@ -191,54 +191,35 @@ describe('copy to space', () => {
);
});
- it(`requires objects to be unique`, async () => {
+ it(`does not allow "overwrite" to be used with "trueCopy"`, async () => {
const payload = {
spaces: ['a-space'],
- objects: [
- { type: 'foo', id: 'bar' },
- { type: 'foo', id: 'bar' },
- ],
+ objects: [{ type: 'foo', id: 'bar' }],
+ overwrite: true,
+ trueCopy: true,
};
const { copyToSpace } = await setup();
expect(() =>
(copyToSpace.routeValidation.body as ObjectType).validate(payload)
- ).toThrowErrorMatchingInlineSnapshot(`"[objects]: duplicate objects are not allowed"`);
+ ).toThrowErrorMatchingInlineSnapshot(`"cannot use [overwrite] with [trueCopy]"`);
});
- it('does not allow namespace agnostic types to be copied (via "supportedTypes" property)', async () => {
+ it(`requires objects to be unique`, async () => {
const payload = {
spaces: ['a-space'],
objects: [
- { type: 'globalType', id: 'bar' },
- { type: 'visualization', id: 'bar' },
+ { type: 'foo', id: 'bar' },
+ { type: 'foo', id: 'bar' },
],
};
const { copyToSpace } = await setup();
- const request = httpServerMock.createKibanaRequest({
- body: payload,
- method: 'post',
- });
-
- const response = await copyToSpace.routeHandler(
- mockRouteContext,
- request,
- kibanaResponseFactory
- );
-
- const { status } = response;
-
- expect(status).toEqual(200);
- expect(importSavedObjectsFromStream).toHaveBeenCalledTimes(1);
- const [importCallOptions] = (importSavedObjectsFromStream as jest.Mock).mock.calls[0];
-
- expect(importCallOptions).toMatchObject({
- namespace: 'a-space',
- supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
- });
+ expect(() =>
+ (copyToSpace.routeValidation.body as ObjectType).validate(payload)
+ ).toThrowErrorMatchingInlineSnapshot(`"[objects]: duplicate objects are not allowed"`);
});
it('copies to multiple spaces', async () => {
@@ -365,58 +346,6 @@ describe('copy to space', () => {
);
});
- it('does not allow namespace agnostic types to be copied (via "supportedTypes" property)', async () => {
- const payload = {
- retries: {
- ['a-space']: [
- {
- type: 'visualization',
- id: 'bar',
- overwrite: true,
- },
- {
- type: 'globalType',
- id: 'bar',
- overwrite: true,
- },
- ],
- },
- objects: [
- {
- type: 'globalType',
- id: 'bar',
- },
- { type: 'visualization', id: 'bar' },
- ],
- };
-
- const { resolveConflicts } = await setup();
-
- const request = httpServerMock.createKibanaRequest({
- body: payload,
- method: 'post',
- });
-
- const response = await resolveConflicts.routeHandler(
- mockRouteContext,
- request,
- kibanaResponseFactory
- );
-
- const { status } = response;
-
- expect(status).toEqual(200);
- expect(resolveSavedObjectsImportErrors).toHaveBeenCalledTimes(1);
- const [
- resolveImportErrorsCallOptions,
- ] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[0];
-
- expect(resolveImportErrorsCallOptions).toMatchObject({
- namespace: 'a-space',
- supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
- });
- });
-
it('resolves conflicts for multiple spaces', async () => {
const payload = {
objects: [{ type: 'visualization', id: 'bar' }],
@@ -459,19 +388,13 @@ describe('copy to space', () => {
resolveImportErrorsFirstCallOptions,
] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[0];
- expect(resolveImportErrorsFirstCallOptions).toMatchObject({
- namespace: 'a-space',
- supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
- });
+ expect(resolveImportErrorsFirstCallOptions).toMatchObject({ namespace: 'a-space' });
const [
resolveImportErrorsSecondCallOptions,
] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[1];
- expect(resolveImportErrorsSecondCallOptions).toMatchObject({
- namespace: 'b-space',
- supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
- });
+ expect(resolveImportErrorsSecondCallOptions).toMatchObject({ namespace: 'b-space' });
});
});
});
diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts
index 4c9f62503a21b..9161d574cddee 100644
--- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts
+++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts
@@ -30,39 +30,49 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
tags: ['access:copySavedObjectsToSpaces'],
},
validate: {
- body: schema.object({
- spaces: schema.arrayOf(
- schema.string({
- validate: (value) => {
- if (!SPACE_ID_REGEX.test(value)) {
- return `lower case, a-z, 0-9, "_", and "-" are allowed`;
- }
- },
- }),
- {
- validate: (spaceIds) => {
- if (_.uniq(spaceIds).length !== spaceIds.length) {
- return 'duplicate space ids are not allowed';
- }
- },
- }
- ),
- objects: schema.arrayOf(
- schema.object({
- type: schema.string(),
- id: schema.string(),
- }),
- {
- validate: (objects) => {
- if (!areObjectsUnique(objects)) {
- return 'duplicate objects are not allowed';
- }
- },
- }
- ),
- includeReferences: schema.boolean({ defaultValue: false }),
- overwrite: schema.boolean({ defaultValue: false }),
- }),
+ body: schema.object(
+ {
+ spaces: schema.arrayOf(
+ schema.string({
+ validate: (value) => {
+ if (!SPACE_ID_REGEX.test(value)) {
+ return `lower case, a-z, 0-9, "_", and "-" are allowed`;
+ }
+ },
+ }),
+ {
+ validate: (spaceIds) => {
+ if (_.uniq(spaceIds).length !== spaceIds.length) {
+ return 'duplicate space ids are not allowed';
+ }
+ },
+ }
+ ),
+ objects: schema.arrayOf(
+ schema.object({
+ type: schema.string(),
+ id: schema.string(),
+ }),
+ {
+ validate: (objects) => {
+ if (!areObjectsUnique(objects)) {
+ return 'duplicate objects are not allowed';
+ }
+ },
+ }
+ ),
+ includeReferences: schema.boolean({ defaultValue: false }),
+ overwrite: schema.boolean({ defaultValue: false }),
+ trueCopy: schema.boolean({ defaultValue: false }),
+ },
+ {
+ validate: (object) => {
+ if (object.overwrite && object.trueCopy) {
+ return 'cannot use [overwrite] with [trueCopy]';
+ }
+ },
+ }
+ ),
},
},
createLicensedRouteHandler(async (context, request, response) => {
@@ -73,12 +83,19 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
getImportExportObjectLimit,
request
);
- const { spaces: destinationSpaceIds, objects, includeReferences, overwrite } = request.body;
+ const {
+ spaces: destinationSpaceIds,
+ objects,
+ includeReferences,
+ overwrite,
+ trueCopy,
+ } = request.body;
const sourceSpaceId = spacesService.getSpaceId(request);
const copyResponse = await copySavedObjectsToSpaces(sourceSpaceId, destinationSpaceIds, {
objects,
includeReferences,
overwrite,
+ trueCopy,
});
return response.ok({ body: copyResponse });
})
@@ -105,6 +122,8 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
type: schema.string(),
id: schema.string(),
overwrite: schema.boolean({ defaultValue: false }),
+ destinationId: schema.maybe(schema.string()),
+ trueCopy: schema.maybe(schema.boolean()),
})
)
),
@@ -122,6 +141,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
}
),
includeReferences: schema.boolean({ defaultValue: false }),
+ trueCopy: schema.boolean({ defaultValue: false }),
}),
},
},
@@ -133,7 +153,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
getImportExportObjectLimit,
request
);
- const { objects, includeReferences, retries } = request.body;
+ const { objects, includeReferences, retries, trueCopy } = request.body;
const sourceSpaceId = spacesService.getSpaceId(request);
const resolveConflictsResponse = await resolveCopySavedObjectsToSpacesConflicts(
sourceSpaceId,
@@ -141,6 +161,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
objects,
includeReferences,
retries,
+ trueCopy,
}
);
return response.ok({ body: resolveConflictsResponse });
diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts
index 190429d2dacd4..25d562aa69d77 100644
--- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts
+++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts
@@ -176,6 +176,34 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
});
});
+ describe('#checkConflicts', () => {
+ test(`throws error if options.namespace is specified`, async () => {
+ const { client } = await createSpacesSavedObjectsClient();
+
+ await expect(
+ // @ts-ignore
+ client.checkConflicts(null, { namespace: 'bar' })
+ ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED);
+ });
+
+ test(`supplements options with the current namespace`, async () => {
+ const { client, baseClient } = await createSpacesSavedObjectsClient();
+ const expectedReturnValue = { errors: [] };
+ baseClient.checkConflicts.mockReturnValue(Promise.resolve(expectedReturnValue));
+
+ const objects = Symbol();
+ const options = Object.freeze({ foo: 'bar' });
+ // @ts-ignore
+ const actualReturnValue = await client.checkConflicts(objects, options);
+
+ expect(actualReturnValue).toBe(expectedReturnValue);
+ expect(baseClient.checkConflicts).toHaveBeenCalledWith(objects, {
+ foo: 'bar',
+ namespace: currentSpace.expectedNamespace,
+ });
+ });
+ });
+
describe('#create', () => {
test(`throws error if options.namespace is specified`, async () => {
const { client } = await createSpacesSavedObjectsClient();
diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts
index 6611725be8b67..acb3c8dbd9551 100644
--- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts
+++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts
@@ -9,6 +9,7 @@ import {
SavedObjectsBulkCreateObject,
SavedObjectsBulkGetObject,
SavedObjectsBulkUpdateObject,
+ SavedObjectsCheckConflictsObject,
SavedObjectsClientContract,
SavedObjectsCreateOptions,
SavedObjectsFindOptions,
@@ -56,6 +57,25 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract {
this.errors = baseClient.errors;
}
+ /**
+ * Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are
+ * multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten.
+ *
+ * @param objects
+ * @param options
+ */
+ public async checkConflicts(
+ objects: SavedObjectsCheckConflictsObject[] = [],
+ options: SavedObjectsBaseOptions = {}
+ ) {
+ throwErrorIfNamespaceSpecified(options);
+
+ return await this.client.checkConflicts(objects, {
+ ...options,
+ namespace: spaceIdToNamespace(this.spaceId),
+ });
+ }
+
/**
* Persists an object
*
diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
index d2c14189e2529..4c0447c29c8f9 100644
--- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
+++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
@@ -397,3 +397,91 @@
"type": "doc"
}
}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_1",
+ "index": ".kibana",
+ "source": {
+ "sharedtype": {
+ "title": "A shared saved-object in all spaces"
+ },
+ "type": "sharedtype",
+ "namespaces": ["default", "space_1", "space_2"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_2a",
+ "index": ".kibana",
+ "source": {
+ "originId": "conflict_2",
+ "sharedtype": {
+ "title": "A shared saved-object in all spaces"
+ },
+ "type": "sharedtype",
+ "namespaces": ["default", "space_1", "space_2"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_2b",
+ "index": ".kibana",
+ "source": {
+ "originId": "conflict_2",
+ "sharedtype": {
+ "title": "A shared saved-object in all spaces"
+ },
+ "type": "sharedtype",
+ "namespaces": ["default", "space_1", "space_2"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_3",
+ "index": ".kibana",
+ "source": {
+ "sharedtype": {
+ "title": "A shared saved-object in all spaces"
+ },
+ "type": "sharedtype",
+ "namespaces": ["default", "space_1", "space_2"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_4a",
+ "index": ".kibana",
+ "source": {
+ "originId": "conflict_4",
+ "sharedtype": {
+ "title": "A shared saved-object in all spaces"
+ },
+ "type": "sharedtype",
+ "namespaces": ["default", "space_1", "space_2"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json
index 7b5b1d86f6bcc..73f0e536b9295 100644
--- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json
+++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json
@@ -182,6 +182,9 @@
"namespaces": {
"type": "keyword"
},
+ "originId": {
+ "type": "keyword"
+ },
"search": {
"properties": {
"columns": {
diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts b/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts
index 0c15ab4bd2f80..45880635586a7 100644
--- a/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts
+++ b/x-pack/test/saved_object_api_integration/common/fixtures/saved_object_test_plugin/server/plugin.ts
@@ -48,6 +48,7 @@ export class Plugin {
name: 'sharedtype',
hidden: false,
namespaceType: 'multiple',
+ management,
mappings,
});
core.savedObjects.registerType({
diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts
index de036494caa83..115f4dca8b4bc 100644
--- a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts
+++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts
@@ -161,7 +161,9 @@ export const expectResponses = {
expect(actualNamespace).to.eql(spaceId);
}
if (isMultiNamespace(type)) {
- if (id === CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1.id) {
+ if (['conflict_1', 'conflict_2a', 'conflict_2b', 'conflict_3', 'conflict_4a'].includes(id)) {
+ expect(actualNamespaces).to.eql([DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]);
+ } else if (id === CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1.id) {
expect(actualNamespaces).to.eql([DEFAULT_SPACE_ID, SPACE_1_ID]);
} else if (id === CASES.MULTI_NAMESPACE_ONLY_SPACE_1.id) {
expect(actualNamespaces).to.eql([SPACE_1_ID]);
diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
index dd32c42597c32..707060cedfe66 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
@@ -6,6 +6,7 @@
import expect from '@kbn/expect';
import { SuperTest } from 'supertest';
+import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases';
import { SPACES } from '../lib/spaces';
import {
@@ -23,6 +24,7 @@ export interface BulkCreateTestDefinition extends TestDefinition {
export type BulkCreateTestSuite = TestSuite;
export interface BulkCreateTestCase extends TestCase {
failure?: 400 | 409; // only used for permitted response case
+ fail409Param?: string;
}
const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake
@@ -56,6 +58,15 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest:
for (let i = 0; i < savedObjects.length; i++) {
const object = savedObjects[i];
const testCase = testCaseArray[i];
+ if (testCase.failure === 409 && testCase.fail409Param === 'unresolvableConflict') {
+ const { type, id } = testCase;
+ const error = SavedObjectsErrorHelpers.createConflictError(type, id);
+ const payload = { ...error.output.payload, metadata: { isNotOverwritable: true } };
+ expect(object.type).to.eql(type);
+ expect(object.id).to.eql(id);
+ expect(object.error).to.eql(payload);
+ continue;
+ }
await expectResponses.permitted(object, testCase);
if (!testCase.failure) {
expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL);
diff --git a/x-pack/test/saved_object_api_integration/common/suites/export.ts b/x-pack/test/saved_object_api_integration/common/suites/export.ts
index 394693677699f..ce5a3538d2060 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/export.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/export.ts
@@ -8,7 +8,7 @@ import { SuperTest } from 'supertest';
import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases';
import { SPACES } from '../lib/spaces';
import { expectResponses, getUrlPrefix } from '../lib/saved_object_test_utils';
-import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types';
+import { ExpectResponseBody, TestDefinition, TestSuite } from '../lib/types';
const {
DEFAULT: { spaceId: DEFAULT_SPACE_ID },
@@ -20,15 +20,28 @@ export interface ExportTestDefinition extends TestDefinition {
request: ReturnType;
}
export type ExportTestSuite = TestSuite;
+interface SuccessResult {
+ type: string;
+ id: string;
+ originId?: string;
+}
export interface ExportTestCase {
title: string;
type: string;
id?: string;
- successResult?: TestCase | TestCase[];
+ successResult?: SuccessResult | SuccessResult[];
failure?: 400 | 403;
}
-export const getTestCases = (spaceId?: string) => ({
+// 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` });
+
+export const getTestCases = (spaceId?: string): { [key: string]: ExportTestCase } => ({
singleNamespaceObject: {
title: 'single-namespace object',
...(spaceId === SPACE_1_ID
@@ -36,7 +49,7 @@ export const getTestCases = (spaceId?: string) => ({
: spaceId === SPACE_2_ID
? CASES.SINGLE_NAMESPACE_SPACE_2
: CASES.SINGLE_NAMESPACE_DEFAULT_SPACE),
- } as ExportTestCase,
+ },
singleNamespaceType: {
// this test explicitly ensures that single-namespace objects from other spaces are not returned
title: 'single-namespace type',
@@ -47,7 +60,7 @@ export const getTestCases = (spaceId?: string) => ({
: spaceId === SPACE_2_ID
? CASES.SINGLE_NAMESPACE_SPACE_2
: CASES.SINGLE_NAMESPACE_DEFAULT_SPACE,
- } as ExportTestCase,
+ },
multiNamespaceObject: {
title: 'multi-namespace object',
...(spaceId === SPACE_1_ID
@@ -55,30 +68,30 @@ export const getTestCases = (spaceId?: string) => ({
: spaceId === SPACE_2_ID
? CASES.MULTI_NAMESPACE_ONLY_SPACE_2
: CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1),
- failure: 400, // multi-namespace types cannot be exported yet
- } as ExportTestCase,
+ },
multiNamespaceType: {
title: 'multi-namespace type',
type: 'sharedtype',
- // successResult:
- // spaceId === SPACE_1_ID
- // ? [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,
- failure: 400, // multi-namespace types cannot be exported yet
- } as ExportTestCase,
+ successResult: (spaceId === SPACE_1_ID
+ ? [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(),
+ },
namespaceAgnosticObject: {
title: 'namespace-agnostic object',
...CASES.NAMESPACE_AGNOSTIC,
- } as ExportTestCase,
+ },
namespaceAgnosticType: {
title: 'namespace-agnostic type',
type: 'globaltype',
successResult: CASES.NAMESPACE_AGNOSTIC,
- } as ExportTestCase,
- hiddenObject: { title: 'hidden object', ...CASES.HIDDEN, failure: 400 } as ExportTestCase,
- hiddenType: { title: 'hidden type', type: 'hiddentype', failure: 400 } as ExportTestCase,
+ },
+ hiddenObject: { title: 'hidden object', ...CASES.HIDDEN, failure: 400 },
+ hiddenType: { title: 'hidden type', type: 'hiddentype', failure: 400 },
});
export const createRequest = ({ type, id }: ExportTestCase) =>
id ? { objects: [{ type, id }] } : { type };
@@ -98,7 +111,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest async (
response: Record
) => {
- const { type, id, successResult = { type, id }, failure } = testCase;
+ const { type, id, successResult = { type, id } as SuccessResult, failure } = testCase;
if (failure === 403) {
// In export only, the API uses "bulk_get" or "find" depending on the parameters it receives.
// The best that could be done here is to have an if statement to ensure at least one of the
@@ -125,11 +138,14 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest x.id === object.id)!;
+ expect(expected).not.to.be(undefined);
+ expect(object.type).to.eql(expected.type);
+ if (object.originId) {
+ expect(object.originId).to.eql(expected.originId);
+ }
expect(object.updated_at).to.match(/^[\d-]{10}T[\d:\.]{12}Z$/);
// don't test attributes, version, or references
}
diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts
index 13f411fc14fc8..8647f7d90f3b5 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/find.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts
@@ -34,6 +34,14 @@ export interface FindTestCase {
failure?: 400 | 403;
}
+// 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` });
+
export const getTestCases = (spaceId?: string) => ({
singleNamespaceType: {
title: 'find single-namespace type',
@@ -51,12 +59,14 @@ export const getTestCases = (spaceId?: string) => ({
title: 'find multi-namespace type',
query: 'type=sharedtype&fields=title',
successResult: {
- savedObjects:
- spaceId === SPACE_1_ID
- ? [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,
+ savedObjects: (spaceId === SPACE_1_ID
+ ? [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(),
},
} as FindTestCase,
namespaceAgnosticType: {
diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts
index a5d2ca238d34e..58941cc16b07a 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/import.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts
@@ -8,49 +8,85 @@ import expect from '@kbn/expect';
import { SuperTest } from 'supertest';
import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases';
import { SPACES } from '../lib/spaces';
-import {
- createRequest,
- expectResponses,
- getUrlPrefix,
- getTestTitle,
-} from '../lib/saved_object_test_utils';
+import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils';
import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types';
export interface ImportTestDefinition extends TestDefinition {
- request: Array<{ type: string; id: string }>;
+ request: Array<{ type: string; id: string; originId?: string }>;
+ overwrite: boolean;
+ trueCopy: boolean;
}
export type ImportTestSuite = TestSuite;
export interface ImportTestCase extends TestCase {
+ originId?: string;
+ expectedNewId?: string;
+ successParam?: string;
failure?: 400 | 409; // only used for permitted response case
+ fail409Param?: string;
}
const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake
const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`;
-const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' });
-const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' });
-const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' });
+// these five saved objects already exist in the sample data:
+// * id: conflict_1
+// * id: conflict_2a, originId: conflict_2
+// * id: conflict_2b, originId: conflict_2
+// * id: conflict_3
+// * id: conflict_4a, originId: conflict_4
+// using the seven conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios
+const CID = 'conflict_';
export const TEST_CASES = Object.freeze({
...CASES,
- NEW_SINGLE_NAMESPACE_OBJ,
- NEW_MULTI_NAMESPACE_OBJ,
- NEW_NAMESPACE_AGNOSTIC_OBJ,
+ CONFLICT_1_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1` }),
+ CONFLICT_1A_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1a`, originId: `${CID}1` }),
+ CONFLICT_1B_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1b`, originId: `${CID}1` }),
+ CONFLICT_2C_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}2c`, originId: `${CID}2` }),
+ CONFLICT_2D_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}2d`, originId: `${CID}2` }),
+ CONFLICT_3A_OBJ: Object.freeze({
+ type: 'sharedtype',
+ id: `${CID}3a`,
+ originId: `${CID}3`,
+ expectedNewId: `${CID}3`,
+ }),
+ CONFLICT_4_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}4`, expectedNewId: `${CID}4a` }),
+ NEW_SINGLE_NAMESPACE_OBJ: Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }),
+ NEW_MULTI_NAMESPACE_OBJ: Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }),
+ NEW_NAMESPACE_AGNOSTIC_OBJ: Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }),
+});
+
+/**
+ * Test cases have additional properties that we don't want to send in HTTP Requests
+ */
+const createRequest = ({ type, id, originId }: ImportTestCase) => ({
+ type,
+ id,
+ ...(originId && { originId }),
+});
+
+const getConflictDest = (id: string) => ({
+ id,
+ title: 'A shared saved-object in all spaces',
+ updatedAt: '2017-09-21T18:59:16.270Z',
});
export function importTestSuiteFactory(es: any, esArchiver: any, supertest: SuperTest) {
- const expectForbidden = expectResponses.forbidden('bulk_create');
+ const expectForbidden = expectResponses.forbidden;
const expectResponseBody = (
testCases: ImportTestCase | ImportTestCase[],
statusCode: 200 | 403,
+ singleRequest: boolean,
+ overwrite: boolean,
+ trueCopy: boolean,
spaceId = SPACES.DEFAULT.spaceId
): ExpectResponseBody => async (response: Record) => {
const testCaseArray = Array.isArray(testCases) ? testCases : [testCases];
if (statusCode === 403) {
const types = testCaseArray.map((x) => x.type);
- await expectForbidden(types)(response);
+ await expectResponses.forbidden('bulk_create')(types)(response);
} else {
// permitted
- const { success, successCount, errors } = response.body;
+ const { success, successCount, successResults, errors } = response.body;
const expectedSuccesses = testCaseArray.filter((x) => !x.failure);
const expectedFailures = testCaseArray.filter((x) => x.failure);
expect(success).to.eql(expectedFailures.length === 0);
@@ -61,12 +97,52 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
expect(response.body).not.to.have.property('errors');
}
for (let i = 0; i < expectedSuccesses.length; i++) {
- const { type, id } = expectedSuccesses[i];
- const { _source } = await expectResponses.successCreated(es, spaceId, type, id);
- expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL);
+ const { type, id, successParam, expectedNewId } = expectedSuccesses[i];
+ // we don't know the order of the returned successResults; search for each one
+ const object = (successResults as Array>).find(
+ (x) => x.type === type && x.id === id
+ );
+ expect(object).not.to.be(undefined);
+ const destinationId = object!.destinationId as string;
+ if (successParam === 'destinationId') {
+ // Kibana created the object with a different ID than what was specified in the import
+ // This can happen due to an unresolvable conflict (so the new ID will be random), or due to an inexact match (so the new ID will
+ // be equal to the ID or originID of the existing object that it inexactly matched)
+ if (expectedNewId) {
+ expect(destinationId).to.be(expectedNewId);
+ } else {
+ // the new ID was randomly generated
+ expect(destinationId).to.match(/^[0-9a-f-]{36}$/);
+ }
+ } else if (successParam === 'trueCopy' || successParam === 'newOrigin') {
+ // the new ID was randomly generated
+ expect(destinationId).to.match(/^[0-9a-f-]{36}$/);
+ } else {
+ expect(destinationId).to.be(undefined);
+ }
+
+ // This assertion is only needed for the case where True Copy mode is disabled and ambiguous source conflicts are detected. When
+ // True Copy mode is permanently enabled, this field will be removed, and this assertion will be redundant and can be removed too.
+ const resultTrueCopy = object!.trueCopy as boolean | undefined;
+ if (successParam === 'newOrigin') {
+ expect(resultTrueCopy).to.be(true);
+ } else {
+ expect(resultTrueCopy).to.be(undefined);
+ }
+
+ if (!singleRequest || overwrite || trueCopy) {
+ // even if the object result was a "success" result, it may not have been created if other resolvable errors were returned
+ const { _source } = await expectResponses.successCreated(
+ es,
+ spaceId,
+ type,
+ destinationId ?? id
+ );
+ expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL);
+ }
}
for (let i = 0; i < expectedFailures.length; i++) {
- const { type, id, failure } = expectedFailures[i];
+ const { type, id, failure, fail409Param, expectedNewId } = expectedFailures[i];
// we don't know the order of the returned errors; search for each one
const object = (errors as Array>).find(
(x) => x.type === type && x.id === id
@@ -76,7 +152,30 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
expect(object!.error).to.eql({ type: 'unsupported_type' });
} else {
// 409
- expect(object!.error).to.eql({ type: 'conflict' });
+ let error: Record = {
+ type: 'conflict',
+ ...(expectedNewId && { destinationId: expectedNewId }),
+ };
+ if (fail409Param === 'ambiguous_conflict_1a1b') {
+ // "ambiguous source" conflict
+ error = {
+ type: 'ambiguous_conflict',
+ destinations: [getConflictDest(`${CID}1`)],
+ };
+ } else if (fail409Param === 'ambiguous_conflict_2c') {
+ // "ambiguous destination" conflict
+ error = {
+ type: 'ambiguous_conflict',
+ destinations: [getConflictDest(`${CID}2a`), getConflictDest(`${CID}2b`)],
+ };
+ } else if (fail409Param === 'ambiguous_conflict_2c2d') {
+ // "ambiguous source and destination" conflict
+ error = {
+ type: 'ambiguous_conflict',
+ destinations: [getConflictDest(`${CID}2a`), getConflictDest(`${CID}2b`)],
+ };
+ }
+ expect(object!.error).to.eql(error);
}
}
}
@@ -84,7 +183,9 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
const createTestDefinitions = (
testCases: ImportTestCase | ImportTestCase[],
forbidden: boolean,
- options?: {
+ options: {
+ overwrite?: boolean;
+ trueCopy?: boolean;
spaceId?: string;
singleRequest?: boolean;
responseBodyOverride?: ExpectResponseBody;
@@ -92,7 +193,14 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
): ImportTestDefinition[] => {
const cases = Array.isArray(testCases) ? testCases : [testCases];
const responseStatusCode = forbidden ? 403 : 200;
- if (!options?.singleRequest) {
+ const {
+ overwrite = false,
+ trueCopy = false,
+ spaceId,
+ singleRequest,
+ responseBodyOverride,
+ } = options;
+ if (!singleRequest) {
// if we are testing cases that should result in a forbidden response, we can do each case individually
// this ensures that multiple test cases of a single type will each result in a forbidden error
return cases.map((x) => ({
@@ -100,8 +208,10 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
request: [createRequest(x)],
responseStatusCode,
responseBody:
- options?.responseBodyOverride ||
- expectResponseBody(x, responseStatusCode, options?.spaceId),
+ responseBodyOverride ||
+ expectResponseBody(x, responseStatusCode, false, overwrite, trueCopy, spaceId),
+ overwrite,
+ trueCopy,
}));
}
// batch into a single request to save time during test execution
@@ -111,8 +221,10 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
request: cases.map((x) => createRequest(x)),
responseStatusCode,
responseBody:
- options?.responseBodyOverride ||
- expectResponseBody(cases, responseStatusCode, options?.spaceId),
+ responseBodyOverride ||
+ expectResponseBody(cases, responseStatusCode, true, overwrite, trueCopy, spaceId),
+ overwrite,
+ trueCopy,
},
];
};
@@ -134,8 +246,9 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
const requestBody = test.request
.map((obj) => JSON.stringify({ ...obj, ...attrs }))
.join('\n');
+ const query = test.overwrite ? '?overwrite=true' : test.trueCopy ? '?trueCopy=true' : '';
await supertest
- .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`)
+ .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import${query}`)
.auth(user?.username, user?.password)
.attach('file', Buffer.from(requestBody, 'utf8'), 'export.ndjson')
.expect(test.responseStatusCode)
diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts
index cb48f26ed645c..52b5610a195e5 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts
@@ -8,34 +8,85 @@ import expect from '@kbn/expect';
import { SuperTest } from 'supertest';
import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases';
import { SPACES } from '../lib/spaces';
-import {
- createRequest,
- expectResponses,
- getUrlPrefix,
- getTestTitle,
-} from '../lib/saved_object_test_utils';
+import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils';
import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types';
export interface ResolveImportErrorsTestDefinition extends TestDefinition {
- request: Array<{ type: string; id: string }>;
+ request: {
+ objects: Array<{ type: string; id: string; originId?: string }>;
+ retries: Array<{ type: string; id: string; overwrite: boolean; destinationId?: string }>;
+ };
overwrite: boolean;
+ trueCopy: boolean;
}
export type ResolveImportErrorsTestSuite = TestSuite;
export interface ResolveImportErrorsTestCase extends TestCase {
+ originId?: string;
+ expectedNewId?: string;
+ successParam?: string;
failure?: 400 | 409; // only used for permitted response case
}
const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake
const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`;
-const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' });
-const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' });
-const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' });
+// these five saved objects already exist in the sample data:
+// * id: conflict_1
+// * id: conflict_2a, originId: conflict_2
+// * id: conflict_2b, originId: conflict_2
+// * id: conflict_3
+// * id: conflict_4a, originId: conflict_4
+// using the five conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios
export const TEST_CASES = Object.freeze({
...CASES,
- NEW_SINGLE_NAMESPACE_OBJ,
- NEW_MULTI_NAMESPACE_OBJ,
- NEW_NAMESPACE_AGNOSTIC_OBJ,
+ CONFLICT_1A_OBJ: Object.freeze({
+ type: 'sharedtype',
+ id: `conflict_1a`,
+ originId: `conflict_1`,
+ expectedNewId: 'some-random-id',
+ }),
+ CONFLICT_1B_OBJ: Object.freeze({
+ type: 'sharedtype',
+ id: `conflict_1b`,
+ originId: `conflict_1`,
+ expectedNewId: 'another-random-id',
+ }),
+ CONFLICT_2C_OBJ: Object.freeze({
+ type: 'sharedtype',
+ id: `conflict_2c`,
+ originId: `conflict_2`,
+ expectedNewId: `conflict_2a`,
+ }),
+ CONFLICT_3A_OBJ: Object.freeze({
+ type: 'sharedtype',
+ id: `conflict_3a`,
+ originId: `conflict_3`,
+ expectedNewId: `conflict_3`,
+ }),
+ CONFLICT_4_OBJ: Object.freeze({
+ type: 'sharedtype',
+ id: `conflict_4`,
+ expectedNewId: `conflict_4a`,
+ }),
+});
+
+/**
+ * Test cases have additional properties that we don't want to send in HTTP Requests
+ */
+const createRequest = (
+ { type, id, originId, expectedNewId, successParam }: ResolveImportErrorsTestCase,
+ overwrite: boolean
+): ResolveImportErrorsTestDefinition['request'] => ({
+ objects: [{ type, id, ...(originId && { originId }) }],
+ retries: [
+ {
+ type,
+ id,
+ overwrite,
+ ...(expectedNewId && { destinationId: expectedNewId }),
+ ...(successParam === 'newOrigin' && { trueCopy: true }),
+ },
+ ],
});
export function resolveImportErrorsTestSuiteFactory(
@@ -55,7 +106,7 @@ export function resolveImportErrorsTestSuiteFactory(
await expectForbidden(types)(response);
} else {
// permitted
- const { success, successCount, errors } = response.body;
+ const { success, successCount, successResults, errors } = response.body;
const expectedSuccesses = testCaseArray.filter((x) => !x.failure);
const expectedFailures = testCaseArray.filter((x) => x.failure);
expect(success).to.eql(expectedFailures.length === 0);
@@ -66,12 +117,48 @@ export function resolveImportErrorsTestSuiteFactory(
expect(response.body).not.to.have.property('errors');
}
for (let i = 0; i < expectedSuccesses.length; i++) {
- const { type, id } = expectedSuccesses[i];
- const { _source } = await expectResponses.successCreated(es, spaceId, type, id);
+ const { type, id, successParam, expectedNewId } = expectedSuccesses[i];
+ // we don't know the order of the returned successResults; search for each one
+ const object = (successResults as Array>).find(
+ (x) => x.type === type && x.id === id
+ );
+ expect(object).not.to.be(undefined);
+ const destinationId = object!.destinationId as string;
+ if (successParam === 'destinationId') {
+ // Kibana created the object with a different ID than what was specified in the import
+ // This can happen due to an unresolvable conflict (so the new ID will be random), or due to an inexact match (so the new ID will
+ // be equal to the ID or originID of the existing object that it inexactly matched)
+ if (expectedNewId) {
+ expect(destinationId).to.be(expectedNewId);
+ } else {
+ // the new ID was randomly generated
+ expect(destinationId).to.match(/^[0-9a-f-]{36}$/);
+ }
+ } else if (successParam === 'trueCopy' || successParam === 'newOrigin') {
+ expect(destinationId).to.be(expectedNewId!);
+ } else {
+ expect(destinationId).to.be(undefined);
+ }
+
+ // This assertion is only needed for the case where True Copy mode is disabled and ambiguous source conflicts are detected. When
+ // True Copy mode is permanently enabled, this field will be removed, and this assertion will be redundant and can be removed too.
+ const resultTrueCopy = object!.trueCopy as boolean | undefined;
+ if (successParam === 'newOrigin') {
+ expect(resultTrueCopy).to.be(true);
+ } else {
+ expect(resultTrueCopy).to.be(undefined);
+ }
+
+ const { _source } = await expectResponses.successCreated(
+ es,
+ spaceId,
+ type,
+ destinationId ?? id
+ );
expect(_source[type][NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL);
}
for (let i = 0; i < expectedFailures.length; i++) {
- const { type, id, failure } = expectedFailures[i];
+ const { type, id, failure, expectedNewId } = expectedFailures[i];
// we don't know the order of the returned errors; search for each one
const object = (errors as Array>).find(
(x) => x.type === type && x.id === id
@@ -81,7 +168,10 @@ export function resolveImportErrorsTestSuiteFactory(
expect(object!.error).to.eql({ type: 'unsupported_type' });
} else {
// 409
- expect(object!.error).to.eql({ type: 'conflict' });
+ expect(object!.error).to.eql({
+ type: 'conflict',
+ ...(expectedNewId && { destinationId: expectedNewId }),
+ });
}
}
}
@@ -89,8 +179,9 @@ export function resolveImportErrorsTestSuiteFactory(
const createTestDefinitions = (
testCases: ResolveImportErrorsTestCase | ResolveImportErrorsTestCase[],
forbidden: boolean,
- overwrite: boolean,
- options?: {
+ options: {
+ overwrite?: boolean;
+ trueCopy?: boolean;
spaceId?: string;
singleRequest?: boolean;
responseBodyOverride?: ExpectResponseBody;
@@ -98,29 +189,40 @@ export function resolveImportErrorsTestSuiteFactory(
): ResolveImportErrorsTestDefinition[] => {
const cases = Array.isArray(testCases) ? testCases : [testCases];
const responseStatusCode = forbidden ? 403 : 200;
- if (!options?.singleRequest) {
+ const {
+ overwrite = false,
+ trueCopy = false,
+ spaceId,
+ singleRequest,
+ responseBodyOverride,
+ } = options;
+ if (!singleRequest) {
// if we are testing cases that should result in a forbidden response, we can do each case individually
// this ensures that multiple test cases of a single type will each result in a forbidden error
return cases.map((x) => ({
title: getTestTitle(x, responseStatusCode),
- request: [createRequest(x)],
+ request: createRequest(x, overwrite),
responseStatusCode,
- responseBody:
- options?.responseBodyOverride ||
- expectResponseBody(x, responseStatusCode, options?.spaceId),
+ responseBody: responseBodyOverride || expectResponseBody(x, responseStatusCode, spaceId),
overwrite,
+ trueCopy,
}));
}
// batch into a single request to save time during test execution
return [
{
title: getTestTitle(cases, responseStatusCode),
- request: cases.map((x) => createRequest(x)),
+ request: cases
+ .map((x) => createRequest(x, overwrite))
+ .reduce((acc, cur) => ({
+ objects: [...acc.objects, ...cur.objects],
+ retries: [...acc.retries, ...cur.retries],
+ })),
responseStatusCode,
responseBody:
- options?.responseBodyOverride ||
- expectResponseBody(cases, responseStatusCode, options?.spaceId),
+ responseBodyOverride || expectResponseBody(cases, responseStatusCode, spaceId),
overwrite,
+ trueCopy,
},
];
};
@@ -139,17 +241,14 @@ export function resolveImportErrorsTestSuiteFactory(
for (const test of tests) {
it(`should return ${test.responseStatusCode} ${test.title}`, async () => {
- const retryAttrs = test.overwrite ? { overwrite: true } : {};
- const retries = JSON.stringify(
- test.request.map(({ type, id }) => ({ type, id, ...retryAttrs }))
- );
- const requestBody = test.request
+ const requestBody = test.request.objects
.map((obj) => JSON.stringify({ ...obj, ...attrs }))
.join('\n');
+ const query = test.trueCopy ? '?trueCopy=true' : '';
await supertest
- .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors`)
+ .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_errors${query}`)
.auth(user?.username, user?.password)
- .field('retries', retries)
+ .field('retries', JSON.stringify(test.request.retries))
.attach('file', Buffer.from(requestBody, 'utf8'), 'export.ndjson')
.expect(test.responseStatusCode)
.then(test.responseBody);
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts
index d83f3449460ce..0cc5969e2b7ab 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts
@@ -20,6 +20,8 @@ const {
SPACE_2: { spaceId: SPACE_2_ID },
} = SPACES;
const { fail400, fail409 } = testCaseFailures;
+const unresolvableConflict = (condition?: boolean) =>
+ condition !== false ? { fail409Param: 'unresolvableConflict' } : {};
const createTestCases = (overwrite: boolean, spaceId: string) => {
// for each permitted (non-403) outcome, if failure !== undefined then we expect
@@ -34,9 +36,18 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
{
...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1,
...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)),
+ ...unresolvableConflict(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID),
+ },
+ {
+ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1,
+ ...fail409(!overwrite || spaceId !== SPACE_1_ID),
+ ...unresolvableConflict(spaceId !== SPACE_1_ID),
+ },
+ {
+ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2,
+ ...fail409(!overwrite || spaceId !== SPACE_2_ID),
+ ...unresolvableConflict(spaceId !== SPACE_2_ID),
},
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) },
{ ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) },
CASES.NEW_SINGLE_NAMESPACE_OBJ,
CASES.NEW_MULTI_NAMESPACE_OBJ,
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts
index f85cd3a36c092..c581a1757565e 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts
@@ -18,15 +18,12 @@ const createTestCases = (spaceId: string) => {
const exportableTypes = [
cases.singleNamespaceObject,
cases.singleNamespaceType,
- cases.namespaceAgnosticObject,
- cases.namespaceAgnosticType,
- ];
- const nonExportableTypes = [
cases.multiNamespaceObject,
cases.multiNamespaceType,
- cases.hiddenObject,
- cases.hiddenType,
+ cases.namespaceAgnosticObject,
+ cases.namespaceAgnosticType,
];
+ const nonExportableTypes = [cases.hiddenObject, cases.hiddenType];
const allTypes = exportableTypes.concat(nonExportableTypes);
return { exportableTypes, nonExportableTypes, allTypes };
};
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts
index 6b4dfe1d05f72..164a01a3c2618 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts
@@ -20,27 +20,78 @@ const {
SPACE_2: { spaceId: SPACE_2_ID },
} = SPACES;
const { fail400, fail409 } = testCaseFailures;
+const destinationId = (condition?: boolean) =>
+ condition !== false ? { successParam: 'destinationId' } : {};
+const newOrigin = () => ({ successParam: 'newOrigin' });
+const ambiguousConflict = (suffix: string) => ({
+ failure: 409 as 409,
+ fail409Param: `ambiguous_conflict_${suffix}`,
+});
-const createTestCases = (spaceId: string) => {
+const createTrueCopyTestCases = () => {
+ // for each outcome, if failure !== undefined then we expect to receive
+ // an error; otherwise, we expect to receive a success result
+ const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN');
+ const importable = cases.map(([, val]) => ({ ...val, successParam: 'trueCopy' }));
+ const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }];
+ const all = [...importable, ...nonImportable];
+ return { importable, nonImportable, all };
+};
+
+const createTestCases = (overwrite: boolean, spaceId: string) => {
// for each permitted (non-403) outcome, if failure !== undefined then we expect
// to receive an error; otherwise, we expect to receive a success result
- const importableTypes = [
- { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(spaceId === DEFAULT_SPACE_ID) },
- { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(spaceId === SPACE_1_ID) },
- { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(spaceId === SPACE_2_ID) },
- { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() },
+ const group1Importable = [
+ // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes
+ {
+ ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE,
+ ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID),
+ },
+ { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) },
+ { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) },
+ { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) },
CASES.NEW_SINGLE_NAMESPACE_OBJ,
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
];
- const nonImportableTypes = [
- { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() },
- { ...CASES.HIDDEN, ...fail400() },
- { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() },
+ const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }];
+ const group1All = group1Importable.concat(group1NonImportable);
+ const group2 = [
+ // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes
+ CASES.NEW_MULTI_NAMESPACE_OBJ,
+ {
+ ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1,
+ ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)),
+ ...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID),
+ },
+ {
+ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1,
+ ...fail409(!overwrite && spaceId === SPACE_1_ID),
+ ...destinationId(spaceId !== SPACE_1_ID),
+ },
+ {
+ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2,
+ ...fail409(!overwrite && spaceId === SPACE_2_ID),
+ ...destinationId(spaceId !== SPACE_2_ID),
+ },
+ { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
+ { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
+ { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict
+ { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict
+ ];
+ const group3 = [
+ // when overwrite=true, all of the objects in this group are errors, so we cannot check the created object attributes
+ // grouping errors together simplifies the test suite code
+ { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict
+ ];
+ const group4 = [
+ // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes
+ { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict
+ CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match
+ CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match
+ { ...CASES.CONFLICT_2C_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID
+ { ...CASES.CONFLICT_2D_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID
];
- const allTypes = importableTypes.concat(nonImportableTypes);
- return { importableTypes, nonImportableTypes, allTypes };
+ return { group1Importable, group1NonImportable, group1All, group2, group3, group4 };
};
export default function ({ getService }: FtrProviderContext) {
@@ -53,27 +104,77 @@ export default function ({ getService }: FtrProviderContext) {
esArchiver,
supertest
);
- const createTests = (spaceId: string) => {
- const { importableTypes, nonImportableTypes, allTypes } = createTestCases(spaceId);
- // use singleRequest to reduce execution time and/or test combined cases
+ const createTests = (overwrite: boolean, trueCopy: boolean, spaceId: string) => {
+ const singleRequest = true;
+
+ if (trueCopy) {
+ const { importable, nonImportable, all } = createTrueCopyTestCases();
+ return {
+ unauthorized: [
+ createTestDefinitions(importable, true, { trueCopy, spaceId }),
+ createTestDefinitions(nonImportable, false, { trueCopy, spaceId, singleRequest }),
+ createTestDefinitions(all, true, {
+ trueCopy,
+ spaceId,
+ singleRequest,
+ responseBodyOverride: expectForbidden('bulk_create')([
+ 'dashboard',
+ 'globaltype',
+ 'isolatedtype',
+ 'sharedtype',
+ ]),
+ }),
+ ].flat(),
+ authorized: createTestDefinitions(all, false, { trueCopy, spaceId, singleRequest }),
+ };
+ }
+
+ const {
+ group1Importable,
+ group1NonImportable,
+ group1All,
+ group2,
+ group3,
+ group4,
+ } = createTestCases(overwrite, spaceId);
return {
unauthorized: [
- createTestDefinitions(importableTypes, true, { spaceId }),
- createTestDefinitions(nonImportableTypes, false, { spaceId, singleRequest: true }),
- createTestDefinitions(allTypes, true, {
+ createTestDefinitions(group1Importable, true, { overwrite, spaceId }),
+ createTestDefinitions(group1NonImportable, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group1All, true, {
+ overwrite,
spaceId,
- singleRequest: true,
- responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']),
+ singleRequest,
+ responseBodyOverride: expectForbidden('bulk_create')([
+ 'dashboard',
+ 'globaltype',
+ 'isolatedtype',
+ ]),
}),
+ createTestDefinitions(group2, true, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group3, true, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group4, true, { overwrite, spaceId, singleRequest }),
+ ].flat(),
+ authorized: [
+ createTestDefinitions(group1All, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group3, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group4, false, { overwrite, spaceId, singleRequest }),
].flat(),
- authorized: createTestDefinitions(allTypes, false, { spaceId, singleRequest: true }),
};
};
describe('_import', () => {
- getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => {
- const suffix = ` within the ${spaceId} space`;
- const { unauthorized, authorized } = createTests(spaceId);
+ getTestScenarios([
+ [false, false],
+ [false, true],
+ [true, false],
+ ]).securityAndSpaces.forEach(({ spaceId, users, modifier }) => {
+ const [overwrite, trueCopy] = modifier!;
+ const suffix = ` within the ${spaceId} space${
+ overwrite ? ' with overwrite enabled' : trueCopy ? ' with trueCopy enabled' : ''
+ }`;
+ const { unauthorized, authorized } = createTests(overwrite, trueCopy, spaceId);
const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => {
addTests(`${user.description}${suffix}`, { user, spaceId, tests });
};
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts
index 8c16e298c7df9..163fb53253c74 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { v4 as uuidv4 } from 'uuid';
import { SPACES } from '../../common/lib/spaces';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { TestUser } from '../../common/lib/types';
@@ -20,30 +21,65 @@ const {
SPACE_2: { spaceId: SPACE_2_ID },
} = SPACES;
const { fail400, fail409 } = testCaseFailures;
+const destinationId = (condition?: boolean) =>
+ condition !== false ? { successParam: 'destinationId' } : {};
+const newOrigin = () => ({ successParam: 'newOrigin' });
+
+const createTrueCopyTestCases = () => {
+ // for each outcome, if failure !== undefined then we expect to receive
+ // an error; otherwise, we expect to receive a success result
+ const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN');
+ const importable = cases.map(([, val]) => ({
+ ...val,
+ successParam: 'trueCopy',
+ expectedNewId: uuidv4(),
+ }));
+ const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }];
+ const all = [...importable, ...nonImportable];
+ return { importable, nonImportable, all };
+};
const createTestCases = (overwrite: boolean, spaceId: string) => {
// for each permitted (non-403) outcome, if failure !== undefined then we expect
// to receive an error; otherwise, we expect to receive a success result
- const importableTypes = [
- {
- ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE,
- ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID),
- },
- { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) },
- { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) },
+ const singleNamespaceObject =
+ spaceId === DEFAULT_SPACE_ID
+ ? CASES.SINGLE_NAMESPACE_DEFAULT_SPACE
+ : spaceId === SPACE_1_ID
+ ? CASES.SINGLE_NAMESPACE_SPACE_1
+ : CASES.SINGLE_NAMESPACE_SPACE_2;
+ const group1Importable = [
+ { ...singleNamespaceObject, ...fail409(!overwrite) },
{ ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) },
- CASES.NEW_SINGLE_NAMESPACE_OBJ,
- CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
];
- const nonImportableTypes = [
- { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() },
- { ...CASES.HIDDEN, ...fail400() },
- { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() },
+ const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }];
+ const group1All = [...group1Importable, ...group1NonImportable];
+ const group2 = [
+ {
+ ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1,
+ ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)),
+ ...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID),
+ },
+ {
+ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1,
+ ...fail409(!overwrite && spaceId === SPACE_1_ID),
+ ...destinationId(spaceId !== SPACE_1_ID),
+ },
+ {
+ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2,
+ ...fail409(!overwrite && spaceId === SPACE_2_ID),
+ ...destinationId(spaceId !== SPACE_2_ID),
+ },
+ { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
+ { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
+ // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict
+ // 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, ...fail409(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a'
+ { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3'
+ { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a'
];
- const allTypes = importableTypes.concat(nonImportableTypes);
- return { importableTypes, nonImportableTypes, allTypes };
+ return { group1Importable, group1NonImportable, group1All, group2 };
};
export default function ({ getService }: FtrProviderContext) {
@@ -56,47 +92,78 @@ export default function ({ getService }: FtrProviderContext) {
esArchiver,
supertest
);
- const createTests = (overwrite: boolean, spaceId: string) => {
- const { importableTypes, nonImportableTypes, allTypes } = createTestCases(overwrite, spaceId);
- const singleRequest = true;
+ const createTests = (overwrite: boolean, trueCopy: boolean, spaceId: string) => {
// use singleRequest to reduce execution time and/or test combined cases
+ const singleRequest = true;
+
+ if (trueCopy) {
+ const { importable, nonImportable, all } = createTrueCopyTestCases();
+ return {
+ unauthorized: [
+ createTestDefinitions(importable, true, { trueCopy, spaceId }),
+ createTestDefinitions(nonImportable, false, { trueCopy, spaceId, singleRequest }),
+ createTestDefinitions(all, true, {
+ trueCopy,
+ spaceId,
+ singleRequest,
+ responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype', 'sharedtype']),
+ }),
+ ].flat(),
+ authorized: createTestDefinitions(all, false, { trueCopy, spaceId, singleRequest }),
+ };
+ }
+
+ const { group1Importable, group1NonImportable, group1All, group2 } = createTestCases(
+ overwrite,
+ spaceId
+ );
return {
unauthorized: [
- createTestDefinitions(importableTypes, true, overwrite, { spaceId }),
- createTestDefinitions(nonImportableTypes, false, overwrite, { spaceId, singleRequest }),
- createTestDefinitions(allTypes, true, overwrite, {
+ createTestDefinitions(group1Importable, true, { overwrite, spaceId }),
+ createTestDefinitions(group1NonImportable, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group1All, true, {
+ overwrite,
spaceId,
singleRequest,
- responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']),
+ responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype']),
}),
+ createTestDefinitions(group2, true, { overwrite, spaceId, singleRequest }),
+ ].flat(),
+ authorized: [
+ createTestDefinitions(group1All, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }),
].flat(),
- authorized: createTestDefinitions(allTypes, false, overwrite, { spaceId, singleRequest }),
};
};
describe('_resolve_import_errors', () => {
- getTestScenarios([false, true]).securityAndSpaces.forEach(
- ({ spaceId, users, modifier: overwrite }) => {
- const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`;
- const { unauthorized, authorized } = createTests(overwrite!, spaceId);
- const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => {
- addTests(`${user.description}${suffix}`, { user, spaceId, tests });
- };
+ getTestScenarios([
+ [false, false],
+ [false, true],
+ [true, false],
+ ]).securityAndSpaces.forEach(({ spaceId, users, modifier }) => {
+ const [overwrite, trueCopy] = modifier!;
+ const suffix = ` within the ${spaceId} space${
+ overwrite ? ' with overwrite enabled' : trueCopy ? ' with trueCopy enabled' : ''
+ }`;
+ const { unauthorized, authorized } = createTests(overwrite, trueCopy, spaceId);
+ const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => {
+ addTests(`${user.description}${suffix}`, { user, spaceId, tests });
+ };
- [
- users.noAccess,
- users.legacyAll,
- users.dualRead,
- users.readGlobally,
- users.readAtSpace,
- users.allAtOtherSpace,
- ].forEach((user) => {
- _addTests(user, unauthorized);
- });
- [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => {
- _addTests(user, authorized);
- });
- }
- );
+ [
+ users.noAccess,
+ users.legacyAll,
+ users.dualRead,
+ users.readGlobally,
+ users.readAtSpace,
+ users.allAtOtherSpace,
+ ].forEach((user) => {
+ _addTests(user, unauthorized);
+ });
+ [users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => {
+ _addTests(user, authorized);
+ });
+ });
});
}
diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts
index 464a5a1e76016..725120687c231 100644
--- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts
@@ -14,6 +14,7 @@ import {
} from '../../common/suites/bulk_create';
const { fail400, fail409 } = testCaseFailures;
+const unresolvableConflict = () => ({ fail409Param: 'unresolvableConflict' });
const createTestCases = (overwrite: boolean) => {
// for each permitted (non-403) outcome, if failure !== undefined then we expect
@@ -23,8 +24,8 @@ const createTestCases = (overwrite: boolean) => {
CASES.SINGLE_NAMESPACE_SPACE_1,
CASES.SINGLE_NAMESPACE_SPACE_2,
{ ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409() },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409() },
+ { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(), ...unresolvableConflict() },
+ { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(), ...unresolvableConflict() },
{ ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) },
CASES.NEW_SINGLE_NAMESPACE_OBJ,
CASES.NEW_MULTI_NAMESPACE_OBJ,
diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts
index 61ff6eeb4bd80..99babf683ccfa 100644
--- a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts
+++ b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts
@@ -18,15 +18,12 @@ const createTestCases = () => {
const exportableTypes = [
cases.singleNamespaceObject,
cases.singleNamespaceType,
- cases.namespaceAgnosticObject,
- cases.namespaceAgnosticType,
- ];
- const nonExportableTypes = [
cases.multiNamespaceObject,
cases.multiNamespaceType,
- cases.hiddenObject,
- cases.hiddenType,
+ cases.namespaceAgnosticObject,
+ cases.namespaceAgnosticType,
];
+ const nonExportableTypes = [cases.hiddenObject, cases.hiddenType];
const allTypes = exportableTypes.concat(nonExportableTypes);
return { exportableTypes, nonExportableTypes, allTypes };
};
diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts
index beec276b3bd73..3ff7925628d38 100644
--- a/x-pack/test/saved_object_api_integration/security_only/apis/import.ts
+++ b/x-pack/test/saved_object_api_integration/security_only/apis/import.ts
@@ -14,27 +14,63 @@ import {
} from '../../common/suites/import';
const { fail400, fail409 } = testCaseFailures;
+const destinationId = (condition?: boolean) =>
+ condition !== false ? { successParam: 'destinationId' } : {};
+const newOrigin = () => ({ successParam: 'newOrigin' });
+const ambiguousConflict = (suffix: string) => ({
+ failure: 409 as 409,
+ fail409Param: `ambiguous_conflict_${suffix}`,
+});
-const createTestCases = () => {
+const createTrueCopyTestCases = () => {
+ // for each outcome, if failure !== undefined then we expect to receive
+ // an error; otherwise, we expect to receive a success result
+ const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN');
+ const importable = cases.map(([, val]) => ({ ...val, successParam: 'trueCopy' }));
+ const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }];
+ const all = [...importable, ...nonImportable];
+ return { importable, nonImportable, all };
+};
+
+const createTestCases = (overwrite: boolean) => {
// for each permitted (non-403) outcome, if failure !== undefined then we expect
// to receive an error; otherwise, we expect to receive a success result
- const importableTypes = [
- { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409() },
+ const group1Importable = [
+ // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes
+ { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) },
CASES.SINGLE_NAMESPACE_SPACE_1,
CASES.SINGLE_NAMESPACE_SPACE_2,
- { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() },
+ { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) },
CASES.NEW_SINGLE_NAMESPACE_OBJ,
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
];
- const nonImportableTypes = [
- { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() },
- { ...CASES.HIDDEN, ...fail400() },
- { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() },
+ const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }];
+ const group1All = group1Importable.concat(group1NonImportable);
+ const group2 = [
+ // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes
+ CASES.NEW_MULTI_NAMESPACE_OBJ,
+ { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) },
+ { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...destinationId() },
+ { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...destinationId() },
+ { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
+ { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
+ { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict
+ { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict
+ ];
+ const group3 = [
+ // when overwrite=true, all of the objects in this group are errors, so we cannot check the created object attributes
+ // grouping errors together simplifies the test suite code
+ { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict
+ ];
+ const group4 = [
+ // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes
+ { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict
+ CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match
+ CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match
+ { ...CASES.CONFLICT_2C_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID
+ { ...CASES.CONFLICT_2D_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID
];
- const allTypes = importableTypes.concat(nonImportableTypes);
- return { importableTypes, nonImportableTypes, allTypes };
+ return { group1Importable, group1NonImportable, group1All, group2, group3, group4 };
};
export default function ({ getService }: FtrProviderContext) {
@@ -47,27 +83,80 @@ export default function ({ getService }: FtrProviderContext) {
esArchiver,
supertest
);
- const createTests = () => {
- const { importableTypes, nonImportableTypes, allTypes } = createTestCases();
+ const createTests = (overwrite: boolean, trueCopy: boolean) => {
// use singleRequest to reduce execution time and/or test combined cases
+ const singleRequest = true;
+
+ if (trueCopy) {
+ const { importable, nonImportable, all } = createTrueCopyTestCases();
+ return {
+ unauthorized: [
+ createTestDefinitions(importable, true, { trueCopy }),
+ createTestDefinitions(nonImportable, false, { trueCopy, singleRequest }),
+ createTestDefinitions(all, true, {
+ trueCopy,
+ singleRequest,
+ responseBodyOverride: expectForbidden('bulk_create')([
+ 'dashboard',
+ 'globaltype',
+ 'isolatedtype',
+ 'sharedtype',
+ ]),
+ }),
+ ].flat(),
+ authorized: createTestDefinitions(all, false, { trueCopy, singleRequest }),
+ };
+ }
+
+ const {
+ group1Importable,
+ group1NonImportable,
+ group1All,
+ group2,
+ group3,
+ group4,
+ } = createTestCases(overwrite);
return {
unauthorized: [
- createTestDefinitions(importableTypes, true),
- createTestDefinitions(nonImportableTypes, false, { singleRequest: true }),
- createTestDefinitions(allTypes, true, {
- singleRequest: true,
- responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']),
+ createTestDefinitions(group1Importable, true, { overwrite }),
+ createTestDefinitions(group1NonImportable, false, { overwrite, singleRequest }),
+ createTestDefinitions(group1All, true, {
+ overwrite,
+ singleRequest,
+ responseBodyOverride: expectForbidden('bulk_create')([
+ 'dashboard',
+ 'globaltype',
+ 'isolatedtype',
+ ]),
}),
+ createTestDefinitions(group2, true, { overwrite, singleRequest }),
+ createTestDefinitions(group3, true, { overwrite, singleRequest }),
+ createTestDefinitions(group4, true, { overwrite, singleRequest }),
+ ].flat(),
+ authorized: [
+ createTestDefinitions(group1All, false, { overwrite, singleRequest }),
+ createTestDefinitions(group2, false, { overwrite, singleRequest }),
+ createTestDefinitions(group3, false, { overwrite, singleRequest }),
+ createTestDefinitions(group4, false, { overwrite, singleRequest }),
].flat(),
- authorized: createTestDefinitions(allTypes, false, { singleRequest: true }),
};
};
describe('_import', () => {
- getTestScenarios().security.forEach(({ users }) => {
- const { unauthorized, authorized } = createTests();
+ getTestScenarios([
+ [false, false],
+ [false, true],
+ [true, false],
+ ]).security.forEach(({ users, modifier }) => {
+ const [overwrite, trueCopy] = modifier!;
+ const suffix = overwrite
+ ? ' with overwrite enabled'
+ : trueCopy
+ ? ' with trueCopy enabled'
+ : '';
+ const { unauthorized, authorized } = createTests(overwrite, trueCopy);
const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => {
- addTests(user.description, { user, tests });
+ addTests(`${user.description}${suffix}`, { user, tests });
};
[
diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts
index a0abe4b0483f8..dacf1d4745274 100644
--- a/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts
+++ b/x-pack/test/saved_object_api_integration/security_only/apis/resolve_import_errors.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { v4 as uuidv4 } from 'uuid';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { TestUser } from '../../common/lib/types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@@ -14,27 +15,45 @@ import {
} from '../../common/suites/resolve_import_errors';
const { fail400, fail409 } = testCaseFailures;
+const destinationId = (condition?: boolean) =>
+ condition !== false ? { successParam: 'destinationId' } : {};
+const newOrigin = () => ({ successParam: 'newOrigin' });
+
+const createTrueCopyTestCases = () => {
+ // for each outcome, if failure !== undefined then we expect to receive
+ // an error; otherwise, we expect to receive a success result
+ const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN');
+ const importable = cases.map(([, val]) => ({
+ ...val,
+ successParam: 'trueCopy',
+ expectedNewId: uuidv4(),
+ }));
+ const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }];
+ const all = [...importable, ...nonImportable];
+ return { importable, nonImportable, all };
+};
const createTestCases = (overwrite: boolean) => {
// for each permitted (non-403) outcome, if failure !== undefined then we expect
// to receive an error; otherwise, we expect to receive a success result
- const importableTypes = [
+ const group1Importable = [
{ ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) },
- CASES.SINGLE_NAMESPACE_SPACE_1,
- CASES.SINGLE_NAMESPACE_SPACE_2,
{ ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) },
- CASES.NEW_SINGLE_NAMESPACE_OBJ,
- CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
];
- const nonImportableTypes = [
- { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() },
- { ...CASES.HIDDEN, ...fail400() },
- { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() },
+ const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }];
+ const group1All = [...group1Importable, ...group1NonImportable];
+ const group2 = [
+ { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) },
+ { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
+ { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
+ // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict
+ // 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, ...fail409(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a'
+ { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3'
+ { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a'
];
- const allTypes = importableTypes.concat(nonImportableTypes);
- return { importableTypes, nonImportableTypes, allTypes };
+ return { group1Importable, group1NonImportable, group1All, group2 };
};
export default function ({ getService }: FtrProviderContext) {
@@ -47,26 +66,58 @@ export default function ({ getService }: FtrProviderContext) {
esArchiver,
supertest
);
- const createTests = (overwrite: boolean) => {
- const { importableTypes, nonImportableTypes, allTypes } = createTestCases(overwrite);
+ const createTests = (overwrite: boolean, trueCopy: boolean) => {
// use singleRequest to reduce execution time and/or test combined cases
+ const singleRequest = true;
+
+ if (trueCopy) {
+ const { importable, nonImportable, all } = createTrueCopyTestCases();
+ return {
+ unauthorized: [
+ createTestDefinitions(importable, true, { trueCopy }),
+ createTestDefinitions(nonImportable, false, { trueCopy, singleRequest }),
+ createTestDefinitions(all, true, {
+ trueCopy,
+ singleRequest,
+ responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype', 'sharedtype']),
+ }),
+ ].flat(),
+ authorized: createTestDefinitions(all, false, { trueCopy, singleRequest }),
+ };
+ }
+
+ const { group1Importable, group1NonImportable, group1All, group2 } = createTestCases(overwrite);
return {
unauthorized: [
- createTestDefinitions(importableTypes, true, overwrite),
- createTestDefinitions(nonImportableTypes, false, overwrite, { singleRequest: true }),
- createTestDefinitions(allTypes, true, overwrite, {
- singleRequest: true,
- responseBodyOverride: expectForbidden(['dashboard', 'globaltype', 'isolatedtype']),
+ createTestDefinitions(group1Importable, true, { overwrite }),
+ createTestDefinitions(group1NonImportable, false, { overwrite, singleRequest }),
+ createTestDefinitions(group1All, true, {
+ overwrite,
+ singleRequest,
+ responseBodyOverride: expectForbidden(['globaltype', 'isolatedtype']),
}),
+ createTestDefinitions(group2, true, { overwrite, singleRequest }),
+ ].flat(),
+ authorized: [
+ createTestDefinitions(group1All, false, { overwrite, singleRequest }),
+ createTestDefinitions(group2, false, { overwrite, singleRequest }),
].flat(),
- authorized: createTestDefinitions(allTypes, false, overwrite, { singleRequest: true }),
};
};
describe('_resolve_import_errors', () => {
- getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => {
- const suffix = overwrite ? ' with overwrite enabled' : '';
- const { unauthorized, authorized } = createTests(overwrite!);
+ getTestScenarios([
+ [false, false],
+ [false, true],
+ [true, false],
+ ]).security.forEach(({ users, modifier }) => {
+ const [overwrite, trueCopy] = modifier!;
+ const suffix = overwrite
+ ? ' with overwrite enabled'
+ : trueCopy
+ ? ' with trueCopy enabled'
+ : '';
+ const { unauthorized, authorized } = createTests(overwrite, trueCopy);
const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => {
addTests(`${user.description}${suffix}`, { user, tests });
};
diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts
index f9edc56b8ffea..74fade39bf7a5 100644
--- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts
@@ -16,6 +16,8 @@ const {
SPACE_2: { spaceId: SPACE_2_ID },
} = SPACES;
const { fail400, fail409 } = testCaseFailures;
+const unresolvableConflict = (condition?: boolean) =>
+ condition !== false ? { fail409Param: 'unresolvableConflict' } : {};
const createTestCases = (overwrite: boolean, spaceId: string) => [
// for each outcome, if failure !== undefined then we expect to receive
@@ -29,9 +31,18 @@ const createTestCases = (overwrite: boolean, spaceId: string) => [
{
...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1,
...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)),
+ ...unresolvableConflict(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID),
+ },
+ {
+ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1,
+ ...fail409(!overwrite || spaceId !== SPACE_1_ID),
+ ...unresolvableConflict(spaceId !== SPACE_1_ID),
+ },
+ {
+ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2,
+ ...fail409(!overwrite || spaceId !== SPACE_2_ID),
+ ...unresolvableConflict(spaceId !== SPACE_2_ID),
},
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) },
{ ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) },
{ ...CASES.HIDDEN, ...fail400() },
CASES.NEW_SINGLE_NAMESPACE_OBJ,
diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts
index 45a76a2f39e37..fcb66da3b0ed1 100644
--- a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts
+++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts
@@ -15,22 +15,75 @@ const {
SPACE_2: { spaceId: SPACE_2_ID },
} = SPACES;
const { fail400, fail409 } = testCaseFailures;
+const destinationId = (condition?: boolean) =>
+ condition !== false ? { successParam: 'destinationId' } : {};
+const newOrigin = () => ({ successParam: 'newOrigin' });
+const ambiguousConflict = (suffix: string) => ({
+ failure: 409 as 409,
+ fail409Param: `ambiguous_conflict_${suffix}`,
+});
-const createTestCases = (spaceId: string) => [
+const createTrueCopyTestCases = () => {
// for each outcome, if failure !== undefined then we expect to receive
// an error; otherwise, we expect to receive a success result
- { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(spaceId === DEFAULT_SPACE_ID) },
- { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(spaceId === SPACE_1_ID) },
- { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(spaceId === SPACE_2_ID) },
- { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() },
- { ...CASES.NAMESPACE_AGNOSTIC, ...fail409() },
- { ...CASES.HIDDEN, ...fail400() },
- CASES.NEW_SINGLE_NAMESPACE_OBJ,
- { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() },
- CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
-];
+ const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN');
+ return [
+ ...cases.map(([, val]) => ({ ...val, successParam: 'trueCopy' })),
+ { ...CASES.HIDDEN, ...fail400() },
+ ];
+};
+
+const createTestCases = (overwrite: boolean, spaceId: string) => {
+ // for each outcome, if failure !== undefined then we expect to receive
+ // an error; otherwise, we expect to receive a success result
+ const group1 = [
+ // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes
+ {
+ ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE,
+ ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID),
+ },
+ { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) },
+ { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) },
+ {
+ ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1,
+ ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)),
+ ...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID),
+ },
+ {
+ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1,
+ ...fail409(!overwrite && spaceId === SPACE_1_ID),
+ ...destinationId(spaceId !== SPACE_1_ID),
+ },
+ {
+ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2,
+ ...fail409(!overwrite && spaceId === SPACE_2_ID),
+ ...destinationId(spaceId !== SPACE_2_ID),
+ },
+ { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) },
+ { ...CASES.HIDDEN, ...fail400() },
+ { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
+ { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
+ { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict
+ { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict
+ CASES.NEW_SINGLE_NAMESPACE_OBJ,
+ CASES.NEW_MULTI_NAMESPACE_OBJ,
+ CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
+ ];
+ const group2 = [
+ // when overwrite=true, all of the objects in this group are errors, so we cannot check the created object attributes
+ // grouping errors together simplifies the test suite code
+ { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict
+ ];
+ const group3 = [
+ // when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes
+ { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict
+ CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match
+ CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match
+ { ...CASES.CONFLICT_2C_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID
+ { ...CASES.CONFLICT_2D_OBJ, ...newOrigin() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID
+ ];
+ return { group1, group2, group3 };
+};
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
@@ -38,15 +91,35 @@ export default function ({ getService }: FtrProviderContext) {
const es = getService('legacyEs');
const { addTests, createTestDefinitions } = importTestSuiteFactory(es, esArchiver, supertest);
- const createTests = (spaceId: string) => {
- const testCases = createTestCases(spaceId);
- return createTestDefinitions(testCases, false, { spaceId, singleRequest: true });
+ const createTests = (overwrite: boolean, trueCopy: boolean, spaceId: string) => {
+ const singleRequest = true;
+ if (trueCopy) {
+ const cases = createTrueCopyTestCases();
+ return createTestDefinitions(cases, false, { trueCopy, spaceId, singleRequest });
+ }
+
+ const { group1, group2, group3 } = createTestCases(overwrite, spaceId);
+ return [
+ createTestDefinitions(group1, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group3, false, { overwrite, spaceId, singleRequest }),
+ ].flat();
};
describe('_import', () => {
- getTestScenarios().spaces.forEach(({ spaceId }) => {
- const tests = createTests(spaceId);
- addTests(`within the ${spaceId} space`, { spaceId, tests });
+ getTestScenarios([
+ [false, false],
+ [false, true],
+ [true, false],
+ ]).spaces.forEach(({ spaceId, modifier }) => {
+ const [overwrite, trueCopy] = modifier!;
+ const suffix = overwrite
+ ? ' with overwrite enabled'
+ : trueCopy
+ ? ' with trueCopy enabled'
+ : '';
+ const tests = createTests(overwrite, trueCopy, spaceId);
+ addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests });
});
});
}
diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts
index a6ef902e2e9eb..f95b3e4b13bec 100644
--- a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts
+++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { v4 as uuidv4 } from 'uuid';
import { SPACES } from '../../common/lib/spaces';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@@ -18,25 +19,58 @@ const {
SPACE_2: { spaceId: SPACE_2_ID },
} = SPACES;
const { fail400, fail409 } = testCaseFailures;
+const destinationId = (condition?: boolean) =>
+ condition !== false ? { successParam: 'destinationId' } : {};
+const newOrigin = () => ({ successParam: 'newOrigin' });
-const createTestCases = (overwrite: boolean, spaceId: string) => [
+const createTrueCopyTestCases = () => {
// for each outcome, if failure !== undefined then we expect to receive
// an error; otherwise, we expect to receive a success result
- {
- ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE,
- ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID),
- },
- { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) },
- { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) },
- { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail400() },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail400() },
- { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail400() },
- { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) },
- { ...CASES.HIDDEN, ...fail400() },
- CASES.NEW_SINGLE_NAMESPACE_OBJ,
- { ...CASES.NEW_MULTI_NAMESPACE_OBJ, ...fail400() },
- CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
-];
+ const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN');
+ return [
+ ...cases.map(([, val]) => ({ ...val, successParam: 'trueCopy', expectedNewId: uuidv4() })),
+ { ...CASES.HIDDEN, ...fail400() },
+ ];
+};
+
+const createTestCases = (overwrite: boolean, spaceId: string) => {
+ // for each outcome, if failure !== undefined then we expect to receive
+ // an error; otherwise, we expect to receive a success result
+ const singleNamespaceObject =
+ spaceId === DEFAULT_SPACE_ID
+ ? CASES.SINGLE_NAMESPACE_DEFAULT_SPACE
+ : spaceId === SPACE_1_ID
+ ? CASES.SINGLE_NAMESPACE_SPACE_1
+ : CASES.SINGLE_NAMESPACE_SPACE_2;
+ return [
+ { ...singleNamespaceObject, ...fail409(!overwrite) },
+ {
+ ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1,
+ ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)),
+ ...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID),
+ },
+ {
+ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1,
+ ...fail409(!overwrite && spaceId === SPACE_1_ID),
+ ...destinationId(spaceId !== SPACE_1_ID),
+ },
+ {
+ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2,
+ ...fail409(!overwrite && spaceId === SPACE_2_ID),
+ ...destinationId(spaceId !== SPACE_2_ID),
+ },
+ { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) },
+ { ...CASES.HIDDEN, ...fail400() },
+ { ...CASES.CONFLICT_1A_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
+ { ...CASES.CONFLICT_1B_OBJ, ...newOrigin() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
+ // all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict
+ // 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, ...fail409(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a'
+ { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3'
+ { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a'
+ ];
+};
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
@@ -48,15 +82,32 @@ export default function ({ getService }: FtrProviderContext) {
esArchiver,
supertest
);
- const createTests = (overwrite: boolean, spaceId: string) => {
+ const createTests = (overwrite: boolean, trueCopy: boolean, spaceId: string) => {
+ const singleRequest = true;
+ if (trueCopy) {
+ const cases = createTrueCopyTestCases();
+ // The resolveImportErrors API doesn't actually have a flag for "trueCopy" mode; rather, we create test cases as if we are resolving
+ // errors from a call to the import API that had trueCopy mode enabled.
+ return createTestDefinitions(cases, false, { trueCopy, spaceId, singleRequest });
+ }
+
const testCases = createTestCases(overwrite, spaceId);
- return createTestDefinitions(testCases, false, overwrite, { spaceId, singleRequest: true });
+ return createTestDefinitions(testCases, false, { overwrite, spaceId, singleRequest });
};
describe('_resolve_import_errors', () => {
- getTestScenarios([false, true]).spaces.forEach(({ spaceId, modifier: overwrite }) => {
- const suffix = overwrite ? ' with overwrite enabled' : '';
- const tests = createTests(overwrite!, spaceId);
+ getTestScenarios([
+ [false, false],
+ [false, true],
+ [true, false],
+ ]).spaces.forEach(({ spaceId, modifier }) => {
+ const [overwrite, trueCopy] = modifier!;
+ const suffix = overwrite
+ ? ' with overwrite enabled'
+ : trueCopy
+ ? ' with trueCopy enabled'
+ : '';
+ const tests = createTests(overwrite, trueCopy, spaceId);
addTests(`within the ${spaceId} space${suffix}`, { spaceId, tests });
});
});
diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
index 9a8a0a1fdda14..7e528c23c20a0 100644
--- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
+++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
@@ -380,11 +380,11 @@
{
"type": "doc",
"value": {
- "id": "sharedtype:default_space_only",
+ "id": "sharedtype:default_only",
"index": ".kibana",
"source": {
"sharedtype": {
- "title": "A shared saved-object in the default space"
+ "title": "A shared saved-object in one space"
},
"type": "sharedtype",
"namespaces": ["default"],
@@ -401,7 +401,7 @@
"index": ".kibana",
"source": {
"sharedtype": {
- "title": "A shared saved-object in the space_1 space"
+ "title": "A shared saved-object in one space"
},
"type": "sharedtype",
"namespaces": ["space_1"],
@@ -418,7 +418,7 @@
"index": ".kibana",
"source": {
"sharedtype": {
- "title": "A shared saved-object in the space_2 space"
+ "title": "A shared saved-object in one space"
},
"type": "sharedtype",
"namespaces": ["space_2"],
@@ -496,3 +496,128 @@
}
}
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_1_default",
+ "index": ".kibana",
+ "source": {
+ "originId": "conflict_1",
+ "sharedtype": {
+ "title": "A shared saved-object in one space"
+ },
+ "type": "sharedtype",
+ "namespaces": ["default"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_1_space_1",
+ "index": ".kibana",
+ "source": {
+ "originId": "conflict_1",
+ "sharedtype": {
+ "title": "A shared saved-object in one space"
+ },
+ "type": "sharedtype",
+ "namespaces": ["space_1"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_1_space_2",
+ "index": ".kibana",
+ "source": {
+ "originId": "conflict_1",
+ "sharedtype": {
+ "title": "A shared saved-object in one space"
+ },
+ "type": "sharedtype",
+ "namespaces": ["space_2"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_2_default",
+ "index": ".kibana",
+ "source": {
+ "originId": "conflict_2",
+ "sharedtype": {
+ "title": "A shared saved-object in one space"
+ },
+ "type": "sharedtype",
+ "namespaces": ["default"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_2_space_1",
+ "index": ".kibana",
+ "source": {
+ "originId": "conflict_2",
+ "sharedtype": {
+ "title": "A shared saved-object in one space"
+ },
+ "type": "sharedtype",
+ "namespaces": ["space_1"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_2_space_2",
+ "index": ".kibana",
+ "source": {
+ "originId": "conflict_2",
+ "sharedtype": {
+ "title": "A shared saved-object in one space"
+ },
+ "type": "sharedtype",
+ "namespaces": ["space_2"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_2_all",
+ "index": ".kibana",
+ "source": {
+ "originId": "conflict_2",
+ "sharedtype": {
+ "title": "A shared saved-object in all spaces"
+ },
+ "type": "sharedtype",
+ "namespaces": ["default", "space_1", "space_2"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json
index 508de68c32f70..a2f8088ce0436 100644
--- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json
+++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json
@@ -162,6 +162,9 @@
"namespaces": {
"type": "keyword"
},
+ "originId": {
+ "type": "keyword"
+ },
"search": {
"properties": {
"columns": {
diff --git a/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts b/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts
index ee03fa6b648af..ed45f0870a45c 100644
--- a/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts
+++ b/x-pack/test/spaces_api_integration/common/fixtures/spaces_test_plugin/server/plugin.ts
@@ -15,6 +15,12 @@ export class Plugin {
name: 'sharedtype',
hidden: false,
namespaceType: 'multiple',
+ management: {
+ importableAndExportable: true,
+ getTitle(obj) {
+ return obj.attributes.title;
+ },
+ },
mappings: {
properties: {
title: { type: 'text' },
diff --git a/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts b/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts
index 67f5d737ba010..3b0f5f8570aa3 100644
--- a/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts
+++ b/x-pack/test/spaces_api_integration/common/lib/saved_object_test_cases.ts
@@ -5,8 +5,8 @@
*/
export const MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES = Object.freeze({
- DEFAULT_SPACE_ONLY: Object.freeze({
- id: 'default_space_only',
+ DEFAULT_ONLY: Object.freeze({
+ id: 'default_only',
existingNamespaces: ['default'],
}),
SPACE_1_ONLY: Object.freeze({
diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
index ebec70793e8fd..0386a2afccb3d 100644
--- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
+++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
@@ -19,6 +19,11 @@ interface CopyToSpaceTest {
response: (resp: TestResponse) => Promise;
}
+interface CopyToSpaceMultiNamespaceTest extends CopyToSpaceTest {
+ testTitle: string;
+ objects: Array>;
+}
+
interface CopyToSpaceTests {
noConflictsWithoutReferences: CopyToSpaceTest;
noConflictsWithReferences: CopyToSpaceTest;
@@ -30,6 +35,7 @@ interface CopyToSpaceTests {
withConflictsResponse: (resp: TestResponse) => Promise;
noConflictsResponse: (resp: TestResponse) => Promise;
};
+ multiNamespaceTestCases: (overwrite: boolean) => CopyToSpaceMultiNamespaceTest[];
}
interface CopyToSpaceTestDefinition {
@@ -53,28 +59,14 @@ interface SpaceBucket {
}
const INITIAL_COUNTS: Record> = {
- [DEFAULT_SPACE_ID]: {
- dashboard: 2,
- visualization: 3,
- 'index-pattern': 1,
- },
- space_1: {
- dashboard: 2,
- visualization: 3,
- 'index-pattern': 1,
- },
- space_2: {
- dashboard: 1,
- },
+ [DEFAULT_SPACE_ID]: { dashboard: 2, visualization: 3, 'index-pattern': 1 },
+ space_1: { dashboard: 2, visualization: 3, 'index-pattern': 1 },
+ space_2: { dashboard: 1 },
};
const getDestinationWithoutConflicts = () => 'space_2';
-const getDestinationWithConflicts = (originSpaceId?: string) => {
- if (!originSpaceId || originSpaceId === DEFAULT_SPACE_ID) {
- return 'space_1';
- }
- return DEFAULT_SPACE_ID;
-};
+const getDestinationWithConflicts = (originSpaceId?: string) =>
+ !originSpaceId || originSpaceId === DEFAULT_SPACE_ID ? 'space_1' : DEFAULT_SPACE_ID;
export function copyToSpaceTestSuiteFactory(
es: any,
@@ -86,27 +78,11 @@ export function copyToSpaceTestSuiteFactory(
index: '.kibana',
body: {
size: 0,
- query: {
- terms: {
- type: ['visualization', 'dashboard', 'index-pattern'],
- },
- },
+ query: { terms: { type: ['visualization', 'dashboard', 'index-pattern'] } },
aggs: {
count: {
- terms: {
- field: 'namespace',
- missing: DEFAULT_SPACE_ID,
- size: 10,
- },
- aggs: {
- countByType: {
- terms: {
- field: 'type',
- missing: 'UNKNOWN',
- size: 10,
- },
- },
- },
+ terms: { field: 'namespace', missing: DEFAULT_SPACE_ID, size: 10 },
+ aggs: { countByType: { terms: { field: 'type', missing: 'UNKNOWN', size: 10 } } },
},
},
},
@@ -135,13 +111,7 @@ export function copyToSpaceTestSuiteFactory(
const { countByType } = spaceBucket;
const expectedBuckets = Object.entries(expectedCounts).reduce((acc, entry) => {
const [type, count] = entry;
- return [
- ...acc,
- {
- key: type,
- doc_count: count,
- },
- ];
+ return [...acc, { key: type, doc_count: count }];
}, [] as CountByTypeBucket[]);
expectedBuckets.sort(bucketSorter);
@@ -154,14 +124,6 @@ export function copyToSpaceTestSuiteFactory(
});
};
- const expectRbacForbiddenResponse = async (resp: TestResponse) => {
- expect(resp.body).to.eql({
- statusCode: 403,
- error: 'Forbidden',
- message: 'Unable to bulk_get dashboard',
- });
- };
-
const expectNotFoundResponse = async (resp: TestResponse) => {
expect(resp.body).to.eql({
statusCode: 404,
@@ -179,6 +141,7 @@ export function copyToSpaceTestSuiteFactory(
[spaceId]: {
success: true,
successCount: 1,
+ successResults: [{ id: 'cts_dashboard', type: 'dashboard' }],
},
} as CopyResponse);
@@ -198,13 +161,22 @@ export function copyToSpaceTestSuiteFactory(
1
);
- const expectNoConflictsWithReferencesResult = async (resp: TestResponse) => {
+ const expectNoConflictsWithReferencesResult = (spaceId: string = DEFAULT_SPACE_ID) => async (
+ resp: TestResponse
+ ) => {
const destination = getDestinationWithoutConflicts();
const result = resp.body as CopyResponse;
expect(result).to.eql({
[destination]: {
success: true,
successCount: 5,
+ successResults: [
+ { id: 'cts_ip_1', type: 'index-pattern' },
+ { id: `cts_vis_1_${spaceId}`, type: 'visualization' },
+ { id: `cts_vis_2_${spaceId}`, type: 'visualization' },
+ { id: 'cts_vis_3', type: 'visualization' },
+ { id: 'cts_dashboard', type: 'dashboard' },
+ ],
},
} as CopyResponse);
@@ -288,6 +260,13 @@ export function copyToSpaceTestSuiteFactory(
[destination]: {
success: true,
successCount: 5,
+ successResults: [
+ { id: 'cts_ip_1', type: 'index-pattern' },
+ { id: `cts_vis_1_${spaceId}`, type: 'visualization' },
+ { id: `cts_vis_2_${spaceId}`, type: 'visualization' },
+ { id: 'cts_vis_3', type: 'visualization' },
+ { id: 'cts_dashboard', type: 'dashboard' },
+ ],
},
} as CopyResponse);
@@ -309,27 +288,25 @@ export function copyToSpaceTestSuiteFactory(
const result = resp.body as CopyResponse;
result[destination].errors!.sort(errorSorter);
+ const expectedSuccessResults = [
+ { id: `cts_vis_1_${spaceId}`, type: 'visualization' },
+ { id: `cts_vis_2_${spaceId}`, type: 'visualization' },
+ ];
const expectedErrors = [
{
- error: {
- type: 'conflict',
- },
+ error: { type: 'conflict' },
id: 'cts_dashboard',
title: `This is the ${spaceId} test space CTS dashboard`,
type: 'dashboard',
},
{
- error: {
- type: 'conflict',
- },
+ error: { type: 'conflict' },
id: 'cts_ip_1',
title: `Copy to Space index pattern 1 from ${spaceId} space`,
type: 'index-pattern',
},
{
- error: {
- type: 'conflict',
- },
+ error: { type: 'conflict' },
id: 'cts_vis_3',
title: `CTS vis 3 from ${spaceId} space`,
type: 'visualization',
@@ -341,16 +318,169 @@ export function copyToSpaceTestSuiteFactory(
[destination]: {
success: false,
successCount: 2,
+ successResults: expectedSuccessResults,
errors: expectedErrors,
},
} as CopyResponse);
- // Query ES to ensure that we copied everything we expected
- await assertSpaceCounts(destination, {
- dashboard: 2,
- visualization: 5,
- 'index-pattern': 1,
- });
+ // Query ES to ensure that no objects were created
+ await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
+ };
+
+ /**
+ * Creates test cases for multi-namespace saved object types.
+ * Note: these are written with the assumption that test data will only be reloaded between each group of test cases, *not* before every
+ * single test case. This saves time during test execution.
+ */
+ const createMultiNamespaceTestCases = (
+ spaceId: string,
+ outcome: 'authorized' | 'unauthorizedRead' | 'unauthorizedWrite' | 'noAccess' = 'authorized'
+ ) => (overwrite: boolean): CopyToSpaceMultiNamespaceTest[] => {
+ // the status code of the HTTP response differs depending on the error type
+ // a 403 error actually comes back as an HTTP 200 response
+ const statusCode = outcome === 'noAccess' ? 404 : 200;
+ const type = 'sharedtype';
+ const v4 = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i);
+ const noConflictId = `${spaceId}_only`;
+ const exactMatchId = 'all_spaces';
+ const inexactMatchId = `conflict_1_${spaceId}`;
+ const ambiguousConflictId = `conflict_2_${spaceId}`;
+
+ const getResult = (response: TestResponse) => (response.body as CopyResponse).space_2;
+ const expectForbiddenResponse = (response: TestResponse) => {
+ expect(response.body).to.eql({
+ space_2: {
+ success: false,
+ successCount: 0,
+ errors: [
+ { statusCode: 403, error: 'Forbidden', message: `Unable to bulk_create sharedtype` },
+ ],
+ },
+ });
+ };
+
+ return [
+ {
+ testTitle: 'copying with no conflict',
+ objects: [{ type, id: noConflictId }],
+ statusCode,
+ response: async (response: TestResponse) => {
+ if (outcome === 'authorized') {
+ const { success, successCount, successResults, errors } = getResult(response);
+ expect(success).to.eql(true);
+ expect(successCount).to.eql(1);
+ const destinationId = successResults![0].destinationId;
+ expect(destinationId).to.match(v4);
+ expect(successResults).to.eql([{ type, id: noConflictId, destinationId }]);
+ expect(errors).to.be(undefined);
+ } else if (outcome === 'noAccess') {
+ expectNotFoundResponse(response);
+ } else {
+ // unauthorized read/write
+ expectForbiddenResponse(response);
+ }
+ },
+ },
+ {
+ testTitle: 'copying with an exact match conflict',
+ objects: [{ type, id: exactMatchId }],
+ statusCode,
+ response: async (response: TestResponse) => {
+ if (outcome === 'authorized') {
+ const { success, successCount, successResults, errors } = getResult(response);
+ if (overwrite) {
+ expect(success).to.eql(true);
+ expect(successCount).to.eql(1);
+ expect(successResults).to.eql([{ type, id: exactMatchId }]);
+ expect(errors).to.be(undefined);
+ } else {
+ expect(success).to.eql(false);
+ expect(successCount).to.eql(0);
+ expect(successResults).to.be(undefined);
+ expect(errors).to.eql([
+ {
+ error: { type: 'conflict' },
+ type,
+ id: exactMatchId,
+ title: 'A shared saved-object in the default, space_1, and space_2 spaces',
+ },
+ ]);
+ }
+ } else if (outcome === 'noAccess') {
+ expectNotFoundResponse(response);
+ } else {
+ // unauthorized read/write
+ expectForbiddenResponse(response);
+ }
+ },
+ },
+ {
+ testTitle: 'copying with an inexact match conflict',
+ objects: [{ type, id: inexactMatchId }],
+ statusCode,
+ response: async (response: TestResponse) => {
+ if (outcome === 'authorized') {
+ const { success, successCount, successResults, errors } = getResult(response);
+ const destinationId = 'conflict_1_space_2';
+ if (overwrite) {
+ expect(success).to.eql(true);
+ expect(successCount).to.eql(1);
+ expect(successResults).to.eql([{ type, id: inexactMatchId, destinationId }]);
+ expect(errors).to.be(undefined);
+ } else {
+ expect(success).to.eql(false);
+ expect(successCount).to.eql(0);
+ expect(successResults).to.be(undefined);
+ expect(errors).to.eql([
+ {
+ error: { type: 'conflict', destinationId },
+ type,
+ id: inexactMatchId,
+ title: 'A shared saved-object in one space',
+ },
+ ]);
+ }
+ } else if (outcome === 'noAccess') {
+ expectNotFoundResponse(response);
+ } else {
+ // unauthorized read/write
+ expectForbiddenResponse(response);
+ }
+ },
+ },
+ {
+ testTitle: 'copying with an ambiguous conflict',
+ objects: [{ type, id: ambiguousConflictId }],
+ statusCode,
+ response: async (response: TestResponse) => {
+ if (outcome === 'authorized') {
+ const { success, successCount, successResults, errors } = getResult(response);
+ const updatedAt = '2017-09-21T18:59:16.270Z';
+ const destinations = [
+ // response should be sorted by ID in ascending order
+ { id: 'conflict_2_all', title: 'A shared saved-object in all spaces', updatedAt },
+ { id: 'conflict_2_space_2', title: 'A shared saved-object in one space', updatedAt },
+ ];
+ expect(success).to.eql(false);
+ expect(successCount).to.eql(0);
+ expect(successResults).to.be(undefined);
+ expect(errors).to.eql([
+ {
+ error: { type: 'ambiguous_conflict', destinations },
+ type,
+ id: ambiguousConflictId,
+ title: 'A shared saved-object in one space',
+ },
+ ]);
+ } else if (outcome === 'noAccess') {
+ expectNotFoundResponse(response);
+ } else {
+ // unauthorized read/write
+ expectForbiddenResponse(response);
+ }
+ },
+ },
+ ];
};
const makeCopyToSpaceTest = (describeFn: DescribeFn) => (
@@ -363,162 +493,153 @@ export function copyToSpaceTestSuiteFactory(
expect(['default', 'space_1']).to.contain(spaceId);
});
- beforeEach(() => esArchiver.load('saved_objects/spaces'));
- afterEach(() => esArchiver.unload('saved_objects/spaces'));
-
- it(`should return ${tests.noConflictsWithoutReferences.statusCode} when copying to space without conflicts or references`, async () => {
- const destination = getDestinationWithoutConflicts();
-
- await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
-
- return supertest
- .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
- .auth(user.username, user.password)
- .send({
- objects: [
- {
- type: 'dashboard',
- id: 'cts_dashboard',
- },
- ],
- spaces: [destination],
- includeReferences: false,
- overwrite: false,
- })
- .expect(tests.noConflictsWithoutReferences.statusCode)
- .then(tests.noConflictsWithoutReferences.response);
- });
-
- it(`should return ${tests.noConflictsWithReferences.statusCode} when copying to space without conflicts with references`, async () => {
- const destination = getDestinationWithoutConflicts();
-
- await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
-
- return supertest
- .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
- .auth(user.username, user.password)
- .send({
- objects: [
- {
- type: 'dashboard',
- id: 'cts_dashboard',
- },
- ],
- spaces: [destination],
- includeReferences: true,
- overwrite: false,
- })
- .expect(tests.noConflictsWithReferences.statusCode)
- .then(tests.noConflictsWithReferences.response);
- });
-
- it(`should return ${tests.withConflictsOverwriting.statusCode} when copying to space with conflicts when overwriting`, async () => {
- const destination = getDestinationWithConflicts(spaceId);
-
- await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
-
- return supertest
- .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
- .auth(user.username, user.password)
- .send({
- objects: [
- {
- type: 'dashboard',
- id: 'cts_dashboard',
- },
- ],
- spaces: [destination],
- includeReferences: true,
- overwrite: true,
- })
- .expect(tests.withConflictsOverwriting.statusCode)
- .then(tests.withConflictsOverwriting.response);
- });
-
- it(`should return ${tests.withConflictsWithoutOverwriting.statusCode} when copying to space with conflicts without overwriting`, async () => {
- const destination = getDestinationWithConflicts(spaceId);
-
- await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
-
- return supertest
- .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
- .auth(user.username, user.password)
- .send({
- objects: [
- {
- type: 'dashboard',
- id: 'cts_dashboard',
- },
- ],
- spaces: [destination],
- includeReferences: true,
- overwrite: false,
- })
- .expect(tests.withConflictsWithoutOverwriting.statusCode)
- .then(tests.withConflictsWithoutOverwriting.response);
- });
-
- it(`should return ${tests.multipleSpaces.statusCode} when copying to multiple spaces`, async () => {
- const conflictDestination = getDestinationWithConflicts(spaceId);
- const noConflictDestination = getDestinationWithoutConflicts();
-
- return supertest
- .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
- .auth(user.username, user.password)
- .send({
- objects: [
- {
- type: 'dashboard',
- id: 'cts_dashboard',
- },
- ],
- spaces: [conflictDestination, noConflictDestination],
- includeReferences: true,
- overwrite: true,
- })
- .expect(tests.multipleSpaces.statusCode)
- .then((response: TestResponse) => {
- if (tests.multipleSpaces.statusCode === 200) {
- expect(Object.keys(response.body).length).to.eql(2);
+ describe('single-namespace types', () => {
+ beforeEach(() => esArchiver.load('saved_objects/spaces'));
+ afterEach(() => esArchiver.unload('saved_objects/spaces'));
+
+ const dashboardObject = { type: 'dashboard', id: 'cts_dashboard' };
+
+ it(`should return ${tests.noConflictsWithoutReferences.statusCode} when copying to space without conflicts or references`, async () => {
+ const destination = getDestinationWithoutConflicts();
+
+ await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
+
+ return supertest
+ .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
+ .auth(user.username, user.password)
+ .send({
+ objects: [dashboardObject],
+ spaces: [destination],
+ includeReferences: false,
+ overwrite: false,
+ })
+ .expect(tests.noConflictsWithoutReferences.statusCode)
+ .then(tests.noConflictsWithoutReferences.response);
+ });
+
+ it(`should return ${tests.noConflictsWithReferences.statusCode} when copying to space without conflicts with references`, async () => {
+ const destination = getDestinationWithoutConflicts();
+
+ await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
+
+ return supertest
+ .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
+ .auth(user.username, user.password)
+ .send({
+ objects: [dashboardObject],
+ spaces: [destination],
+ includeReferences: true,
+ overwrite: false,
+ })
+ .expect(tests.noConflictsWithReferences.statusCode)
+ .then(tests.noConflictsWithReferences.response);
+ });
+
+ it(`should return ${tests.withConflictsOverwriting.statusCode} when copying to space with conflicts when overwriting`, async () => {
+ const destination = getDestinationWithConflicts(spaceId);
+
+ await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
+
+ return supertest
+ .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
+ .auth(user.username, user.password)
+ .send({
+ objects: [dashboardObject],
+ spaces: [destination],
+ includeReferences: true,
+ overwrite: true,
+ })
+ .expect(tests.withConflictsOverwriting.statusCode)
+ .then(tests.withConflictsOverwriting.response);
+ });
+
+ it(`should return ${tests.withConflictsWithoutOverwriting.statusCode} when copying to space with conflicts without overwriting`, async () => {
+ const destination = getDestinationWithConflicts(spaceId);
+
+ await assertSpaceCounts(destination, INITIAL_COUNTS[destination]);
+
+ return supertest
+ .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
+ .auth(user.username, user.password)
+ .send({
+ objects: [dashboardObject],
+ spaces: [destination],
+ includeReferences: true,
+ overwrite: false,
+ })
+ .expect(tests.withConflictsWithoutOverwriting.statusCode)
+ .then(tests.withConflictsWithoutOverwriting.response);
+ });
+
+ it(`should return ${tests.multipleSpaces.statusCode} when copying to multiple spaces`, async () => {
+ const conflictDestination = getDestinationWithConflicts(spaceId);
+ const noConflictDestination = getDestinationWithoutConflicts();
+
+ return supertest
+ .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
+ .auth(user.username, user.password)
+ .send({
+ objects: [dashboardObject],
+ spaces: [conflictDestination, noConflictDestination],
+ includeReferences: true,
+ overwrite: true,
+ })
+ .expect(tests.multipleSpaces.statusCode)
+ .then((response: TestResponse) => {
+ if (tests.multipleSpaces.statusCode === 200) {
+ expect(Object.keys(response.body).length).to.eql(2);
+ return Promise.all([
+ tests.multipleSpaces.noConflictsResponse({
+ body: { [noConflictDestination]: response.body[noConflictDestination] },
+ }),
+ tests.multipleSpaces.withConflictsResponse({
+ body: { [conflictDestination]: response.body[conflictDestination] },
+ }),
+ ]);
+ }
+
+ // non-200 status codes will not have a response body broken out by space id, like above.
return Promise.all([
- tests.multipleSpaces.noConflictsResponse({
- body: {
- [noConflictDestination]: response.body[noConflictDestination],
- },
- }),
- tests.multipleSpaces.withConflictsResponse({
- body: {
- [conflictDestination]: response.body[conflictDestination],
- },
- }),
+ tests.multipleSpaces.noConflictsResponse(response),
+ tests.multipleSpaces.withConflictsResponse(response),
]);
- }
-
- // non-200 status codes will not have a response body broken out by space id, like above.
- return Promise.all([
- tests.multipleSpaces.noConflictsResponse(response),
- tests.multipleSpaces.withConflictsResponse(response),
- ]);
- });
+ });
+ });
+
+ it(`should return ${tests.nonExistentSpace.statusCode} when copying to non-existent space`, async () => {
+ return supertest
+ .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
+ .auth(user.username, user.password)
+ .send({
+ objects: [dashboardObject],
+ spaces: ['non_existent_space'],
+ includeReferences: false,
+ overwrite: true,
+ })
+ .expect(tests.nonExistentSpace.statusCode)
+ .then(tests.nonExistentSpace.response);
+ });
});
- it(`should return ${tests.nonExistentSpace.statusCode} when copying to non-existent space`, async () => {
- return supertest
- .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
- .auth(user.username, user.password)
- .send({
- objects: [
- {
- type: 'dashboard',
- id: 'cts_dashboard',
- },
- ],
- spaces: ['non_existent_space'],
- includeReferences: false,
- overwrite: true,
- })
- .expect(tests.nonExistentSpace.statusCode)
- .then(tests.nonExistentSpace.response);
+ [false, true].forEach((overwrite) => {
+ const spaces = ['space_2'];
+ const includeReferences = false;
+ describe(`multi-namespace types with overwrite=${overwrite}`, () => {
+ before(() => esArchiver.load('saved_objects/spaces'));
+ after(() => esArchiver.unload('saved_objects/spaces'));
+
+ const testCases = tests.multiNamespaceTestCases(overwrite);
+ testCases.forEach(({ testTitle, objects, statusCode, response }) => {
+ it(`should return ${statusCode} when ${testTitle}`, async () => {
+ return supertest
+ .post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
+ .auth(user.username, user.password)
+ .send({ objects, spaces, includeReferences, overwrite })
+ .expect(statusCode)
+ .then(response);
+ });
+ });
+ });
});
});
};
@@ -534,10 +655,10 @@ export function copyToSpaceTestSuiteFactory(
expectNoConflictsForNonExistentSpaceResult,
createExpectWithConflictsOverwritingResult,
createExpectWithConflictsWithoutOverwritingResult,
- expectRbacForbiddenResponse,
expectNotFoundResponse,
createExpectUnauthorizedAtSpaceWithReferencesResult,
createExpectUnauthorizedAtSpaceWithoutReferencesResult,
+ createMultiNamespaceTestCases,
originSpaces: ['default', 'space_1'],
};
}
diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts
index 15a90092f5517..69b5697d8a9a8 100644
--- a/x-pack/test/spaces_api_integration/common/suites/delete.ts
+++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts
@@ -130,7 +130,7 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
expect(buckets).to.eql(expectedBuckets);
- // There were seven multi-namespace objects.
+ // There were eleven multi-namespace objects.
// Since Space 2 was deleted, any multi-namespace objects that existed in that space
// are updated to remove it, and of those, any that don't exist in any space are deleted.
const multiNamespaceResponse = await es.search({
@@ -138,16 +138,13 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
body: { query: { terms: { type: ['sharedtype'] } } },
});
const docs: [Record] = multiNamespaceResponse.hits.hits;
- expect(docs).length(6); // just six results, since spaces_2_only got deleted
- Object.values(CASES).forEach(({ id, existingNamespaces }) => {
- const remainingNamespaces = existingNamespaces.filter((x) => x !== 'space_2');
- const doc = docs.find((x) => x._id === `sharedtype:${id}`);
- if (remainingNamespaces.length > 0) {
- expect(doc?._source?.namespaces).to.eql(remainingNamespaces);
- } else {
- expect(doc).to.be(undefined);
- }
+ expect(docs).length(10); // just ten results, since spaces_2_only got deleted
+ docs.forEach((doc) => () => {
+ const containsSpace2 = doc?._source?.namespaces.includes('space_2');
+ expect(containsSpace2).to.eql(false);
});
+ const space2OnlyObjExists = docs.some((x) => x._id === CASES.SPACE_2_ONLY);
+ expect(space2OnlyObjExists).to.eql(false);
};
const expectNotFound = (resp: { [key: string]: any }) => {
diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
index 3529d8f3ae9c9..3dab8a4e7f244 100644
--- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
+++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
@@ -20,12 +20,19 @@ interface ResolveCopyToSpaceTest {
response: (resp: TestResponse) => Promise;
}
+interface ResolveCopyToSpaceMultiNamespaceTest extends ResolveCopyToSpaceTest {
+ testTitle: string;
+ objects: Array>;
+ retries: Record;
+}
+
interface ResolveCopyToSpaceTests {
withReferencesNotOverwriting: ResolveCopyToSpaceTest;
withReferencesOverwriting: ResolveCopyToSpaceTest;
withoutReferencesOverwriting: ResolveCopyToSpaceTest;
withoutReferencesNotOverwriting: ResolveCopyToSpaceTest;
nonExistentSpace: ResolveCopyToSpaceTest;
+ multiNamespaceTestCases: () => ResolveCopyToSpaceMultiNamespaceTest[];
}
interface ResolveCopyToSpaceTestDefinition {
@@ -76,6 +83,7 @@ export function resolveCopyToSpaceConflictsSuite(
[destination]: {
success: true,
successCount: 1,
+ successResults: [{ id: 'cts_vis_3', type: 'visualization' }],
},
});
const [dashboard, visualization] = await getObjectsAtSpace(destination);
@@ -94,6 +102,7 @@ export function resolveCopyToSpaceConflictsSuite(
[destinationSpaceId]: {
success: true,
successCount: 1,
+ successResults: [{ id: 'cts_dashboard', type: 'dashboard' }],
},
});
const [dashboard, visualization] = await getObjectsAtSpace(destinationSpaceId);
@@ -119,9 +128,7 @@ export function resolveCopyToSpaceConflictsSuite(
successCount: 0,
errors: [
{
- error: {
- type: 'conflict',
- },
+ error: { type: 'conflict' },
id: 'cts_vis_3',
title: `CTS vis 3 from ${sourceSpaceId} space`,
type: 'visualization',
@@ -149,9 +156,7 @@ export function resolveCopyToSpaceConflictsSuite(
successCount: 0,
errors: [
{
- error: {
- type: 'conflict',
- },
+ error: { type: 'conflict' },
id: 'cts_dashboard',
title: `This is the ${sourceSpaceId} test space CTS dashboard`,
type: 'dashboard',
@@ -264,6 +269,106 @@ export function resolveCopyToSpaceConflictsSuite(
}
};
+ /**
+ * Creates test cases for multi-namespace saved object types.
+ * Note: these are written with the assumption that test data will only be reloaded between each group of test cases, *not* before every
+ * single test case. This saves time during test execution.
+ */
+ const createMultiNamespaceTestCases = (
+ spaceId: string,
+ outcome: 'authorized' | 'unauthorizedRead' | 'unauthorizedWrite' | 'noAccess' = 'authorized'
+ ) => (): ResolveCopyToSpaceMultiNamespaceTest[] => {
+ // the status code of the HTTP response differs depending on the error type
+ // a 403 error actually comes back as an HTTP 200 response
+ const statusCode = outcome === 'noAccess' ? 404 : 200;
+ const type = 'sharedtype';
+ const exactMatchId = 'all_spaces';
+ const inexactMatchId = `conflict_1_${spaceId}`;
+ const ambiguousConflictId = `conflict_2_${spaceId}`;
+
+ const createRetries = (overwriteRetry: Record) => ({ space_2: [overwriteRetry] });
+ const getResult = (response: TestResponse) => (response.body as CopyResponse).space_2;
+ const expectForbiddenResponse = (response: TestResponse) => {
+ expect(response.body).to.eql({
+ space_2: {
+ success: false,
+ successCount: 0,
+ errors: [
+ { statusCode: 403, error: 'Forbidden', message: `Unable to bulk_create sharedtype` },
+ ],
+ },
+ });
+ };
+ const expectSuccessResponse = (response: TestResponse, id: string, destinationId?: string) => {
+ const { success, successCount, successResults, errors } = getResult(response);
+ expect(success).to.eql(true);
+ expect(successCount).to.eql(1);
+ expect(errors).to.be(undefined);
+ expect(successResults).to.eql([{ type, id, ...(destinationId && { destinationId }) }]);
+ };
+
+ return [
+ {
+ testTitle: 'copying with an exact match conflict',
+ objects: [{ type, id: exactMatchId }],
+ retries: createRetries({ type, id: exactMatchId, overwrite: true }),
+ statusCode,
+ response: async (response: TestResponse) => {
+ if (outcome === 'authorized') {
+ expectSuccessResponse(response, exactMatchId);
+ } else if (outcome === 'noAccess') {
+ expectNotFoundResponse(response);
+ } else {
+ // unauthorized read/write
+ expectForbiddenResponse(response);
+ }
+ },
+ },
+ {
+ testTitle: 'copying with an inexact match conflict',
+ objects: [{ type, id: inexactMatchId }],
+ retries: createRetries({
+ type,
+ id: inexactMatchId,
+ overwrite: true,
+ destinationId: 'conflict_1_space_2',
+ }),
+ statusCode,
+ response: async (response: TestResponse) => {
+ if (outcome === 'authorized') {
+ expectSuccessResponse(response, inexactMatchId, 'conflict_1_space_2');
+ } else if (outcome === 'noAccess') {
+ expectNotFoundResponse(response);
+ } else {
+ // unauthorized read/write
+ expectForbiddenResponse(response);
+ }
+ },
+ },
+ {
+ testTitle: 'copying with an ambiguous conflict',
+ objects: [{ type, id: ambiguousConflictId }],
+ retries: createRetries({
+ type,
+ id: ambiguousConflictId,
+ overwrite: true,
+ destinationId: 'conflict_2_space_2',
+ }),
+ statusCode,
+ response: async (response: TestResponse) => {
+ if (outcome === 'authorized') {
+ expectSuccessResponse(response, ambiguousConflictId, 'conflict_2_space_2');
+ } else if (outcome === 'noAccess') {
+ expectNotFoundResponse(response);
+ } else {
+ // unauthorized read/write
+ expectForbiddenResponse(response);
+ }
+ },
+ },
+ ];
+ };
+
const makeResolveCopyToSpaceConflictsTest = (describeFn: DescribeFn) => (
description: string,
{ user = {}, spaceId = DEFAULT_SPACE_ID, tests }: ResolveCopyToSpaceTestDefinition
@@ -274,147 +379,105 @@ export function resolveCopyToSpaceConflictsSuite(
expect(['default', 'space_1']).to.contain(spaceId);
});
- beforeEach(() => esArchiver.load('saved_objects/spaces'));
- afterEach(() => esArchiver.unload('saved_objects/spaces'));
-
- it(`should return ${tests.withReferencesNotOverwriting.statusCode} when not overwriting, with references`, async () => {
- const destination = getDestinationSpace(spaceId);
-
- return supertestWithoutAuth
- .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
- .auth(user.username, user.password)
- .send({
- objects: [
- {
- type: 'dashboard',
- id: 'cts_dashboard',
- },
- ],
- includeReferences: true,
- retries: {
- [destination]: [
- {
- type: 'visualization',
- id: 'cts_vis_3',
- overwrite: false,
- },
- ],
- },
- })
- .expect(tests.withReferencesNotOverwriting.statusCode)
- .then(tests.withReferencesNotOverwriting.response);
- });
-
- it(`should return ${tests.withReferencesOverwriting.statusCode} when overwriting, with references`, async () => {
- const destination = getDestinationSpace(spaceId);
-
- return supertestWithoutAuth
- .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
- .auth(user.username, user.password)
- .send({
- objects: [
- {
- type: 'dashboard',
- id: 'cts_dashboard',
- },
- ],
- includeReferences: true,
- retries: {
- [destination]: [
- {
- type: 'visualization',
- id: 'cts_vis_3',
- overwrite: true,
- },
- ],
- },
- })
- .expect(tests.withReferencesOverwriting.statusCode)
- .then(tests.withReferencesOverwriting.response);
- });
-
- it(`should return ${tests.withoutReferencesOverwriting.statusCode} when overwriting, without references`, async () => {
- const destination = getDestinationSpace(spaceId);
-
- return supertestWithoutAuth
- .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
- .auth(user.username, user.password)
- .send({
- objects: [
- {
- type: 'dashboard',
- id: 'cts_dashboard',
- },
- ],
- includeReferences: false,
- retries: {
- [destination]: [
- {
- type: 'dashboard',
- id: 'cts_dashboard',
- overwrite: true,
- },
- ],
- },
- })
- .expect(tests.withoutReferencesOverwriting.statusCode)
- .then(tests.withoutReferencesOverwriting.response);
- });
-
- it(`should return ${tests.withoutReferencesNotOverwriting.statusCode} when not overwriting, without references`, async () => {
- const destination = getDestinationSpace(spaceId);
-
- return supertestWithoutAuth
- .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
- .auth(user.username, user.password)
- .send({
- objects: [
- {
- type: 'dashboard',
- id: 'cts_dashboard',
- },
- ],
- includeReferences: false,
- retries: {
- [destination]: [
- {
- type: 'dashboard',
- id: 'cts_dashboard',
- overwrite: false,
- },
- ],
- },
- })
- .expect(tests.withoutReferencesNotOverwriting.statusCode)
- .then(tests.withoutReferencesNotOverwriting.response);
+ describe('single-namespace types', () => {
+ beforeEach(() => esArchiver.load('saved_objects/spaces'));
+ afterEach(() => esArchiver.unload('saved_objects/spaces'));
+
+ const dashboardObject = { type: 'dashboard', id: 'cts_dashboard' };
+ const visualizationObject = { type: 'visualization', id: 'cts_vis_3' };
+
+ it(`should return ${tests.withReferencesNotOverwriting.statusCode} when not overwriting, with references`, async () => {
+ const destination = getDestinationSpace(spaceId);
+
+ return supertestWithoutAuth
+ .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
+ .auth(user.username, user.password)
+ .send({
+ objects: [dashboardObject],
+ includeReferences: true,
+ retries: { [destination]: [{ ...visualizationObject, overwrite: false }] },
+ })
+ .expect(tests.withReferencesNotOverwriting.statusCode)
+ .then(tests.withReferencesNotOverwriting.response);
+ });
+
+ it(`should return ${tests.withReferencesOverwriting.statusCode} when overwriting, with references`, async () => {
+ const destination = getDestinationSpace(spaceId);
+
+ return supertestWithoutAuth
+ .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
+ .auth(user.username, user.password)
+ .send({
+ objects: [dashboardObject],
+ includeReferences: true,
+ retries: { [destination]: [{ ...visualizationObject, overwrite: true }] },
+ })
+ .expect(tests.withReferencesOverwriting.statusCode)
+ .then(tests.withReferencesOverwriting.response);
+ });
+
+ it(`should return ${tests.withoutReferencesOverwriting.statusCode} when overwriting, without references`, async () => {
+ const destination = getDestinationSpace(spaceId);
+
+ return supertestWithoutAuth
+ .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
+ .auth(user.username, user.password)
+ .send({
+ objects: [dashboardObject],
+ includeReferences: false,
+ retries: { [destination]: [{ ...dashboardObject, overwrite: true }] },
+ })
+ .expect(tests.withoutReferencesOverwriting.statusCode)
+ .then(tests.withoutReferencesOverwriting.response);
+ });
+
+ it(`should return ${tests.withoutReferencesNotOverwriting.statusCode} when not overwriting, without references`, async () => {
+ const destination = getDestinationSpace(spaceId);
+
+ return supertestWithoutAuth
+ .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
+ .auth(user.username, user.password)
+ .send({
+ objects: [dashboardObject],
+ includeReferences: false,
+ retries: { [destination]: [{ ...dashboardObject, overwrite: false }] },
+ })
+ .expect(tests.withoutReferencesNotOverwriting.statusCode)
+ .then(tests.withoutReferencesNotOverwriting.response);
+ });
+
+ it(`should return ${tests.nonExistentSpace.statusCode} when resolving within a non-existent space`, async () => {
+ const destination = NON_EXISTENT_SPACE_ID;
+
+ return supertestWithoutAuth
+ .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
+ .auth(user.username, user.password)
+ .send({
+ objects: [dashboardObject],
+ includeReferences: false,
+ retries: { [destination]: [{ ...dashboardObject, overwrite: true }] },
+ })
+ .expect(tests.nonExistentSpace.statusCode)
+ .then(tests.nonExistentSpace.response);
+ });
});
- it(`should return ${tests.nonExistentSpace.statusCode} when resolving within a non-existent space`, async () => {
- const destination = NON_EXISTENT_SPACE_ID;
-
- return supertestWithoutAuth
- .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
- .auth(user.username, user.password)
- .send({
- objects: [
- {
- type: 'dashboard',
- id: 'cts_dashboard',
- },
- ],
- includeReferences: false,
- retries: {
- [destination]: [
- {
- type: 'dashboard',
- id: 'cts_dashboard',
- overwrite: true,
- },
- ],
- },
- })
- .expect(tests.nonExistentSpace.statusCode)
- .then(tests.nonExistentSpace.response);
+ const includeReferences = false;
+ describe(`multi-namespace types with "overwrite" retry`, () => {
+ before(() => esArchiver.load('saved_objects/spaces'));
+ after(() => esArchiver.unload('saved_objects/spaces'));
+
+ const testCases = tests.multiNamespaceTestCases();
+ testCases.forEach(({ testTitle, objects, retries, statusCode, response }) => {
+ it(`should return ${statusCode} when ${testTitle}`, async () => {
+ return supertestWithoutAuth
+ .post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
+ .auth(user.username, user.password)
+ .send({ objects, includeReferences, retries })
+ .expect(statusCode)
+ .then(response);
+ });
+ });
});
});
};
@@ -433,6 +496,7 @@ export function resolveCopyToSpaceConflictsSuite(
createExpectUnauthorizedAtSpaceWithReferencesResult,
createExpectReadonlyAtSpaceWithReferencesResult,
createExpectUnauthorizedAtSpaceWithoutReferencesResult,
+ createMultiNamespaceTestCases,
originSpaces: ['default', 'space_1'],
NON_EXISTENT_SPACE_ID,
};
diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts
index 08450f48567c8..2c4fc6d38d79d 100644
--- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts
+++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts
@@ -25,6 +25,7 @@ export default function copyToSpaceSpacesAndSecuritySuite({ getService }: TestIn
createExpectUnauthorizedAtSpaceWithReferencesResult,
createExpectUnauthorizedAtSpaceWithoutReferencesResult,
expectNotFoundResponse,
+ createMultiNamespaceTestCases,
} = copyToSpaceTestSuiteFactory(es, esArchiver, supertestWithoutAuth);
describe('copy to spaces', () => {
@@ -55,325 +56,148 @@ export default function copyToSpaceSpacesAndSecuritySuite({ getService }: TestIn
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
},
},
- ].forEach((scenario) => {
- copyToSpaceTest(`user with no access from the ${scenario.spaceId} space`, {
- spaceId: scenario.spaceId,
- user: scenario.users.noAccess,
+ ].forEach(({ spaceId, ...scenario }) => {
+ const definitionNoAccess = (user: { username: string; password: string }) => ({
+ spaceId,
+ user,
tests: {
- noConflictsWithoutReferences: {
- statusCode: 404,
- response: expectNotFoundResponse,
- },
- noConflictsWithReferences: {
- statusCode: 404,
- response: expectNotFoundResponse,
- },
- withConflictsOverwriting: {
- statusCode: 404,
- response: expectNotFoundResponse,
- },
- withConflictsWithoutOverwriting: {
- statusCode: 404,
- response: expectNotFoundResponse,
- },
+ noConflictsWithoutReferences: { statusCode: 404, response: expectNotFoundResponse },
+ noConflictsWithReferences: { statusCode: 404, response: expectNotFoundResponse },
+ withConflictsOverwriting: { statusCode: 404, response: expectNotFoundResponse },
+ withConflictsWithoutOverwriting: { statusCode: 404, response: expectNotFoundResponse },
multipleSpaces: {
statusCode: 404,
withConflictsResponse: expectNotFoundResponse,
noConflictsResponse: expectNotFoundResponse,
},
- nonExistentSpace: {
- statusCode: 404,
- response: expectNotFoundResponse,
- },
+ nonExistentSpace: { statusCode: 404, response: expectNotFoundResponse },
+ multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'noAccess'),
},
});
-
- copyToSpaceTest(`superuser from the ${scenario.spaceId} space`, {
- spaceId: scenario.spaceId,
- user: scenario.users.superuser,
- tests: {
- noConflictsWithoutReferences: {
- statusCode: 200,
- response: expectNoConflictsWithoutReferencesResult,
- },
- noConflictsWithReferences: {
- statusCode: 200,
- response: expectNoConflictsWithReferencesResult,
- },
- withConflictsOverwriting: {
- statusCode: 200,
- response: createExpectWithConflictsOverwritingResult(scenario.spaceId),
- },
- withConflictsWithoutOverwriting: {
- statusCode: 200,
- response: createExpectWithConflictsWithoutOverwritingResult(scenario.spaceId),
- },
- multipleSpaces: {
- statusCode: 200,
- withConflictsResponse: createExpectWithConflictsOverwritingResult(scenario.spaceId),
- noConflictsResponse: expectNoConflictsWithReferencesResult,
- },
- nonExistentSpace: {
- statusCode: 200,
- response: expectNoConflictsForNonExistentSpaceResult,
- },
+ // In *this* test suite, a user who is unauthorized to write (but authorized to read) in the destination space will get the same exact
+ // results as a user who is unauthorized to read in the destination space. However, that may not *always* be the case depending on the
+ // input that is submitted, due to the `validateReferences` check that can trigger a `bulkGet` for the destination space. See also the
+ // integration tests in `./resolve_copy_to_space_conflicts`, which behave differently.
+ const commonUnauthorizedTests = {
+ noConflictsWithoutReferences: {
+ statusCode: 200,
+ response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
+ spaceId,
+ 'without-conflicts'
+ ),
},
- });
-
- copyToSpaceTest(`rbac user with all globally from the ${scenario.spaceId} space`, {
- spaceId: scenario.spaceId,
- user: scenario.users.allGlobally,
- tests: {
- noConflictsWithoutReferences: {
- statusCode: 200,
- response: expectNoConflictsWithoutReferencesResult,
- },
- noConflictsWithReferences: {
- statusCode: 200,
- response: expectNoConflictsWithReferencesResult,
- },
- withConflictsOverwriting: {
- statusCode: 200,
- response: createExpectWithConflictsOverwritingResult(scenario.spaceId),
- },
- withConflictsWithoutOverwriting: {
- statusCode: 200,
- response: createExpectWithConflictsWithoutOverwritingResult(scenario.spaceId),
- },
- multipleSpaces: {
- statusCode: 200,
- withConflictsResponse: createExpectWithConflictsOverwritingResult(scenario.spaceId),
- noConflictsResponse: expectNoConflictsWithReferencesResult,
- },
- nonExistentSpace: {
- statusCode: 200,
- response: expectNoConflictsForNonExistentSpaceResult,
- },
+ noConflictsWithReferences: {
+ statusCode: 200,
+ response: createExpectUnauthorizedAtSpaceWithReferencesResult(
+ spaceId,
+ 'without-conflicts'
+ ),
},
- });
-
- copyToSpaceTest(`dual-privileges user from the ${scenario.spaceId} space`, {
- spaceId: scenario.spaceId,
- user: scenario.users.dualAll,
- tests: {
- noConflictsWithoutReferences: {
- statusCode: 200,
- response: expectNoConflictsWithoutReferencesResult,
- },
- noConflictsWithReferences: {
- statusCode: 200,
- response: expectNoConflictsWithReferencesResult,
- },
- withConflictsOverwriting: {
- statusCode: 200,
- response: createExpectWithConflictsOverwritingResult(scenario.spaceId),
- },
- withConflictsWithoutOverwriting: {
- statusCode: 200,
- response: createExpectWithConflictsWithoutOverwritingResult(scenario.spaceId),
- },
- multipleSpaces: {
- statusCode: 200,
- withConflictsResponse: createExpectWithConflictsOverwritingResult(scenario.spaceId),
- noConflictsResponse: expectNoConflictsWithReferencesResult,
- },
- nonExistentSpace: {
- statusCode: 200,
- response: expectNoConflictsForNonExistentSpaceResult,
- },
+ withConflictsOverwriting: {
+ statusCode: 200,
+ response: createExpectUnauthorizedAtSpaceWithReferencesResult(spaceId, 'with-conflicts'),
},
- });
-
- copyToSpaceTest(`legacy user from the ${scenario.spaceId} space`, {
- spaceId: scenario.spaceId,
- user: scenario.users.legacyAll,
- tests: {
- noConflictsWithoutReferences: {
- statusCode: 404,
- response: expectNotFoundResponse,
- },
- noConflictsWithReferences: {
- statusCode: 404,
- response: expectNotFoundResponse,
- },
- withConflictsOverwriting: {
- statusCode: 404,
- response: expectNotFoundResponse,
- },
- withConflictsWithoutOverwriting: {
- statusCode: 404,
- response: expectNotFoundResponse,
- },
- multipleSpaces: {
- statusCode: 404,
- withConflictsResponse: expectNotFoundResponse,
- noConflictsResponse: expectNotFoundResponse,
- },
- nonExistentSpace: {
- statusCode: 404,
- response: expectNotFoundResponse,
- },
+ withConflictsWithoutOverwriting: {
+ statusCode: 200,
+ response: createExpectUnauthorizedAtSpaceWithReferencesResult(spaceId, 'with-conflicts'),
},
- });
-
- copyToSpaceTest(`rbac user with read globally from the ${scenario.spaceId} space`, {
- spaceId: scenario.spaceId,
- user: scenario.users.readGlobally,
+ multipleSpaces: {
+ statusCode: 200,
+ withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult(
+ spaceId,
+ 'with-conflicts'
+ ),
+ noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult(
+ spaceId,
+ 'without-conflicts'
+ ),
+ },
+ nonExistentSpace: {
+ statusCode: 200,
+ response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId, 'non-existent'),
+ },
+ };
+ const definitionUnauthorizedRead = (user: { username: string; password: string }) => ({
+ spaceId,
+ user,
tests: {
- noConflictsWithoutReferences: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
- scenario.spaceId,
- 'without-conflicts'
- ),
- },
- noConflictsWithReferences: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'without-conflicts'
- ),
- },
- withConflictsOverwriting: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'with-conflicts'
- ),
- },
- withConflictsWithoutOverwriting: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'with-conflicts'
- ),
- },
- multipleSpaces: {
- statusCode: 200,
- withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'with-conflicts'
- ),
- noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'without-conflicts'
- ),
- },
- nonExistentSpace: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
- scenario.spaceId,
- 'non-existent'
- ),
- },
+ ...commonUnauthorizedTests,
+ multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'unauthorizedRead'),
},
});
-
- copyToSpaceTest(`dual-privileges readonly user from the ${scenario.spaceId} space`, {
- spaceId: scenario.spaceId,
- user: scenario.users.dualRead,
+ const definitionUnauthorizedWrite = (user: { username: string; password: string }) => ({
+ spaceId,
+ user,
tests: {
- noConflictsWithoutReferences: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
- scenario.spaceId,
- 'without-conflicts'
- ),
- },
- noConflictsWithReferences: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'without-conflicts'
- ),
- },
- withConflictsOverwriting: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'with-conflicts'
- ),
- },
- withConflictsWithoutOverwriting: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'with-conflicts'
- ),
- },
- multipleSpaces: {
- statusCode: 200,
- withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'with-conflicts'
- ),
- noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'without-conflicts'
- ),
- },
- nonExistentSpace: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
- scenario.spaceId,
- 'non-existent'
- ),
- },
+ ...commonUnauthorizedTests,
+ multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'unauthorizedWrite'),
},
});
-
- copyToSpaceTest(`rbac user with all at space from the ${scenario.spaceId} space`, {
- spaceId: scenario.spaceId,
- user: scenario.users.allAtSpace,
+ const definitionAuthorized = (user: { username: string; password: string }) => ({
+ spaceId,
+ user,
tests: {
noConflictsWithoutReferences: {
statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
- scenario.spaceId,
- 'without-conflicts'
- ),
+ response: expectNoConflictsWithoutReferencesResult,
},
noConflictsWithReferences: {
statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'without-conflicts'
- ),
+ response: expectNoConflictsWithReferencesResult(spaceId),
},
withConflictsOverwriting: {
statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'with-conflicts'
- ),
+ response: createExpectWithConflictsOverwritingResult(spaceId),
},
withConflictsWithoutOverwriting: {
statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'with-conflicts'
- ),
+ response: createExpectWithConflictsWithoutOverwritingResult(spaceId),
},
multipleSpaces: {
statusCode: 200,
- withConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'with-conflicts'
- ),
- noConflictsResponse: createExpectUnauthorizedAtSpaceWithReferencesResult(
- scenario.spaceId,
- 'without-conflicts'
- ),
+ withConflictsResponse: createExpectWithConflictsOverwritingResult(spaceId),
+ noConflictsResponse: expectNoConflictsWithReferencesResult(spaceId),
},
nonExistentSpace: {
statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
- scenario.spaceId,
- 'non-existent'
- ),
+ response: expectNoConflictsForNonExistentSpaceResult,
},
+ multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'authorized'),
},
});
+
+ copyToSpaceTest(
+ `user with no access from the ${spaceId} space`,
+ definitionNoAccess(scenario.users.noAccess)
+ );
+ copyToSpaceTest(
+ `superuser from the ${spaceId} space`,
+ definitionAuthorized(scenario.users.superuser)
+ );
+ copyToSpaceTest(
+ `rbac user with all globally from the ${spaceId} space`,
+ definitionAuthorized(scenario.users.allGlobally)
+ );
+ copyToSpaceTest(
+ `dual-privileges user from the ${spaceId} space`,
+ definitionAuthorized(scenario.users.dualAll)
+ );
+ copyToSpaceTest(
+ `legacy user from the ${spaceId} space`,
+ definitionNoAccess(scenario.users.legacyAll)
+ );
+ copyToSpaceTest(
+ `rbac user with read globally from the ${spaceId} space`,
+ definitionUnauthorizedWrite(scenario.users.readGlobally)
+ );
+ copyToSpaceTest(
+ `dual-privileges readonly user from the ${spaceId} space`,
+ definitionUnauthorizedWrite(scenario.users.dualRead)
+ );
+ copyToSpaceTest(
+ `rbac user with all at space from the ${spaceId} space`,
+ definitionUnauthorizedRead(scenario.users.allAtSpace)
+ );
});
});
}
diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts
index 472ec1a927126..b81f2965eba22 100644
--- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts
+++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts
@@ -25,6 +25,7 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Tes
createExpectUnauthorizedAtSpaceWithReferencesResult,
createExpectReadonlyAtSpaceWithReferencesResult,
createExpectUnauthorizedAtSpaceWithoutReferencesResult,
+ createMultiNamespaceTestCases,
NON_EXISTENT_SPACE_ID,
} = resolveCopyToSpaceConflictsSuite(esArchiver, supertestWithAuth, supertestWithoutAuth);
@@ -56,10 +57,10 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Tes
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
},
},
- ].forEach((scenario) => {
- resolveCopyToSpaceConflictsTest(`user with no access from the ${scenario.spaceId} space`, {
- spaceId: scenario.spaceId,
- user: scenario.users.noAccess,
+ ].forEach(({ spaceId, ...scenario }) => {
+ const definitionNoAccess = (user: { username: string; password: string }) => ({
+ spaceId,
+ user,
tests: {
withReferencesNotOverwriting: {
statusCode: 404,
@@ -81,226 +82,131 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Tes
statusCode: 404,
response: expectNotFoundResponse,
},
+ multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'noAccess'),
},
});
-
- resolveCopyToSpaceConflictsTest(`superuser from the ${scenario.spaceId} space`, {
- spaceId: scenario.spaceId,
- user: scenario.users.superuser,
+ const definitionUnauthorizedRead = (user: { username: string; password: string }) => ({
+ spaceId,
+ user,
tests: {
withReferencesNotOverwriting: {
statusCode: 200,
- response: createExpectNonOverriddenResponseWithReferences(scenario.spaceId),
+ response: createExpectUnauthorizedAtSpaceWithReferencesResult(spaceId),
},
withReferencesOverwriting: {
statusCode: 200,
- response: createExpectOverriddenResponseWithReferences(scenario.spaceId),
+ response: createExpectUnauthorizedAtSpaceWithReferencesResult(spaceId),
},
withoutReferencesOverwriting: {
statusCode: 200,
- response: createExpectOverriddenResponseWithoutReferences(scenario.spaceId),
+ response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId),
},
withoutReferencesNotOverwriting: {
statusCode: 200,
- response: createExpectNonOverriddenResponseWithoutReferences(scenario.spaceId),
+ response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId),
},
nonExistentSpace: {
statusCode: 200,
- response: createExpectOverriddenResponseWithoutReferences(
- scenario.spaceId,
+ response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
+ spaceId,
NON_EXISTENT_SPACE_ID
),
},
+ multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'unauthorizedRead'),
},
});
-
- resolveCopyToSpaceConflictsTest(
- `rbac user with all globally from the ${scenario.spaceId} space`,
- {
- spaceId: scenario.spaceId,
- user: scenario.users.allGlobally,
- tests: {
- withReferencesNotOverwriting: {
- statusCode: 200,
- response: createExpectNonOverriddenResponseWithReferences(scenario.spaceId),
- },
- withReferencesOverwriting: {
- statusCode: 200,
- response: createExpectOverriddenResponseWithReferences(scenario.spaceId),
- },
- withoutReferencesOverwriting: {
- statusCode: 200,
- response: createExpectOverriddenResponseWithoutReferences(scenario.spaceId),
- },
- withoutReferencesNotOverwriting: {
- statusCode: 200,
- response: createExpectNonOverriddenResponseWithoutReferences(scenario.spaceId),
- },
- nonExistentSpace: {
- statusCode: 200,
- response: createExpectOverriddenResponseWithoutReferences(
- scenario.spaceId,
- NON_EXISTENT_SPACE_ID
- ),
- },
- },
- }
- );
-
- resolveCopyToSpaceConflictsTest(`dual-privileges user from the ${scenario.spaceId} space`, {
- spaceId: scenario.spaceId,
- user: scenario.users.dualAll,
+ const definitionUnauthorizedWrite = (user: { username: string; password: string }) => ({
+ spaceId,
+ user,
tests: {
withReferencesNotOverwriting: {
statusCode: 200,
- response: createExpectNonOverriddenResponseWithReferences(scenario.spaceId),
+ response: createExpectReadonlyAtSpaceWithReferencesResult(spaceId),
},
withReferencesOverwriting: {
statusCode: 200,
- response: createExpectOverriddenResponseWithReferences(scenario.spaceId),
+ response: createExpectReadonlyAtSpaceWithReferencesResult(spaceId),
},
withoutReferencesOverwriting: {
statusCode: 200,
- response: createExpectOverriddenResponseWithoutReferences(scenario.spaceId),
+ response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId),
},
withoutReferencesNotOverwriting: {
statusCode: 200,
- response: createExpectNonOverriddenResponseWithoutReferences(scenario.spaceId),
+ response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(spaceId),
},
nonExistentSpace: {
statusCode: 200,
- response: createExpectOverriddenResponseWithoutReferences(
- scenario.spaceId,
+ response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
+ spaceId,
NON_EXISTENT_SPACE_ID
),
},
+ multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'unauthorizedWrite'),
},
});
-
- resolveCopyToSpaceConflictsTest(`legacy user from the ${scenario.spaceId} space`, {
- spaceId: scenario.spaceId,
- user: scenario.users.legacyAll,
+ const definitionAuthorized = (user: { username: string; password: string }) => ({
+ spaceId,
+ user,
tests: {
withReferencesNotOverwriting: {
- statusCode: 404,
- response: expectNotFoundResponse,
+ statusCode: 200,
+ response: createExpectNonOverriddenResponseWithReferences(spaceId),
},
withReferencesOverwriting: {
- statusCode: 404,
- response: expectNotFoundResponse,
+ statusCode: 200,
+ response: createExpectOverriddenResponseWithReferences(spaceId),
},
withoutReferencesOverwriting: {
- statusCode: 404,
- response: expectNotFoundResponse,
+ statusCode: 200,
+ response: createExpectOverriddenResponseWithoutReferences(spaceId),
},
withoutReferencesNotOverwriting: {
- statusCode: 404,
- response: expectNotFoundResponse,
+ statusCode: 200,
+ response: createExpectNonOverriddenResponseWithoutReferences(spaceId),
},
nonExistentSpace: {
- statusCode: 404,
- response: expectNotFoundResponse,
+ statusCode: 200,
+ response: createExpectOverriddenResponseWithoutReferences(
+ spaceId,
+ NON_EXISTENT_SPACE_ID
+ ),
},
+ multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId, 'authorized'),
},
});
resolveCopyToSpaceConflictsTest(
- `rbac user with read globally from the ${scenario.spaceId} space`,
- {
- spaceId: scenario.spaceId,
- user: scenario.users.readGlobally,
- tests: {
- withReferencesNotOverwriting: {
- statusCode: 200,
- response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId),
- },
- withReferencesOverwriting: {
- statusCode: 200,
- response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId),
- },
- withoutReferencesOverwriting: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId),
- },
- withoutReferencesNotOverwriting: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId),
- },
- nonExistentSpace: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
- scenario.spaceId,
- NON_EXISTENT_SPACE_ID
- ),
- },
- },
- }
+ `user with no access from the ${spaceId} space`,
+ definitionNoAccess(scenario.users.noAccess)
);
-
resolveCopyToSpaceConflictsTest(
- `dual-privileges readonly user from the ${scenario.spaceId} space`,
- {
- spaceId: scenario.spaceId,
- user: scenario.users.dualRead,
- tests: {
- withReferencesNotOverwriting: {
- statusCode: 200,
- response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId),
- },
- withReferencesOverwriting: {
- statusCode: 200,
- response: createExpectReadonlyAtSpaceWithReferencesResult(scenario.spaceId),
- },
- withoutReferencesOverwriting: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId),
- },
- withoutReferencesNotOverwriting: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId),
- },
- nonExistentSpace: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
- scenario.spaceId,
- NON_EXISTENT_SPACE_ID
- ),
- },
- },
- }
+ `superuser from the ${spaceId} space`,
+ definitionAuthorized(scenario.users.superuser)
);
-
resolveCopyToSpaceConflictsTest(
- `rbac user with all at space from the ${scenario.spaceId} space`,
- {
- spaceId: scenario.spaceId,
- user: scenario.users.allAtSpace,
- tests: {
- withReferencesNotOverwriting: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithReferencesResult(scenario.spaceId),
- },
- withReferencesOverwriting: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithReferencesResult(scenario.spaceId),
- },
- withoutReferencesOverwriting: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId),
- },
- withoutReferencesNotOverwriting: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(scenario.spaceId),
- },
- nonExistentSpace: {
- statusCode: 200,
- response: createExpectUnauthorizedAtSpaceWithoutReferencesResult(
- scenario.spaceId,
- NON_EXISTENT_SPACE_ID
- ),
- },
- },
- }
+ `rbac user with all globally from the ${spaceId} space`,
+ definitionAuthorized(scenario.users.allGlobally)
+ );
+ resolveCopyToSpaceConflictsTest(
+ `dual-privileges user from the ${spaceId} space`,
+ definitionAuthorized(scenario.users.dualAll)
+ );
+ resolveCopyToSpaceConflictsTest(
+ `legacy user from the ${spaceId} space`,
+ definitionNoAccess(scenario.users.legacyAll)
+ );
+ resolveCopyToSpaceConflictsTest(
+ `rbac user with read globally from the ${spaceId} space`,
+ definitionUnauthorizedWrite(scenario.users.readGlobally)
+ );
+ resolveCopyToSpaceConflictsTest(
+ `dual-privileges readonly user from the ${spaceId} space`,
+ definitionUnauthorizedWrite(scenario.users.dualRead)
+ );
+ resolveCopyToSpaceConflictsTest(
+ `rbac user with all at space from the ${spaceId} space`,
+ definitionUnauthorizedRead(scenario.users.allAtSpace)
);
});
});
diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts
index f3e6580e439bb..ddd029c8d7d68 100644
--- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts
+++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts
@@ -25,7 +25,7 @@ const createTestCases = (spaceId: string) => {
const namespaces = [spaceId];
return [
// Test cases to check adding the target namespace to different saved objects
- { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) },
+ { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) },
{ ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) },
{ ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) },
{ ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) },
@@ -37,7 +37,7 @@ const createTestCases = (spaceId: string) => {
// These are non-exhaustive, they only check cases for adding two additional namespaces to a saved object
// More permutations are covered in the corresponding spaces_only test suite
{
- ...CASES.DEFAULT_SPACE_ONLY,
+ ...CASES.DEFAULT_ONLY,
namespaces: [SPACE_1_ID, SPACE_2_ID],
...fail404(spaceId !== DEFAULT_SPACE_ID),
},
diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts
index d83020a9598f1..4b120a71213b7 100644
--- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts
+++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts
@@ -29,7 +29,7 @@ const createTestCases = (spaceId: string) => {
// Test cases to check removing the target namespace from different saved objects
let namespaces = [spaceId];
const singleSpace = [
- { id: CASES.DEFAULT_SPACE_ONLY.id, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) },
+ { id: CASES.DEFAULT_ONLY.id, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) },
{ id: CASES.SPACE_1_ONLY.id, namespaces, ...fail404(spaceId !== SPACE_1_ID) },
{ id: CASES.SPACE_2_ONLY.id, namespaces, ...fail404(spaceId !== SPACE_2_ID) },
{ id: CASES.DEFAULT_AND_SPACE_1.id, namespaces, ...fail404(spaceId === SPACE_2_ID) },
diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts
index 75b35fecd5d83..85efe797c7402 100644
--- a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts
+++ b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts
@@ -20,6 +20,7 @@ export default function copyToSpacesOnlySuite({ getService }: FtrProviderContext
expectNoConflictsForNonExistentSpaceResult,
createExpectWithConflictsOverwritingResult,
createExpectWithConflictsWithoutOverwritingResult,
+ createMultiNamespaceTestCases,
originSpaces,
} = copyToSpaceTestSuiteFactory(es, esArchiver, supertestWithoutAuth);
@@ -34,7 +35,7 @@ export default function copyToSpacesOnlySuite({ getService }: FtrProviderContext
},
noConflictsWithReferences: {
statusCode: 200,
- response: expectNoConflictsWithReferencesResult,
+ response: expectNoConflictsWithReferencesResult(spaceId),
},
withConflictsOverwriting: {
statusCode: 200,
@@ -47,12 +48,13 @@ export default function copyToSpacesOnlySuite({ getService }: FtrProviderContext
multipleSpaces: {
statusCode: 200,
withConflictsResponse: createExpectWithConflictsOverwritingResult(spaceId),
- noConflictsResponse: expectNoConflictsWithReferencesResult,
+ noConflictsResponse: expectNoConflictsWithReferencesResult(spaceId),
},
nonExistentSpace: {
statusCode: 200,
response: expectNoConflictsForNonExistentSpaceResult,
},
+ multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId),
},
});
});
diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts
index ef2735de3d3db..5c84475d32850 100644
--- a/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts
+++ b/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts
@@ -19,6 +19,7 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Ftr
createExpectNonOverriddenResponseWithoutReferences,
createExpectOverriddenResponseWithReferences,
createExpectOverriddenResponseWithoutReferences,
+ createMultiNamespaceTestCases,
NON_EXISTENT_SPACE_ID,
originSpaces,
} = resolveCopyToSpaceConflictsSuite(esArchiver, supertestWithAuth, supertestWithoutAuth);
@@ -51,6 +52,7 @@ export default function resolveCopyToSpaceConflictsTestSuite({ getService }: Ftr
NON_EXISTENT_SPACE_ID
),
},
+ multiNamespaceTestCases: createMultiNamespaceTestCases(spaceId),
},
});
});
diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts
index 5cdebf9edfcfd..25ba986a12fd8 100644
--- a/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts
+++ b/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts
@@ -27,7 +27,7 @@ const { fail404 } = testCaseFailures;
const createSingleTestCases = (spaceId: string) => {
const namespaces = ['some-space-id'];
return [
- { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) },
+ { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) },
{ ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) },
{ ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) },
{ ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) },
@@ -43,7 +43,7 @@ const createSingleTestCases = (spaceId: string) => {
*/
const createMultiTestCases = () => {
const allSpaces = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID];
- let id = CASES.DEFAULT_SPACE_ONLY.id;
+ let id = CASES.DEFAULT_ONLY.id;
const one = [{ id, namespaces: allSpaces }];
id = CASES.DEFAULT_AND_SPACE_1.id;
const two = [{ id, namespaces: allSpaces }];
diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts
index 8bcd294b38f3f..2c4506b723533 100644
--- a/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts
+++ b/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts
@@ -27,7 +27,7 @@ const { fail404 } = testCaseFailures;
const createSingleTestCases = (spaceId: string) => {
const namespaces = [spaceId];
return [
- { ...CASES.DEFAULT_SPACE_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) },
+ { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) },
{ ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) },
{ ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) },
{ ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) },
@@ -43,7 +43,7 @@ const createSingleTestCases = (spaceId: string) => {
*/
const createMultiTestCases = () => {
const nonExistentSpaceId = 'does_not_exist'; // space that doesn't exist
- let id = CASES.DEFAULT_SPACE_ONLY.id;
+ let id = CASES.DEFAULT_ONLY.id;
const one = [
{ id, namespaces: [nonExistentSpaceId] },
{ id, namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID] },