Alerting: Add validation for path separators in the rule group edit modal (#90887)
Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>
This commit is contained in:
co-authored by
Tom Ratcliffe
parent
c694c97651
commit
746e2eeee6
@@ -1,6 +1,4 @@
|
|||||||
import { render } from '@testing-library/react';
|
import { render, screen, userEvent } from 'test/test-utils';
|
||||||
import * as React from 'react';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import { byLabelText, byTestId, byText, byTitle } from 'testing-library-selector';
|
import { byLabelText, byTestId, byText, byTitle } from 'testing-library-selector';
|
||||||
|
|
||||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||||
@@ -13,8 +11,6 @@ import {
|
|||||||
mockPromRecordingRule,
|
mockPromRecordingRule,
|
||||||
mockRulerAlertingRule,
|
mockRulerAlertingRule,
|
||||||
mockRulerRecordingRule,
|
mockRulerRecordingRule,
|
||||||
mockRulerRuleGroup,
|
|
||||||
mockStore,
|
|
||||||
} from '../../mocks';
|
} from '../../mocks';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
|
|
||||||
@@ -31,29 +27,8 @@ const ui = {
|
|||||||
tableRows: byTestId('row'),
|
tableRows: byTestId('row'),
|
||||||
noRulesText: byText('This group does not contain alert rules.'),
|
noRulesText: byText('This group does not contain alert rules.'),
|
||||||
};
|
};
|
||||||
mockRulerRuleGroup({
|
|
||||||
name: 'group1',
|
|
||||||
rules: [
|
|
||||||
mockRulerRecordingRule({
|
|
||||||
record: 'instance:node_num_cpu:sum',
|
|
||||||
expr: 'count without (cpu) (count without (mode) (node_cpu_seconds_total{job="integrations/node_exporter"}))',
|
|
||||||
labels: { type: 'cpu' },
|
|
||||||
}),
|
|
||||||
mockRulerAlertingRule({ alert: 'nonRecordingRule' }),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('app/types', () => ({
|
const noop = () => jest.fn();
|
||||||
...jest.requireActual('app/types'),
|
|
||||||
useDispatch: () => jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
function getProvidersWrapper() {
|
|
||||||
return function Wrapper({ children }: React.PropsWithChildren<{}>) {
|
|
||||||
const store = mockStore(() => null);
|
|
||||||
return <Provider store={store}>{children}</Provider>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('EditGroupModal', () => {
|
describe('EditGroupModal', () => {
|
||||||
it('Should disable all inputs but interval when intervalEditOnly is set', async () => {
|
it('Should disable all inputs but interval when intervalEditOnly is set', async () => {
|
||||||
@@ -65,9 +40,7 @@ describe('EditGroupModal', () => {
|
|||||||
|
|
||||||
const group = namespace.groups[0];
|
const group = namespace.groups[0];
|
||||||
|
|
||||||
render(<EditCloudGroupModal namespace={namespace} group={group} intervalEditOnly onClose={() => jest.fn()} />, {
|
render(<EditCloudGroupModal namespace={namespace} group={group} intervalEditOnly onClose={noop} />);
|
||||||
wrapper: getProvidersWrapper(),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await ui.input.namespace.find()).toHaveAttribute('readonly');
|
expect(await ui.input.namespace.find()).toHaveAttribute('readonly');
|
||||||
expect(ui.input.group.get()).toHaveAttribute('readonly');
|
expect(ui.input.group.get()).toHaveAttribute('readonly');
|
||||||
@@ -107,9 +80,7 @@ describe('EditGroupModal component on cloud alert rules', () => {
|
|||||||
|
|
||||||
const group = promNs.groups[0];
|
const group = promNs.groups[0];
|
||||||
|
|
||||||
render(<EditCloudGroupModal namespace={promNs} group={group} onClose={() => jest.fn()} />, {
|
render(<EditCloudGroupModal namespace={promNs} group={group} onClose={noop} />);
|
||||||
wrapper: getProvidersWrapper(),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await ui.input.namespace.find()).toHaveValue('prometheus-ns');
|
expect(await ui.input.namespace.find()).toHaveValue('prometheus-ns');
|
||||||
expect(ui.input.namespace.get()).not.toHaveAttribute('readonly');
|
expect(ui.input.namespace.get()).not.toHaveAttribute('readonly');
|
||||||
@@ -128,9 +99,7 @@ describe('EditGroupModal component on cloud alert rules', () => {
|
|||||||
|
|
||||||
const group = promNs.groups[0];
|
const group = promNs.groups[0];
|
||||||
|
|
||||||
render(<EditCloudGroupModal namespace={promNs} group={group} onClose={jest.fn()} />, {
|
render(<EditCloudGroupModal namespace={promNs} group={group} onClose={noop} />);
|
||||||
wrapper: getProvidersWrapper(),
|
|
||||||
});
|
|
||||||
expect(ui.table.query()).not.toBeInTheDocument();
|
expect(ui.table.query()).not.toBeInTheDocument();
|
||||||
expect(await ui.noRulesText.find()).toBeInTheDocument();
|
expect(await ui.noRulesText.find()).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -163,10 +132,11 @@ describe('EditGroupModal component on grafana-managed alert rules', () => {
|
|||||||
|
|
||||||
const grafanaGroup1 = grafanaNamespace.groups[0];
|
const grafanaGroup1 = grafanaNamespace.groups[0];
|
||||||
|
|
||||||
|
const renderWithGrafanaGroup = () =>
|
||||||
|
render(<EditCloudGroupModal namespace={grafanaNamespace} group={grafanaGroup1} onClose={noop} />);
|
||||||
|
|
||||||
it('Should show alert table', async () => {
|
it('Should show alert table', async () => {
|
||||||
render(<EditCloudGroupModal namespace={grafanaNamespace} group={grafanaGroup1} onClose={jest.fn()} />, {
|
renderWithGrafanaGroup();
|
||||||
wrapper: getProvidersWrapper(),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await ui.input.namespace.find()).toHaveValue('namespace1');
|
expect(await ui.input.namespace.find()).toHaveValue('namespace1');
|
||||||
expect(ui.input.group.get()).toHaveValue('grafanaGroup1');
|
expect(ui.input.group.get()).toHaveValue('grafanaGroup1');
|
||||||
@@ -178,18 +148,22 @@ describe('EditGroupModal component on grafana-managed alert rules', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should have folder input in readonly mode', async () => {
|
it('Should have folder input in readonly mode', async () => {
|
||||||
render(<EditCloudGroupModal namespace={grafanaNamespace} group={grafanaGroup1} onClose={jest.fn()} />, {
|
renderWithGrafanaGroup();
|
||||||
wrapper: getProvidersWrapper(),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await ui.input.namespace.find()).toHaveAttribute('readonly');
|
expect(await ui.input.namespace.find()).toHaveAttribute('readonly');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should not display folder link if no folderUrl provided', async () => {
|
it('Should not display folder link if no folderUrl provided', async () => {
|
||||||
render(<EditCloudGroupModal namespace={grafanaNamespace} group={grafanaGroup1} onClose={jest.fn()} />, {
|
renderWithGrafanaGroup();
|
||||||
wrapper: getProvidersWrapper(),
|
|
||||||
});
|
|
||||||
expect(await ui.input.namespace.find()).toHaveValue('namespace1');
|
expect(await ui.input.namespace.find()).toHaveValue('namespace1');
|
||||||
expect(ui.folderLink.query()).not.toBeInTheDocument();
|
expect(ui.folderLink.query()).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not allow slashes in the group name', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderWithGrafanaGroup();
|
||||||
|
await user.type(await ui.input.group.find(), 'group/with/slashes');
|
||||||
|
await user.click(ui.input.interval.get());
|
||||||
|
expect(await screen.findByText(/cannot contain \"\/\"/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { EvaluationIntervalLimitExceeded } from '../InvalidIntervalWarning';
|
|||||||
import { decodeGrafanaNamespace, encodeGrafanaNamespace } from '../expressions/util';
|
import { decodeGrafanaNamespace, encodeGrafanaNamespace } from '../expressions/util';
|
||||||
import { EvaluationGroupQuickPick } from '../rule-editor/EvaluationGroupQuickPick';
|
import { EvaluationGroupQuickPick } from '../rule-editor/EvaluationGroupQuickPick';
|
||||||
import { MIN_TIME_RANGE_STEP_S } from '../rule-editor/GrafanaEvaluationBehavior';
|
import { MIN_TIME_RANGE_STEP_S } from '../rule-editor/GrafanaEvaluationBehavior';
|
||||||
|
import { checkForPathSeparator } from '../rule-editor/util';
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 10;
|
const ITEMS_PER_PAGE = 10;
|
||||||
|
|
||||||
@@ -299,6 +300,11 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
readOnly={intervalEditOnly || isGrafanaManagedGroup}
|
readOnly={intervalEditOnly || isGrafanaManagedGroup}
|
||||||
{...register('namespaceName', {
|
{...register('namespaceName', {
|
||||||
required: 'Namespace name is required.',
|
required: 'Namespace name is required.',
|
||||||
|
validate: {
|
||||||
|
// for Grafana-managed we do not validate the name of the folder because we use the UID anyway
|
||||||
|
pathSeparator: (namespaceName) =>
|
||||||
|
isGrafanaManagedGroup ? true : checkForPathSeparator(namespaceName),
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
@@ -331,6 +337,9 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
readOnly={intervalEditOnly}
|
readOnly={intervalEditOnly}
|
||||||
{...register('groupName', {
|
{...register('groupName', {
|
||||||
required: 'Evaluation group name is required.',
|
required: 'Evaluation group name is required.',
|
||||||
|
validate: {
|
||||||
|
pathSeparator: (namespace) => checkForPathSeparator(namespace),
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
Reference in New Issue
Block a user