import expect from '@kbn/expect' ; import { SuperTest } from 'supertest' ; import { EsArchiver } from '@kbn/es-archiver' ; import { SavedObject } from 'src/core/server' ; import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants' ; import { CopyResponse } from '../../../../plugins/spaces/server/lib/copy_to_spaces' ; import { getUrlPrefix } from '../lib/space_test_utils' ; import { DescribeFn , TestDefinitionAuthentication } from '../lib/types' ; type TestResponse = Record < string , any > ; interface ResolveCopyToSpaceTest { statusCode : number ; response : ( resp : TestResponse ) => Promise < void > ; } interface ResolveCopyToSpaceMultiNamespaceTest extends ResolveCopyToSpaceTest { testTitle : string ; objects : Array < Record < string , any > > ; retries : Record < string , any > ; } interface ResolveCopyToSpaceTests { withReferencesNotOverwriting : ResolveCopyToSpaceTest ; withReferencesOverwriting : ResolveCopyToSpaceTest ; withoutReferencesOverwriting : ResolveCopyToSpaceTest ; withoutReferencesNotOverwriting : ResolveCopyToSpaceTest ; nonExistentSpace : ResolveCopyToSpaceTest ; multiNamespaceTestCases : ( ) => ResolveCopyToSpaceMultiNamespaceTest [ ] ; } interface ResolveCopyToSpaceTestDefinition { user ? : TestDefinitionAuthentication ; spaceId ? : string ; tests : ResolveCopyToSpaceTests ; } const NON_EXISTENT_SPACE_ID = 'non_existent_space' ; const getDestinationSpace = ( originSpaceId ? ) => { if ( ! originSpaceId || originSpaceId === DEFAULT_SPACE_ID ) { return 'space_1' ; } return DEFAULT_SPACE_ID ; } ; export function resolveCopyToSpaceConflictsSuite ( esArchiver , supertestWithAuth , supertestWithoutAuth ) { const getVisualizationAtSpace = async ( spaceId ) : Promise < SavedObject < any > > => { return supertestWithAuth . get ( `${ getUrlPrefix ( spaceId ) }/api/saved_objects/visualization/cts_vis_3_${ spaceId }` ) . then ( ( response ) => response . body ) ; } ; const getDashboardAtSpace = async ( spaceId ) : Promise < SavedObject < any > > => { return supertestWithAuth . get ( `${ getUrlPrefix ( spaceId ) }/api/saved_objects/dashboard/cts_dashboard_${ spaceId }` ) . then ( ( response ) => response . body ) ; } ; const getObjectsAtSpace = async ( spaceId ) : Promise < [ SavedObject < any > , SavedObject < any > ] > => { const dashboard = await getDashboardAtSpace ( spaceId ) ; const visualization = await getVisualizationAtSpace ( spaceId ) ; return [ dashboard , visualization ] ; } ; const createExpectOverriddenResponseWithReferences = ( sourceSpaceId ) => async ( response ) => { const destination = getDestinationSpace ( sourceSpaceId ) ; const result = response . body ; expect ( result ) . to . eql ( { [ destination ] : { success : true , successCount : 2 , successResults : [ { id : `cts_ip_1_${ sourceSpaceId }` , type : 'index-pattern' , meta : { title : `Copy to Space index pattern 1 from ${ sourceSpaceId } space` , icon : 'indexPatternApp' , } , destinationId : `cts_ip_1_${ destination }` , overwrite : true , } , { id : `cts_vis_3_${ sourceSpaceId }` , type : 'visualization' , meta : { title : `CTS vis 3 from ${ sourceSpaceId } space` , icon : 'visualizeApp' , } , destinationId : `cts_vis_3_${ destination }` , overwrite : true , } , ] , } , } ) ; const [ dashboard , visualization ] = await getObjectsAtSpace ( destination ) ; expect ( dashboard . attributes . title ) . to . eql ( `This is the ${ destination } test space CTS dashboard` ) ; expect ( visualization . attributes . title ) . to . eql ( `CTS vis 3 from ${ sourceSpaceId } space` ) ; } ; const createExpectOverriddenResponseWithoutReferences = ( sourceSpaceId , destinationSpaceId = getDestinationSpace ( sourceSpaceId ) ) => async ( response ) => { const result = response . body ; expect ( result ) . to . eql ( { [ destinationSpaceId ] : { success : true , successCount : 1 , successResults : [ { id : `cts_dashboard_${ sourceSpaceId }` , type : 'dashboard' , meta : { title : `This is the ${ sourceSpaceId } test space CTS dashboard` , icon : 'dashboardApp' , } , destinationId : `cts_dashboard_${ destinationSpaceId }` , overwrite : true , } , ] , } , } ) ; const [ dashboard , visualization ] = await getObjectsAtSpace ( destinationSpaceId ) ; expect ( dashboard . attributes . title ) . to . eql ( `This is the ${ sourceSpaceId } test space CTS dashboard` ) ; if ( destinationSpaceId === NON_EXISTENT_SPACE_ID ) { expect ( ( visualization as any ) . statusCode ) . to . eql ( 404 ) ; } else { expect ( visualization . attributes . title ) . to . eql ( `CTS vis 3 from ${ destinationSpaceId } space` ) ; } } ; const createExpectNonOverriddenResponseWithReferences = ( sourceSpaceId ) => async ( response ) => { const destination = getDestinationSpace ( sourceSpaceId ) ; const result = response . body ; expect ( result ) . to . eql ( { [ destination ] : { success : false , successCount : 0 , errors : [ { error : { type : 'conflict' , destinationId : `cts_ip_1_${ destination }` , } , id : `cts_ip_1_${ sourceSpaceId }` , title : `Copy to Space index pattern 1 from ${ sourceSpaceId } space` , meta : { title : `Copy to Space index pattern 1 from ${ sourceSpaceId } space` , icon : 'indexPatternApp' , } , type : 'index-pattern' , } , { error : { type : 'conflict' , destinationId : `cts_vis_3_${ destination }` , } , id : `cts_vis_3_${ sourceSpaceId }` , title : `CTS vis 3 from ${ sourceSpaceId } space` , meta : { title : `CTS vis 3 from ${ sourceSpaceId } space` , icon : 'visualizeApp' , } , type : 'visualization' , } , ] , } , } ) ; const [ dashboard , visualization ] = await getObjectsAtSpace ( destination ) ; expect ( dashboard . attributes . title ) . to . eql ( `This is the ${ destination } test space CTS dashboard` ) ; expect ( visualization . attributes . title ) . to . eql ( `CTS vis 3 from ${ destination } space` ) ; } ; const createExpectNonOverriddenResponseWithoutReferences = ( sourceSpaceId ) => async ( response ) => { const destination = getDestinationSpace ( sourceSpaceId ) ; const result = response . body ; expect ( result ) . to . eql ( { [ destination ] : { success : false , successCount : 0 , errors : [ { error : { type : 'conflict' , destinationId : `cts_dashboard_${ destination }` , } , id : `cts_dashboard_${ sourceSpaceId }` , type : 'dashboard' , title : `This is the ${ sourceSpaceId } test space CTS dashboard` , meta : { title : `This is the ${ sourceSpaceId } test space CTS dashboard` , icon : 'dashboardApp' , } , } , ] , } , } ) ; const [ dashboard , visualization ] = await getObjectsAtSpace ( destination ) ; expect ( dashboard . attributes . title ) . to . eql ( `This is the ${ destination } test space CTS dashboard` ) ; expect ( visualization . attributes . title ) . to . eql ( `CTS vis 3 from ${ destination } space` ) ; } ; const expectRouteForbiddenResponse = async ( resp ) => { expect ( resp . body ) . to . eql ( { statusCode : 403 , error : 'Forbidden' , message : 'Forbidden' , } ) ; } ; const expectRouteNotFoundResponse = async ( resp ) => { expect ( resp . body ) . to . eql ( { statusCode : 404 , error : 'Not Found' , message : 'Not Found' , } ) ; } ; const createExpectUnauthorizedAtSpaceWithReferencesResult = ( spaceId = DEFAULT_SPACE_ID ) => async ( resp ) => { const destination = getDestinationSpace ( spaceId ) ; const result = resp . body as CopyResponse ; expect ( result ) . to . eql ( { [ destination ] : { success : false , successCount : 0 , errors : [ { statusCode : 403 , error : 'Forbidden' , message : 'Unable to bulk_create index-pattern,visualization' , } , ] , } , } as CopyResponse ) ; const [ dashboard , visualization ] = await getObjectsAtSpace ( destination ) ; expect ( dashboard . attributes . title ) . to . eql ( `This is the ${ destination } test space CTS dashboard` ) ; expect ( visualization . attributes . title ) . to . eql ( `CTS vis 3 from ${ destination } space` ) ; } ; const createExpectUnauthorizedAtSpaceWithoutReferencesResult = ( sourceSpaceId = DEFAULT_SPACE_ID , destinationSpaceId = getDestinationSpace ( sourceSpaceId ) ) => async ( resp ) => { const result = resp . body as CopyResponse ; expect ( result ) . to . eql ( { [ destinationSpaceId ] : { success : false , successCount : 0 , errors : [ { statusCode : 403 , error : 'Forbidden' , message : 'Unable to bulk_create dashboard' , } , ] , } , } as CopyResponse ) ; const [ dashboard , visualization ] = await getObjectsAtSpace ( destinationSpaceId ) ; if ( destinationSpaceId === NON_EXISTENT_SPACE_ID ) { expect ( ( dashboard as any ) . statusCode ) . to . eql ( 404 ) ; expect ( ( visualization as any ) . statusCode ) . to . eql ( 404 ) ; } else { expect ( dashboard . attributes . title ) . to . eql ( `This is the ${ destinationSpaceId } test space CTS dashboard` ) ; expect ( visualization . attributes . title ) . to . eql ( `CTS vis 3 from ${ destinationSpaceId } space` ) ; } } ; /**
     * 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 , outcome = 'authorized' ) => ( ) : ResolveCopyToSpaceMultiNamespaceTest [ ] => { const statusCode = outcome === 'noAccess' ? 403 : 200 ; const type = 'sharedtype' ; const exactMatchId = 'each_space' ; const inexactMatchIdA = `conflict_1a_${ spaceId }` ; const inexactMatchIdB = `conflict_1b_${ spaceId }` ; const inexactMatchIdC = `conflict_1c_default_and_space_1` ; const ambiguousConflictId = `conflict_2_${ spaceId }` ; const createRetries = ( overwriteRetry ) => ( { space_2 : [ overwriteRetry ] , } ) ; const getResult = ( response ) => ( response . body as CopyResponse ) . space_2 ; const expectSavedObjectForbiddenResponse = ( response ) => { expect ( response . body ) . to . eql ( { space_2 : { success : false , successCount : 0 , errors : [ { statusCode : 403 , error : 'Forbidden' , message : `Unable to bulk_create sharedtype` } , ] , } , } ) ; } ; const expectSavedObjectSuccessResponse = ( response , id , destinationId ? ) => { const { success , successCount , successResults , errors } = getResult ( response ) ; expect ( success ) . to . eql ( true ) ; expect ( successCount ) . to . eql ( 1 ) ; expect ( errors ) . to . be ( undefined ) ; const title = ( ( ) => { switch ( id ) { case exactMatchId : return 'A shared saved-object in the default, space_1, and space_2 spaces' ; case inexactMatchIdA : return 'This is used to test an inexact match conflict for an originId -> originId match' ; case inexactMatchIdB : return 'This is used to test an inexact match conflict for an originId -> id match' ; case inexactMatchIdC : return 'This is used to test an inexact match conflict for an id -> originId match' ; default : return 'A shared saved-object in one space' ; } } ) ( ) ; const meta = { title , icon : 'beaker' } ; expect ( successResults ) . to . eql ( [ { type , id , meta , overwrite : true , ... ( 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 ) => { if ( outcome === 'authorized' ) { expectSavedObjectSuccessResponse ( response , exactMatchId ) ; } else if ( outcome === 'noAccess' ) { expectRouteForbiddenResponse ( response ) ; } else { expectSavedObjectForbiddenResponse ( response ) ; } } , } , { testTitle : 'copying with an inexact match conflict (a) - originId matches existing originId' , objects : [ { type , id : inexactMatchIdA } ] , retries : createRetries ( { type , id : inexactMatchIdA , overwrite : true , destinationId : 'conflict_1a_space_2' , } ) , statusCode , response : async ( response ) => { if ( outcome === 'authorized' ) { expectSavedObjectSuccessResponse ( response , inexactMatchIdA , 'conflict_1a_space_2' ) ; } else if ( outcome === 'noAccess' ) { expectRouteForbiddenResponse ( response ) ; } else { expectSavedObjectForbiddenResponse ( response ) ; } } , } , { testTitle : 'copying with an inexact match conflict (b) - originId matches existing id' , objects : [ { type , id : inexactMatchIdB } ] , retries : createRetries ( { type , id : inexactMatchIdB , overwrite : true , destinationId : 'conflict_1b_space_2' , } ) , statusCode , response : async ( response ) => { if ( outcome === 'authorized' ) { expectSavedObjectSuccessResponse ( response , inexactMatchIdB , 'conflict_1b_space_2' ) ; } else if ( outcome === 'noAccess' ) { expectRouteForbiddenResponse ( response ) ; } else { expectSavedObjectForbiddenResponse ( response ) ; } } , } , { testTitle : 'copying with an inexact match conflict (c) - id matches existing originId' , objects : [ { type , id : inexactMatchIdC } ] , retries : createRetries ( { type , id : inexactMatchIdC , overwrite : true , destinationId : 'conflict_1c_space_2' , } ) , statusCode , response : async ( response ) => { if ( outcome === 'authorized' ) { expectSavedObjectSuccessResponse ( response , inexactMatchIdC , 'conflict_1c_space_2' ) ; } else if ( outcome === 'noAccess' ) { expectRouteForbiddenResponse ( response ) ; } else { expectSavedObjectForbiddenResponse ( 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 ) => { if ( outcome === 'authorized' ) { expectSavedObjectSuccessResponse ( response , ambiguousConflictId , 'conflict_2_space_2' ) ; } else if ( outcome === 'noAccess' ) { expectRouteForbiddenResponse ( response ) ; } else { expectSavedObjectForbiddenResponse ( response ) ; } } , } , ] ; } ; const makeResolveCopyToSpaceConflictsTest = ( describeFn ) => ( description , { user = { } , spaceId = DEFAULT_SPACE_ID , tests } ) => { describeFn ( description , ( ) => { before ( ( ) => { expect ( [ 'default' , 'space_1' ] ) . to . contain ( spaceId ) ; } ) ; describe ( 'single-namespace types' , ( ) => { beforeEach ( ( ) => esArchiver . load ( 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' ) ) ; afterEach ( ( ) => esArchiver . unload ( 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' ) ) ; const dashboardObject = { type : 'dashboard' , id : `cts_dashboard_${ spaceId }` } ; const visualizationObject = { type : 'visualization' , id : `cts_vis_3_${ spaceId }` } ; const indexPatternObject = { type : 'index-pattern' , id : `cts_ip_1_${ spaceId }` } ; 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 , createNewCopies : false , retries : { [ destination ] : [ { ... indexPatternObject , destinationId : `cts_ip_1_${ destination }` , overwrite : false , } , { ... visualizationObject , destinationId : `cts_vis_3_${ destination }` , 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 , createNewCopies : false , retries : { [ destination ] : [ { ... indexPatternObject , destinationId : `cts_ip_1_${ destination }` , overwrite : true , } , { ... visualizationObject , destinationId : `cts_vis_3_${ destination }` , 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 , createNewCopies : false , retries : { [ destination ] : [ { ... dashboardObject , destinationId : `cts_dashboard_${ destination }` , 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 , createNewCopies : false , retries : { [ destination ] : [ { ... dashboardObject , destinationId : `cts_dashboard_${ destination }` , 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 , createNewCopies : false , retries : { [ destination ] : [ { ... dashboardObject , destinationId : `cts_dashboard_${ destination }` , overwrite : true , } , ] , } , } ) . expect ( tests . nonExistentSpace . statusCode ) . then ( tests . nonExistentSpace . response ) ; } ) ; } ) ; const includeReferences = false ; const createNewCopies = false ; describe ( `multi-namespace types with "overwrite" retry` , ( ) => { before ( ( ) => esArchiver . load ( 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' ) ) ; after ( ( ) => esArchiver . unload ( 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/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 , createNewCopies , retries } ) . expect ( statusCode ) . then ( response ) ; } ) ; } ) ; } ) ; } ) ; } ; const resolveCopyToSpaceConflictsTest = makeResolveCopyToSpaceConflictsTest ( describe ) ; resolveCopyToSpaceConflictsTest . only = makeResolveCopyToSpaceConflictsTest ( describe . only ) ; return { resolveCopyToSpaceConflictsTest , expectRouteForbiddenResponse , expectRouteNotFoundResponse , createExpectOverriddenResponseWithReferences , createExpectOverriddenResponseWithoutReferences , createExpectNonOverriddenResponseWithReferences , createExpectNonOverriddenResponseWithoutReferences , createExpectUnauthorizedAtSpaceWithReferencesResult , createExpectUnauthorizedAtSpaceWithoutReferencesResult , createMultiNamespaceTestCases , originSpaces : [ 'default' , 'space_1' ] , NON_EXISTENT_SPACE_ID , } ; }