import { cloneDeep } from 'lodash' ; import { RulesClient , ConstructorOptions } from './rules_client' ; import { savedObjectsClientMock , loggingSystemMock } from '../../../../src/core/server/mocks' ; import { taskManagerMock } from '../../task_manager/server/mocks' ; import { ruleTypeRegistryMock } from './rule_type_registry.mock' ; import { alertingAuthorizationMock } from './authorization/alerting_authorization.mock' ; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks' ; import { actionsClientMock , actionsAuthorizationMock } from '../../actions/server/mocks' ; import { AlertingAuthorization } from './authorization/alerting_authorization' ; import { ActionsAuthorization } from '../../actions/server' ; import { SavedObjectsErrorHelpers } from '../../../../src/core/server' ; import { RetryForConflictsAttempts } from './lib/retry_if_conflicts' ; import { TaskStatus } from '../../task_manager/server/task' ; import { RecoveredActionGroup } from '../common' ; let rulesClient ; const MockAlertId = 'alert-id' ; const ConflictAfterRetries = RetryForConflictsAttempts + 1 ; const taskManager = taskManagerMock . createStart ( ) ; const ruleTypeRegistry = ruleTypeRegistryMock . create ( ) ; const unsecuredSavedObjectsClient = savedObjectsClientMock . create ( ) ; const encryptedSavedObjects = encryptedSavedObjectsMock . createClient ( ) ; const authorization = alertingAuthorizationMock . create ( ) ; const actionsAuthorization = actionsAuthorizationMock . create ( ) ; const kibanaVersion = 'v7.10.0' ; const logger = loggingSystemMock . create ( ) . get ( ) ; const rulesClientParams = { taskManager , ruleTypeRegistry , unsecuredSavedObjectsClient , authorization : authorization as unknown as AlertingAuthorization , actionsAuthorization : actionsAuthorization as unknown as ActionsAuthorization , spaceId : 'default' , namespace : 'default' , getUserName : jest . fn ( ) , createAPIKey : jest . fn ( ) , logger , encryptedSavedObjectsClient : encryptedSavedObjects , getActionsClient : jest . fn ( ) , getEventLogClient : jest . fn ( ) , kibanaVersion , } ; describe ( 'rules_client_conflict_retries' , ( ) => { describe ( `1 retry works for method` , ( ) => { beforeEach ( ( ) => { mockSavedObjectUpdateConflictErrorTimes ( 1 ) ; } ) ; testFn ( update , true ) ; testFn ( updateApiKey , true ) ; testFn ( enable , true ) ; testFn ( disable , true ) ; testFn ( muteAll , true ) ; testFn ( unmuteAll , true ) ; testFn ( muteInstance , true ) ; testFn ( unmuteInstance , true ) ; } ) ; describe ( `${ ConflictAfterRetries } retries fails with conflict error` , ( ) => { beforeEach ( ( ) => { mockSavedObjectUpdateConflictErrorTimes ( ConflictAfterRetries ) ; } ) ; testFn ( update , false ) ; testFn ( updateApiKey , false ) ; testFn ( enable , false ) ; testFn ( disable , false ) ; testFn ( muteAll , false ) ; testFn ( unmuteAll , false ) ; testFn ( muteInstance , false ) ; testFn ( unmuteInstance , false ) ; } ) ; } ) ; async function update ( success ) { try { await rulesClient . update ( { id : MockAlertId , data : { schedule : { interval : '5s' } , name : 'cba' , tags : [ 'bar' ] , params : { bar : true } , throttle : '10s' , notifyWhen : null , actions : [ ] , } , } ) ; } catch ( err ) { expect ( logger . warn ) . lastCalledWith ( `rulesClient.update('alert-id') conflict, exceeded retries` ) ; return expectConflict ( success , err , 'create' ) ; } expectSuccess ( success , 3 , 'create' ) ; expect ( logger . debug ) . nthCalledWith ( 1 , `rulesClient.update('alert-id') conflict, retrying ...` ) ; } async function updateApiKey ( success ) { try { await rulesClient . updateApiKey ( { id : MockAlertId } ) ; } catch ( err ) { return expectConflict ( success , err ) ; } expectSuccess ( success ) ; } async function enable ( success ) { setupRawAlertMocks ( { } , { enabled : false } ) ; try { await rulesClient . enable ( { id : MockAlertId } ) ; } catch ( err ) { return expectConflict ( success , err ) ; } expectSuccess ( success , 3 ) ; } async function disable ( success ) { try { await rulesClient . disable ( { id : MockAlertId } ) ; } catch ( err ) { return expectConflict ( success , err ) ; } expectSuccess ( success ) ; } async function muteAll ( success ) { try { await rulesClient . muteAll ( { id : MockAlertId } ) ; } catch ( err ) { return expectConflict ( success , err ) ; } expectSuccess ( success ) ; } async function unmuteAll ( success ) { try { await rulesClient . unmuteAll ( { id : MockAlertId } ) ; } catch ( err ) { return expectConflict ( success , err ) ; } expectSuccess ( success ) ; } async function muteInstance ( success ) { try { await rulesClient . muteInstance ( { alertId : MockAlertId , alertInstanceId : 'instance-id' } ) ; } catch ( err ) { return expectConflict ( success , err ) ; } expectSuccess ( success ) ; } async function unmuteInstance ( success ) { setupRawAlertMocks ( { } , { mutedInstanceIds : [ 'instance-id' ] } ) ; try { await rulesClient . unmuteInstance ( { alertId : MockAlertId , alertInstanceId : 'instance-id' } ) ; } catch ( err ) { return expectConflict ( success , err ) ; } expectSuccess ( success ) ; } function expectSuccess ( success , count = 2 , method = 'update' ) { expect ( success ) . toBe ( true ) ; expect ( unsecuredSavedObjectsClient [ method ] ) . toHaveBeenCalledTimes ( count ) ; expect ( logger . debug ) . toHaveBeenCalled ( ) ; } function expectConflict ( success , err , method = 'update' ) { const conflictErrorMessage = SavedObjectsErrorHelpers . createConflictError ( 'alert' , MockAlertId ) . message ; expect ( `${ err }` ) . toBe ( `Error: ${ conflictErrorMessage }` ) ; expect ( success ) . toBe ( false ) ; expect ( unsecuredSavedObjectsClient [ method ] ) . toHaveBeenCalledTimes ( ConflictAfterRetries ) ; expect ( logger . debug ) . toBeCalledTimes ( RetryForConflictsAttempts ) ; expect ( logger . warn ) . toBeCalledTimes ( 1 ) ; } function testFn ( fn , success ) { test ( `${ fn . name }` , async ( ) => await fn ( success ) ) ; } function mockSavedObjectUpdateConflictErrorTimes ( times ) { const mockUpdateValue = { id : MockAlertId , type : 'alert' , attributes : { actions : [ ] , scheduledTaskId : 'scheduled-task-id' , } , references : [ ] , } ; unsecuredSavedObjectsClient . update . mockResolvedValue ( mockUpdateValue ) ; unsecuredSavedObjectsClient . create . mockResolvedValue ( mockUpdateValue ) ; for ( let i = 0 ; i < times ; i ++ ) { unsecuredSavedObjectsClient . update . mockRejectedValueOnce ( SavedObjectsErrorHelpers . createConflictError ( 'alert' , MockAlertId ) ) ; unsecuredSavedObjectsClient . create . mockRejectedValueOnce ( SavedObjectsErrorHelpers . createConflictError ( 'alert' , MockAlertId ) ) ; } } function setupRawAlertMocks ( overrides = { } , attributeOverrides = { } ) { const rawAlert = { id : MockAlertId , type : 'alert' , attributes : { enabled : true , tags : [ 'foo' ] , alertTypeId : 'myType' , schedule : { interval : '10s' } , consumer : 'myApp' , scheduledTaskId : 'task-123' , params : { } , throttle : null , actions : [ ] , muteAll : false , mutedInstanceIds : [ ] , ... attributeOverrides , } , references : [ ] , version : '123' , ... overrides , } ; const decryptedRawAlert = { ... rawAlert , attributes : { ... rawAlert . attributes , apiKey : Buffer . from ( '123:abc' ) . toString ( 'base64' ) , } , } ; unsecuredSavedObjectsClient . get . mockReset ( ) ; encryptedSavedObjects . getDecryptedAsInternalUser . mockReset ( ) ; unsecuredSavedObjectsClient . get . mockImplementation ( async ( ) => cloneDeep ( rawAlert ) ) ; encryptedSavedObjects . getDecryptedAsInternalUser . mockImplementation ( async ( ) => cloneDeep ( decryptedRawAlert ) ) ; } beforeEach ( ( ) => { jest . resetAllMocks ( ) ; rulesClientParams . createAPIKey . mockResolvedValue ( { apiKeysEnabled : false } ) ; rulesClientParams . getUserName . mockResolvedValue ( 'elastic' ) ; taskManager . runNow . mockResolvedValue ( { id : '' } ) ; taskManager . schedule . mockResolvedValue ( { id : 'scheduled-task-id' , scheduledAt : new Date ( ) , attempts : 0 , status : TaskStatus . Idle , runAt : new Date ( ) , startedAt : null , retryAt : null , state : { } , ownerId : null , taskType : 'task-type' , params : { } , } ) ; taskManager . get . mockResolvedValue ( { id : 'task-123' , taskType : 'alerting:123' , scheduledAt : new Date ( ) , attempts : 1 , status : TaskStatus . Idle , runAt : new Date ( ) , startedAt : null , retryAt : null , state : { } , params : { alertId : '1' , } , ownerId : null , } ) ; const actionsClient = actionsClientMock . create ( ) ; actionsClient . getBulk . mockResolvedValue ( [ ] ) ; rulesClientParams . getActionsClient . mockResolvedValue ( actionsClient ) ; ruleTypeRegistry . get . mockImplementation ( ( id ) => ( { id : '123' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' } ] , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , recoveryActionGroup : RecoveredActionGroup , async executor ( ) { } , producer : 'alerts' , } ) ) ; ruleTypeRegistry . get . mockReturnValue ( { id : 'myType' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' } ] , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , recoveryActionGroup : RecoveredActionGroup , async executor ( ) { } , producer : 'alerts' , } ) ; rulesClient = new RulesClient ( rulesClientParams ) ; setupRawAlertMocks ( ) ; } ) ;