import { TaskRunnerFactory } from './task_runner' ; import { RuleTypeRegistry , ConstructorOptions } from './rule_type_registry' ; import { ActionGroup , RuleType } from './types' ; import { taskManagerMock } from '../../task_manager/server/mocks' ; import { ILicenseState } from './lib/license_state' ; import { licenseStateMock } from './lib/license_state.mock' ; import { licensingMock } from '../../licensing/server/mocks' ; let mockedLicenseState ; let ruleTypeRegistryParams ; const taskManager = taskManagerMock . createSetup ( ) ; beforeEach ( ( ) => { jest . resetAllMocks ( ) ; mockedLicenseState = licenseStateMock . create ( ) ; ruleTypeRegistryParams = { taskManager , taskRunnerFactory : new TaskRunnerFactory ( ) , licenseState : mockedLicenseState , licensing : licensingMock . createSetup ( ) , } ; } ) ; describe ( 'has()' , ( ) => { test ( 'returns false for unregistered rule types' , ( ) => { const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; expect ( registry . has ( 'foo' ) ) . toEqual ( false ) ; } ) ; test ( 'returns true for registered rule types' , ( ) => { const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; registry . register ( { id : 'foo' , name : 'Foo' , actionGroups : [ { id : 'default' , name : 'Default' , } , ] , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , executor : jest . fn ( ) , producer : 'alerts' , } ) ; expect ( registry . has ( 'foo' ) ) . toEqual ( true ) ; } ) ; } ) ; describe ( 'register()' , ( ) => { test ( 'throws if RuleType Id contains invalid characters' , ( ) => { const ruleType = { id : 'test' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , ] , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , executor : jest . fn ( ) , producer : 'alerts' , } ; const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; const invalidCharacters = [ ' ' , ':' , '*' , '*' , '/' ] ; for ( const char of invalidCharacters ) { expect ( ( ) => registry . register ( { ... ruleType , id : `${ ruleType . id }${ char }` } ) ) . toThrowError ( new Error ( `expected RuleType Id not to include invalid character: ${ char }` ) ) ; } const [ first , second ] = invalidCharacters ; expect ( ( ) => registry . register ( { ... ruleType , id : `${ first }${ ruleType . id }${ second }` } ) ) . toThrowError ( new Error ( `expected RuleType Id not to include invalid characters: ${ first }, ${ second }` ) ) ; } ) ; test ( 'throws if RuleType Id isnt a string' , ( ) => { const ruleType = { id : 123 as unknown as string , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , ] , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , executor : jest . fn ( ) , producer : 'alerts' , } ; const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; expect ( ( ) => registry . register ( ruleType ) ) . toThrowError ( new Error ( `expected value of type [string] but got [number]` ) ) ; } ) ; test ( 'throws if RuleType ruleTaskTimeout is not a valid duration' , ( ) => { const ruleType = { id : '123' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , ] , ruleTaskTimeout : '23 milisec' , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , executor : jest . fn ( ) , producer : 'alerts' , } ; const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; expect ( ( ) => registry . register ( ruleType ) ) . toThrowError ( new Error ( `Rule type \"123\" has invalid timeout: string is not a valid duration: 23 milisec.` ) ) ; } ) ; test ( 'throws if defaultScheduleInterval isnt valid' , ( ) => { const ruleType = { id : '123' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , ] , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , executor : jest . fn ( ) , producer : 'alerts' , defaultScheduleInterval : 'foobar' , } ; const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; expect ( ( ) => registry . register ( ruleType ) ) . toThrowError ( new Error ( `Rule type \"123\" has invalid default interval: string is not a valid duration: foobar.` ) ) ; } ) ; test ( 'throws if minimumScheduleInterval isnt valid' , ( ) => { const ruleType = { id : '123' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , ] , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , executor : jest . fn ( ) , producer : 'alerts' , minimumScheduleInterval : 'foobar' , } ; const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; expect ( ( ) => registry . register ( ruleType ) ) . toThrowError ( new Error ( `Rule type \"123\" has invalid minimum interval: string is not a valid duration: foobar.` ) ) ; } ) ; test ( 'throws if RuleType action groups contains reserved group id' , ( ) => { const ruleType = { id : 'test' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , { id : 'recovered' , name : 'Recovered' , } as unknown as ActionGroup < 'NotReserved' > , ] , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , executor : jest . fn ( ) , producer : 'alerts' , } ; const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; expect ( ( ) => registry . register ( ruleType ) ) . toThrowError ( new Error ( `Rule type [id="${ ruleType . id }"] cannot be registered. Action groups [recovered] are reserved by the framework.` ) ) ; } ) ; test ( 'allows an RuleType to specify a custom recovery group' , ( ) => { const ruleType = { id : 'test' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , ] , defaultActionGroupId : 'default' , recoveryActionGroup : { id : 'backToAwesome' , name : 'Back To Awesome' , } , executor : jest . fn ( ) , producer : 'alerts' , minimumLicenseRequired : 'basic' , isExportable : true , } ; const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; registry . register ( ruleType ) ; expect ( registry . get ( 'test' ) . actionGroups ) . toMatchInlineSnapshot ( `
      Array [
        Object {
          "id": "default",
          "name": "Default",
        },
        Object {
          "id": "backToAwesome",
          "name": "Back To Awesome",
        },
      ]
    ` ) ; } ) ; test ( 'allows an RuleType to specify a custom rule task timeout' , ( ) => { const ruleType = { id : 'test' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , ] , defaultActionGroupId : 'default' , ruleTaskTimeout : '13m' , executor : jest . fn ( ) , producer : 'alerts' , minimumLicenseRequired : 'basic' , isExportable : true , } ; const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; registry . register ( ruleType ) ; expect ( registry . get ( 'test' ) . ruleTaskTimeout ) . toBe ( '13m' ) ; } ) ; test ( 'throws if the custom recovery group is contained in the RuleType action groups' , ( ) => { const ruleType = { id : 'test' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , { id : 'backToAwesome' , name : 'Back To Awesome' , } , ] , recoveryActionGroup : { id : 'backToAwesome' , name : 'Back To Awesome' , } , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , executor : jest . fn ( ) , producer : 'alerts' , } ; const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; expect ( ( ) => registry . register ( ruleType ) ) . toThrowError ( new Error ( `Rule type [id="${ ruleType . id }"] cannot be registered. Action group [backToAwesome] cannot be used as both a recovery and an active action group.` ) ) ; } ) ; test ( 'registers the executor with the task manager' , ( ) => { const ruleType = { id : 'test' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , ] , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , executor : jest . fn ( ) , producer : 'alerts' , ruleTaskTimeout : '20m' , } ; const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; registry . register ( ruleType ) ; expect ( taskManager . registerTaskDefinitions ) . toHaveBeenCalledTimes ( 1 ) ; expect ( taskManager . registerTaskDefinitions . mock . calls [ 0 ] ) . toMatchInlineSnapshot ( `
      Array [
        Object {
          "alerting:test": Object {
            "createTaskRunner": [Function],
            "timeout": "20m",
            "title": "Test",
          },
        },
      ]
    ` ) ; } ) ; test ( 'shallow clones the given rule type' , ( ) => { const ruleType = { id : 'test' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , ] , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , executor : jest . fn ( ) , producer : 'alerts' , } ; const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; registry . register ( ruleType ) ; ruleType . name = 'Changed' ; expect ( registry . get ( 'test' ) . name ) . toEqual ( 'Test' ) ; } ) ; test ( 'should throw an error if type is already registered' , ( ) => { const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; registry . register ( { id : 'test' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , ] , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , executor : jest . fn ( ) , producer : 'alerts' , } ) ; expect ( ( ) => registry . register ( { id : 'test' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , ] , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , executor : jest . fn ( ) , producer : 'alerts' , } ) ) . toThrowErrorMatchingInlineSnapshot ( `"Rule type \\"test\\" is already registered."` ) ; } ) ; } ) ; describe ( 'get()' , ( ) => { test ( 'should return registered type' , ( ) => { const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; registry . register ( { id : 'test' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , ] , defaultActionGroupId : 'default' , minimumLicenseRequired : 'basic' , isExportable : true , executor : jest . fn ( ) , producer : 'alerts' , } ) ; const ruleType = registry . get ( 'test' ) ; expect ( ruleType ) . toMatchInlineSnapshot ( `
      Object {
        "actionGroups": Array [
          Object {
            "id": "default",
            "name": "Default",
          },
          Object {
            "id": "recovered",
            "name": "Recovered",
          },
        ],
        "actionVariables": Object {
          "context": Array [],
          "params": Array [],
          "state": Array [],
        },
        "defaultActionGroupId": "default",
        "executor": [MockFunction],
        "id": "test",
        "isExportable": true,
        "minimumLicenseRequired": "basic",
        "name": "Test",
        "producer": "alerts",
        "recoveryActionGroup": Object {
          "id": "recovered",
          "name": "Recovered",
        },
      }
    ` ) ; } ) ; test ( `should throw an error if type isn't registered` , ( ) => { const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; expect ( ( ) => registry . get ( 'test' ) ) . toThrowErrorMatchingInlineSnapshot ( `"Rule type \\"test\\" is not registered."` ) ; } ) ; } ) ; describe ( 'list()' , ( ) => { test ( 'should return empty when nothing is registered' , ( ) => { const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; const result = registry . list ( ) ; expect ( result ) . toMatchInlineSnapshot ( `Set {}` ) ; } ) ; test ( 'should return registered types' , ( ) => { const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; registry . register ( { id : 'test' , name : 'Test' , actionGroups : [ { id : 'testActionGroup' , name : 'Test Action Group' , } , ] , defaultActionGroupId : 'testActionGroup' , isExportable : true , ruleTaskTimeout : '20m' , minimumLicenseRequired : 'basic' , executor : jest . fn ( ) , producer : 'alerts' , } ) ; const result = registry . list ( ) ; expect ( result ) . toMatchInlineSnapshot ( `
      Set {
        Object {
          "actionGroups": Array [
            Object {
              "id": "testActionGroup",
              "name": "Test Action Group",
            },
            Object {
              "id": "recovered",
              "name": "Recovered",
            },
          ],
          "actionVariables": Object {
            "context": Array [],
            "params": Array [],
            "state": Array [],
          },
          "defaultActionGroupId": "testActionGroup",
          "defaultScheduleInterval": undefined,
          "enabledInLicense": false,
          "id": "test",
          "isExportable": true,
          "minimumLicenseRequired": "basic",
          "minimumScheduleInterval": undefined,
          "name": "Test",
          "producer": "alerts",
          "recoveryActionGroup": Object {
            "id": "recovered",
            "name": "Recovered",
          },
          "ruleTaskTimeout": "20m",
        },
      }
    ` ) ; } ) ; test ( 'should return action variables state and empty context' , ( ) => { const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; registry . register ( ruleTypeWithVariables ( 'x' , '' , 's' ) ) ; const ruleType = registry . get ( 'x' ) ; expect ( ruleType . actionVariables ) . toBeTruthy ( ) ; const context = ruleType . actionVariables ! . context ; const state = ruleType . actionVariables ! . state ; expect ( context ) . toBeTruthy ( ) ; expect ( context ! . length ) . toBe ( 0 ) ; expect ( state ) . toBeTruthy ( ) ; expect ( state ! . length ) . toBe ( 1 ) ; expect ( state ! [ 0 ] ) . toEqual ( { name : 's' , description : 'x state' } ) ; } ) ; test ( 'should return action variables context and empty state' , ( ) => { const registry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; registry . register ( ruleTypeWithVariables ( 'x' , 'c' , '' ) ) ; const ruleType = registry . get ( 'x' ) ; expect ( ruleType . actionVariables ) . toBeTruthy ( ) ; const context = ruleType . actionVariables ! . context ; const state = ruleType . actionVariables ! . state ; expect ( state ) . toBeTruthy ( ) ; expect ( state ! . length ) . toBe ( 0 ) ; expect ( context ) . toBeTruthy ( ) ; expect ( context ! . length ) . toBe ( 1 ) ; expect ( context ! [ 0 ] ) . toEqual ( { name : 'c' , description : 'x context' } ) ; } ) ; } ) ; describe ( 'ensureRuleTypeEnabled' , ( ) => { let ruleTypeRegistry ; beforeEach ( ( ) => { ruleTypeRegistry = new RuleTypeRegistry ( ruleTypeRegistryParams ) ; ruleTypeRegistry . register ( { id : 'test' , name : 'Test' , actionGroups : [ { id : 'default' , name : 'Default' , } , ] , defaultActionGroupId : 'default' , executor : jest . fn ( ) , producer : 'alerts' , isExportable : true , minimumLicenseRequired : 'basic' , recoveryActionGroup : { id : 'recovered' , name : 'Recovered' } , } ) ; } ) ; test ( 'should call ensureLicenseForAlertType on the license state' , async ( ) => { ruleTypeRegistry . ensureRuleTypeEnabled ( 'test' ) ; expect ( mockedLicenseState . ensureLicenseForRuleType ) . toHaveBeenCalled ( ) ; } ) ; test ( 'should throw when ensureLicenseForAlertType throws' , async ( ) => { mockedLicenseState . ensureLicenseForRuleType . mockImplementation ( ( ) => { throw new Error ( 'Fail' ) ; } ) ; expect ( ( ) => ruleTypeRegistry . ensureRuleTypeEnabled ( 'test' ) ) . toThrowErrorMatchingInlineSnapshot ( `"Fail"` ) ; } ) ; } ) ; function ruleTypeWithVariables < ActionGroupIds extends string > ( id , context , state ) { const baseAlert = { id , name : `${ id }-name` , actionGroups : [ ] , defaultActionGroupId : id , isExportable : true , minimumLicenseRequired : 'basic' , async executor ( ) { } , producer : 'alerts' , } ; if ( ! context && ! state ) return baseAlert ; return { ... baseAlert , actionVariables : { ... ( context ? { context : [ { name : context , description : `${ id } context` } ] } : { } ) , ... ( state ? { state : [ { name : state , description : `${ id } state` } ] } : { } ) , } , } ; }