import { MatcherOperator, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; import { InheritableProperties, computeInheritedTree, findMatchingRoutes, getInheritedProperties, matchLabels, normalizeRoute, } from './notification-policies'; import 'core-js/stable/structured-clone'; const CATCH_ALL_ROUTE: Route = { receiver: 'ALL', object_matchers: [], }; describe('findMatchingRoutes', () => { const policies: Route = { receiver: 'ROOT', group_by: ['grafana_folder'], object_matchers: [], routes: [ { receiver: 'A', object_matchers: [['team', MatcherOperator.equal, 'operations']], routes: [ { receiver: 'B1', object_matchers: [['region', MatcherOperator.equal, 'europe']], }, { receiver: 'B2', object_matchers: [['region', MatcherOperator.equal, 'nasa']], }, ], }, { receiver: 'C', object_matchers: [['foo', MatcherOperator.equal, 'bar']], }, ], group_wait: '10s', group_interval: '1m', }; it('should match root route with no matching labels', () => { const matches = findMatchingRoutes(policies, []); expect(matches).toHaveLength(1); expect(matches[0].route).toHaveProperty('receiver', 'ROOT'); }); it('should match parent route with no matching children', () => { const matches = findMatchingRoutes(policies, [['team', 'operations']]); expect(matches).toHaveLength(1); expect(matches[0].route).toHaveProperty('receiver', 'A'); }); it('should match route with negative matchers', () => { const policiesWithNegative = { ...policies, routes: policies.routes?.concat({ receiver: 'D', object_matchers: [['name', MatcherOperator.notEqual, 'gilles']], }), }; const matches = findMatchingRoutes(policiesWithNegative, [['name', 'konrad']]); expect(matches).toHaveLength(1); expect(matches[0].route).toHaveProperty('receiver', 'D'); }); it('should match child route of matching parent', () => { const matches = findMatchingRoutes(policies, [ ['team', 'operations'], ['region', 'europe'], ]); expect(matches).toHaveLength(1); expect(matches[0].route).toHaveProperty('receiver', 'B1'); }); it('should match simple policy', () => { const matches = findMatchingRoutes(policies, [['foo', 'bar']]); expect(matches).toHaveLength(1); expect(matches[0].route).toHaveProperty('receiver', 'C'); }); it('should match catch-all route', () => { const policiesWithAll: Route = { ...policies, routes: [CATCH_ALL_ROUTE, ...(policies.routes ?? [])], }; const matches = findMatchingRoutes(policiesWithAll, []); expect(matches).toHaveLength(1); expect(matches[0].route).toHaveProperty('receiver', 'ALL'); }); it('should match multiple routes with continue', () => { const policiesWithAll: Route = { ...policies, routes: [ { ...CATCH_ALL_ROUTE, continue: true, }, ...(policies.routes ?? []), ], }; const matches = findMatchingRoutes(policiesWithAll, [['foo', 'bar']]); expect(matches).toHaveLength(2); expect(matches[0].route).toHaveProperty('receiver', 'ALL'); expect(matches[1].route).toHaveProperty('receiver', 'C'); }); it('should not match grandchild routes with same labels as parent', () => { const policies: Route = { receiver: 'PARENT', group_by: ['grafana_folder'], object_matchers: [['foo', MatcherOperator.equal, 'bar']], routes: [ { receiver: 'CHILD', object_matchers: [['baz', MatcherOperator.equal, 'qux']], routes: [ { receiver: 'GRANDCHILD', object_matchers: [['foo', MatcherOperator.equal, 'bar']], }, ], }, ], group_wait: '10s', group_interval: '1m', }; const matches = findMatchingRoutes(policies, [['foo', 'bar']]); expect(matches).toHaveLength(1); expect(matches[0].route).toHaveProperty('receiver', 'PARENT'); }); }); describe('getInheritedProperties()', () => { describe('group_by: []', () => { it('should get group_by: [] from parent', () => { const parent: Route = { receiver: 'PARENT', group_by: ['label'], }; const child: Route = { receiver: 'CHILD', group_by: [], }; const childInherited = getInheritedProperties(parent, child); expect(childInherited).toHaveProperty('group_by', ['label']); }); it('should get group_by: [] from parent inherited properties', () => { const parent: Route = { receiver: 'PARENT', group_by: [], }; const child: Route = { receiver: 'CHILD', group_by: [], }; const parentInherited = { group_by: ['label'] }; const childInherited = getInheritedProperties(parent, child, parentInherited); expect(childInherited).toHaveProperty('group_by', ['label']); }); it('should not inherit if the child overrides an inheritable value (group_by)', () => { const parent: Route = { receiver: 'PARENT', group_by: ['parentLabel'], }; const child: Route = { receiver: 'CHILD', group_by: ['childLabel'], }; const childInherited = getInheritedProperties(parent, child); expect(childInherited).not.toHaveProperty('group_by'); }); it('should inherit if group_by is undefined', () => { const parent: Route = { receiver: 'PARENT', group_by: ['label'], }; const child: Route = { receiver: 'CHILD', group_by: undefined, }; const childInherited = getInheritedProperties(parent, child); expect(childInherited).toHaveProperty('group_by', ['label']); }); it('should inherit from grandparent when parent is inheriting', () => { const parentInheritedProperties: InheritableProperties = { receiver: 'grandparent' }; const parent: Route = { receiver: null, group_by: ['foo'] }; const child: Route = { receiver: null }; const childInherited = getInheritedProperties(parent, child, parentInheritedProperties); expect(childInherited).toHaveProperty('receiver', 'grandparent'); expect(childInherited.group_by).toEqual(['foo']); }); }); describe('regular "undefined" or "null" values', () => { it('should compute inherited properties being undefined', () => { const parent: Route = { receiver: 'PARENT', group_wait: '10s', }; const child: Route = { receiver: 'CHILD', }; const childInherited = getInheritedProperties(parent, child); expect(childInherited).toHaveProperty('group_wait', '10s'); }); it('should compute inherited properties being null', () => { const parent: Route = { receiver: 'PARENT', group_wait: '10s', }; const child: Route = { receiver: null, }; const childInherited = getInheritedProperties(parent, child); expect(childInherited).toHaveProperty('receiver', 'PARENT'); }); it('should compute inherited properties being undefined from parent inherited properties', () => { const parent: Route = { receiver: 'PARENT', }; const child: Route = { receiver: 'CHILD', }; const childInherited = getInheritedProperties(parent, child, { group_wait: '10s' }); expect(childInherited).toHaveProperty('group_wait', '10s'); }); it('should not inherit if the child overrides an inheritable value', () => { const parent: Route = { receiver: 'PARENT', group_wait: '10s', }; const child: Route = { receiver: 'CHILD', group_wait: '30s', }; const childInherited = getInheritedProperties(parent, child); expect(childInherited).not.toHaveProperty('group_wait'); }); it('should not inherit if the child overrides an inheritable value and the parent inherits', () => { const parent: Route = { receiver: 'PARENT', }; const child: Route = { receiver: 'CHILD', group_wait: '30s', }; const childInherited = getInheritedProperties(parent, child, { group_wait: '60s' }); expect(childInherited).not.toHaveProperty('group_wait'); }); it('should inherit if the child property is an empty string', () => { const parent: Route = { receiver: 'PARENT', }; const child: Route = { receiver: '', group_wait: '30s', }; const childInherited = getInheritedProperties(parent, child); expect(childInherited).toHaveProperty('receiver', 'PARENT'); }); }); describe('timing options', () => { it('should inherit timing options', () => { const parent: Route = { receiver: 'PARENT', group_wait: '1m', group_interval: '2m', }; const child: Route = { repeat_interval: '999s', }; const childInherited = getInheritedProperties(parent, child); expect(childInherited).toHaveProperty('group_wait', '1m'); expect(childInherited).toHaveProperty('group_interval', '2m'); }); }); it('should not inherit mute timings from parent route', () => { const parent: Route = { receiver: 'PARENT', group_by: ['parentLabel'], mute_time_intervals: ['Mon-Fri 09:00-17:00'], }; const child: Route = { receiver: 'CHILD', group_by: ['childLabel'], }; const childInherited = getInheritedProperties(parent, child); expect(childInherited).not.toHaveProperty('mute_time_intervals'); }); }); describe('computeInheritedTree', () => { it('should merge properties from parent', () => { const parent: Route = { receiver: 'PARENT', group_wait: '1m', group_interval: '2m', repeat_interval: '3m', routes: [ { repeat_interval: '999s', }, ], }; const treeRoot = computeInheritedTree(parent); expect(treeRoot).toHaveProperty('group_wait', '1m'); expect(treeRoot).toHaveProperty('group_interval', '2m'); expect(treeRoot).toHaveProperty('repeat_interval', '3m'); expect(treeRoot).toHaveProperty('routes.0.group_wait', '1m'); expect(treeRoot).toHaveProperty('routes.0.group_interval', '2m'); expect(treeRoot).toHaveProperty('routes.0.repeat_interval', '999s'); }); it('should not regress #73573', () => { const parent: Route = { routes: [ { group_wait: '1m', group_interval: '2m', repeat_interval: '3m', routes: [ { group_wait: '10m', group_interval: '20m', repeat_interval: '30m', }, { repeat_interval: '999m', }, ], }, ], }; const treeRoot = computeInheritedTree(parent); expect(treeRoot).toHaveProperty('routes.0.group_wait', '1m'); expect(treeRoot).toHaveProperty('routes.0.group_interval', '2m'); expect(treeRoot).toHaveProperty('routes.0.repeat_interval', '3m'); expect(treeRoot).toHaveProperty('routes.0.routes.0.group_wait', '10m'); expect(treeRoot).toHaveProperty('routes.0.routes.0.group_interval', '20m'); expect(treeRoot).toHaveProperty('routes.0.routes.0.repeat_interval', '30m'); expect(treeRoot).toHaveProperty('routes.0.routes.1.group_wait', '1m'); expect(treeRoot).toHaveProperty('routes.0.routes.1.group_interval', '2m'); expect(treeRoot).toHaveProperty('routes.0.routes.1.repeat_interval', '999m'); }); }); describe('normalizeRoute', () => { it('should map matchers property to object_matchers', function () { const route: RouteWithID = { id: '1', matchers: ['foo=bar', 'foo=~ba.*'], }; const normalized = normalizeRoute(route); expect(normalized.object_matchers).toHaveLength(2); expect(normalized.object_matchers).toContainEqual(['foo', MatcherOperator.equal, 'bar']); expect(normalized.object_matchers).toContainEqual(['foo', MatcherOperator.regex, 'ba.*']); expect(normalized).not.toHaveProperty('matchers'); }); it('should map match and match_re properties to object_matchers', function () { const route: RouteWithID = { id: '1', match: { foo: 'bar', }, match_re: { team: 'op.*', }, }; const normalized = normalizeRoute(route); expect(normalized.object_matchers).toHaveLength(2); expect(normalized.object_matchers).toContainEqual(['foo', MatcherOperator.equal, 'bar']); expect(normalized.object_matchers).toContainEqual(['team', MatcherOperator.regex, 'op.*']); expect(normalized).not.toHaveProperty('match'); expect(normalized).not.toHaveProperty('match_re'); }); }); describe('matchLabels', () => { it('should match with non-matching matchers', () => { const result = matchLabels( [ ['foo', MatcherOperator.equal, ''], ['team', MatcherOperator.equal, 'operations'], ], [['team', 'operations']] ); expect(result).toHaveProperty('matches', true); expect(result.labelsMatch).toMatchSnapshot(); }); it('should match with non-equal matchers', () => { const result = matchLabels( [ ['foo', MatcherOperator.notEqual, 'bar'], ['team', MatcherOperator.equal, 'operations'], ], [['team', 'operations']] ); expect(result).toHaveProperty('matches', true); expect(result.labelsMatch).toMatchSnapshot(); }); it('should not match with a set of matchers', () => { const result = matchLabels( [ ['foo', MatcherOperator.notEqual, 'bar'], ['team', MatcherOperator.equal, 'operations'], ], [ ['team', 'operations'], ['foo', 'bar'], ] ); expect(result).toHaveProperty('matches', false); expect(result.labelsMatch).toMatchSnapshot(); }); });