diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.test.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.test.tsx index 68cb32856d3..c6655a898da 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.test.tsx @@ -1,12 +1,10 @@ -import { render, screen, waitFor, within } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import React from 'react'; +import { render, screen, waitFor, within, userEvent } from 'test/test-utils'; import { byRole, byTestId, byText } from 'testing-library-selector'; import { AccessControlAction } from 'app/types/accessControl'; import 'core-js/stable/structured-clone'; -import { TestProvider } from '../../../../../../../test/helpers/TestProvider'; import { MatcherOperator } from '../../../../../../plugins/datasource/alertmanager/types'; import { Labels } from '../../../../../../types/unified-alerting-dto'; import { mockApi, setupMswServer } from '../../../mockApi'; @@ -140,9 +138,7 @@ describe('NotificationPreview', () => { mockOneAlertManager(); mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]); - render(, { - wrapper: TestProvider, - }); + render(); await userEvent.click(ui.previewButton.get()); await waitFor(() => { @@ -166,9 +162,7 @@ describe('NotificationPreview', () => { mockTwoAlertManagers(); mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]); - render(, { - wrapper: TestProvider, - }); + render(); await waitFor(() => { expect(ui.loadingIndicator.query()).not.toBeInTheDocument(); }); @@ -195,9 +189,7 @@ describe('NotificationPreview', () => { mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]); mockHasEditPermission(true); - render(, { - wrapper: TestProvider, - }); + render(); await waitFor(() => { expect(ui.loadingIndicator.query()).not.toBeInTheDocument(); }); @@ -219,9 +211,7 @@ describe('NotificationPreview', () => { mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]); mockHasEditPermission(false); - render(, { - wrapper: TestProvider, - }); + render(); await waitFor(() => { expect(ui.loadingIndicator.query()).not.toBeInTheDocument(); }); @@ -266,8 +256,7 @@ describe('NotificationPreviewByAlertmanager', () => { alertManagerSource={grafanaAlertManagerDataSource} potentialInstances={potentialInstances} onlyOneAM={true} - />, - { wrapper: TestProvider } + /> ); await waitFor(() => { @@ -321,8 +310,7 @@ describe('NotificationPreviewByAlertmanager', () => { alertManagerSource={grafanaAlertManagerDataSource} potentialInstances={potentialInstances} onlyOneAM={true} - />, - { wrapper: TestProvider } + /> ); await waitFor(() => { @@ -376,8 +364,7 @@ describe('NotificationPreviewByAlertmanager', () => { alertManagerSource={grafanaAlertManagerDataSource} potentialInstances={potentialInstances} onlyOneAM={true} - />, - { wrapper: TestProvider } + /> ); await waitFor(() => { @@ -402,4 +389,80 @@ describe('NotificationPreviewByAlertmanager', () => { expect(matchingInstances1).toHaveTextContent(/job=prometheus/); expect(matchingInstances1).toHaveTextContent(/severity=warning/); }); + + describe('regex matching', () => { + it('does not match regex in middle of the word as alertmanager will anchor when queried via API', async () => { + const potentialInstances: Labels[] = [{ regexfield: 'foobarfoo' }]; + + mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) => + amConfigBuilder + .addReceivers((b) => b.withName('email')) + .withRoute((routeBuilder) => + routeBuilder + .withReceiver('email') + .addRoute((rb) => rb.withReceiver('email').addMatcher('regexfield', MatcherOperator.regex, 'bar')) + ) + ); + + render( + + ); + + expect(await screen.findByText(/default policy/i)).toBeInTheDocument(); + expect(screen.queryByText(/regexfield/)).not.toBeInTheDocument(); + }); + + it('matches regex at the start of the word', async () => { + const potentialInstances: Labels[] = [{ regexfield: 'baaaaaaah' }]; + + mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) => + amConfigBuilder + .addReceivers((b) => b.withName('email')) + .withRoute((routeBuilder) => + routeBuilder + .withReceiver('email') + .addRoute((rb) => rb.withReceiver('email').addMatcher('regexfield', MatcherOperator.regex, 'ba.*h')) + ) + ); + + render( + + ); + + expect(await screen.findByText(/regexfield/i)).toBeInTheDocument(); + }); + + it('handles negated regex correctly', async () => { + const potentialInstances: Labels[] = [{ regexfield: 'thing' }]; + + mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) => + amConfigBuilder + .addReceivers((b) => b.withName('email')) + .withRoute((routeBuilder) => + routeBuilder + .withReceiver('email') + .addRoute((rb) => rb.withReceiver('email').addMatcher('regexfield', MatcherOperator.notRegex, 'thing')) + ) + ); + + render( + + ); + + expect(await screen.findByText(/default policy/i)).toBeInTheDocument(); + expect(screen.queryByText(/regexfield/i)).not.toBeInTheDocument(); + }); + }); }); diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx index 1013135fe67..db0cfbddd19 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx @@ -68,8 +68,7 @@ function NotificationRouteHeader({ - {instancesCount ?? '-'} - {pluralize('instance', instancesCount)} + {instancesCount ?? '-'} {pluralize('instance', instancesCount)}
diff --git a/public/app/features/alerting/unified/utils/notification-policies.test.ts b/public/app/features/alerting/unified/utils/notification-policies.test.ts index b6f12be2866..9dd13f9dfdd 100644 --- a/public/app/features/alerting/unified/utils/notification-policies.test.ts +++ b/public/app/features/alerting/unified/utils/notification-policies.test.ts @@ -476,6 +476,17 @@ describe('matchLabels', () => { expect(result).toHaveProperty('matches', false); expect(result.labelsMatch).toMatchSnapshot(); }); + + it('does not match unanchored regular expressions', () => { + const result = matchLabels([['foo', MatcherOperator.regex, 'bar']], [['foo', 'barbarbar']]); + // This may seem unintuitive, but this is how Alertmanager matches, as it anchors the regex + expect(result.matches).toEqual(false); + }); + + it('matches regular expressions with wildcards', () => { + const result = matchLabels([['foo', MatcherOperator.regex, '.*bar.*']], [['foo', 'barbarbar']]); + expect(result.matches).toEqual(true); + }); }); describe('unquoteRouteMatchers', () => { diff --git a/public/app/features/alerting/unified/utils/notification-policies.ts b/public/app/features/alerting/unified/utils/notification-policies.ts index 8b419161078..8e60490d7f8 100644 --- a/public/app/features/alerting/unified/utils/notification-policies.ts +++ b/public/app/features/alerting/unified/utils/notification-policies.ts @@ -232,8 +232,17 @@ type OperatorPredicate = (labelValue: string, matcherValue: string) => boolean; const OperatorFunctions: Record = { [MatcherOperator.equal]: (lv, mv) => lv === mv, [MatcherOperator.notEqual]: (lv, mv) => lv !== mv, - [MatcherOperator.regex]: (lv, mv) => new RegExp(mv).test(lv), - [MatcherOperator.notRegex]: (lv, mv) => !new RegExp(mv).test(lv), + // At the time of writing, Alertmanager compiles to another (anchored) Regular Expression, + // so we should also anchor our UI matches for consistency with this behaviour + // https://github.com/prometheus/alertmanager/blob/fd37ce9c95898ca68be1ab4d4529517174b73c33/pkg/labels/matcher.go#L69 + [MatcherOperator.regex]: (lv, mv) => { + const re = new RegExp(`^(?:${mv})$`); + return re.test(lv); + }, + [MatcherOperator.notRegex]: (lv, mv) => { + const re = new RegExp(`^(?:${mv})$`); + return !re.test(lv); + }, }; function isLabelMatchInSet(matcher: ObjectMatcher, labels: Label[]): boolean {