2025-04-01 10:38:02 +09:00

324 lines
9.8 KiB
TypeScript

import { createAction } from '@reduxjs/toolkit';
import { omit } from 'lodash';
import { GrafanaRuleIdentifier } from 'app/types/unified-alerting';
import { PostableRulerRuleGroupDTO } from 'app/types/unified-alerting-dto';
import { mockGrafanaRulerRule, mockRulerAlertingRule, mockRulerGrafanaRule, mockRulerRecordingRule } from '../../mocks';
import { fromRulerRule } from '../../utils/rule-id';
import {
SwapOperation,
addRuleAction,
deleteRuleAction,
moveRuleGroupAction,
pauseRuleAction,
renameRuleGroupAction,
reorder,
reorderRulesInRuleGroupAction,
ruleGroupReducer,
swapItems,
updateRuleAction,
updateRuleGroupAction,
} from './ruleGroups';
describe('pausing rules', () => {
// pausing only works for Grafana managed rules
it('should pause a Grafana managed rule in a group', () => {
const initialGroup: PostableRulerRuleGroupDTO = {
name: 'group-1',
interval: '5m',
rules: [
mockRulerGrafanaRule({}, { uid: '1' }),
mockRulerGrafanaRule({}, { uid: '2' }),
mockRulerGrafanaRule({}, { uid: '3' }),
],
};
// we will pause rule with UID "2"
const action = pauseRuleAction({ uid: '2', pause: true });
const output = ruleGroupReducer(initialGroup, action);
expect(output).toHaveProperty('rules');
expect(output.rules).toHaveLength(initialGroup.rules.length);
expect(output).toHaveProperty('rules.1.grafana_alert.is_paused', true);
expect(output.rules[0]).toStrictEqual(initialGroup.rules[0]);
expect(output.rules[2]).toStrictEqual(initialGroup.rules[2]);
expect(output).toMatchSnapshot();
});
it('should throw if the uid does not exist in the group', () => {
const group: PostableRulerRuleGroupDTO = {
name: 'group-1',
interval: '5m',
rules: [mockRulerGrafanaRule({}, { uid: '1' })],
};
const action = pauseRuleAction({ uid: '2', pause: true });
expect(() => {
ruleGroupReducer(group, action);
}).toThrow();
});
});
describe('removing a rule', () => {
it('should remove a Grafana managed ruler rule without touching other rules', () => {
const ruleToDelete = mockRulerGrafanaRule({}, { uid: '2' });
const initialGroup: PostableRulerRuleGroupDTO = {
name: 'group-1',
interval: '5m',
rules: [mockRulerGrafanaRule({}, { uid: '1' }), ruleToDelete, mockRulerGrafanaRule({}, { uid: '3' })],
};
const ruleIdentifier = fromRulerRule('my-datasource', 'my-namespace', 'group-1', ruleToDelete);
const action = deleteRuleAction({ identifier: ruleIdentifier });
const output = ruleGroupReducer(initialGroup, action);
expect(output).toHaveProperty('rules');
expect(output.rules).toHaveLength(2);
expect(output.rules[0]).toStrictEqual(initialGroup.rules[0]);
expect(output.rules[1]).toStrictEqual(initialGroup.rules[2]);
expect(output).toMatchSnapshot();
});
it('should remove a Data source managed ruler rule without touching other rules', () => {
const ruleToDelete = mockRulerAlertingRule({
alert: 'delete me',
});
const initialGroup: PostableRulerRuleGroupDTO = {
name: 'group-1',
interval: '5m',
rules: [
mockRulerAlertingRule({ alert: 'do not delete me' }),
ruleToDelete,
mockRulerRecordingRule({
record: 'do not delete me',
}),
],
};
const ruleIdentifier = fromRulerRule('my-datasource', 'my-namespace', 'group-1', ruleToDelete);
const action = deleteRuleAction({ identifier: ruleIdentifier });
const output = ruleGroupReducer(initialGroup, action);
expect(output).toHaveProperty('rules');
expect(output.rules).toHaveLength(2);
expect(output.rules[0]).toStrictEqual(initialGroup.rules[0]);
expect(output.rules[1]).toStrictEqual(initialGroup.rules[2]);
expect(output).toMatchSnapshot();
});
});
describe('add rule', () => {
it('should add a single rule to a rule group', () => {
const ruleToAdd = mockGrafanaRulerRule({ uid: '3' });
const rule1 = mockRulerGrafanaRule({}, { uid: '1' });
const rule2 = mockRulerGrafanaRule({}, { uid: '2' });
const initialGroup: PostableRulerRuleGroupDTO = {
name: 'group-1',
interval: '5m',
rules: [rule1, rule2],
};
const action = addRuleAction({ rule: ruleToAdd });
const output = ruleGroupReducer(initialGroup, action);
expect(output).toHaveProperty('name', 'group-1');
expect(output).toHaveProperty('interval', '5m');
expect(output.rules).toHaveLength(3);
expect(output.rules[0]).toBe(rule1);
expect(output.rules[1]).toBe(rule2);
expect(output.rules[2]).toBe(ruleToAdd);
});
it('should allow adding the rule to a new group with custom name and interval', () => {
const ruleToAdd = mockGrafanaRulerRule({ uid: '1' });
const initialGroup: PostableRulerRuleGroupDTO = {
name: 'default',
interval: '1m',
rules: [],
};
const action = addRuleAction({ rule: ruleToAdd, groupName: 'new group', interval: '10m' });
const output = ruleGroupReducer(initialGroup, action);
expect(output).toHaveProperty('name', 'new group');
expect(output).toHaveProperty('interval', '10m');
expect(output.rules).toHaveLength(1);
expect(output.rules[0]).toBe(ruleToAdd);
});
});
describe('update rule', () => {
it('should update a single rule in a rule group', () => {
const ruleToUpdate = mockGrafanaRulerRule({ uid: '1' });
const ruleIdentifier = fromRulerRule('datasource', 'namespace', 'group', ruleToUpdate);
const updatedRule: typeof ruleToUpdate = { ...ruleToUpdate, labels: { foo: 'bar' } };
const otherRule = mockRulerGrafanaRule({}, { uid: '2' });
const initialGroup: PostableRulerRuleGroupDTO = {
name: 'group-1',
interval: '5m',
rules: [ruleToUpdate, otherRule],
};
const action = updateRuleAction({ identifier: ruleIdentifier, rule: updatedRule });
const output = ruleGroupReducer(initialGroup, action);
expect(output.rules).toHaveLength(2);
expect(output.rules[0]).toStrictEqual(updatedRule);
expect(output.rules[1]).toBe(otherRule);
});
it('should throw when rule is not found', () => {
const rule = mockGrafanaRulerRule({ uid: '1' });
const ruleIdentifier: GrafanaRuleIdentifier = {
uid: 'wrong one',
ruleSourceName: 'grafana',
};
const initialGroup: PostableRulerRuleGroupDTO = {
name: 'group-1',
interval: '5m',
rules: [rule],
};
const action = updateRuleAction({ identifier: ruleIdentifier, rule });
expect(() => {
ruleGroupReducer(initialGroup, action);
}).toThrow('no rule matching identifier found');
});
});
describe('re-order rules', () => {
const r1 = mockGrafanaRulerRule({ uid: 'r1' });
const r2 = mockGrafanaRulerRule({ uid: 'r2' });
const r3 = mockGrafanaRulerRule({ uid: 'r3' });
const initialGroup: PostableRulerRuleGroupDTO = {
name: 'group-1',
interval: '5m',
rules: [r1, r2, r3],
};
const swaps: SwapOperation[] = [
[0, 1],
[2, 1],
];
const action = reorderRulesInRuleGroupAction({ swaps });
const output = ruleGroupReducer(initialGroup, action);
expect(output.rules).toHaveLength(3);
});
describe('rename rule group', () => {
const initialGroup: PostableRulerRuleGroupDTO = {
name: 'group-1',
interval: '5m',
rules: [],
};
it('should allow updating the group name and interval', () => {
const output = ruleGroupReducer(initialGroup, renameRuleGroupAction({ groupName: 'group-2', interval: '999m' }));
expect(output).toHaveProperty('name', 'group-2');
expect(output).toHaveProperty('interval', '999m');
expect(omit(output, ['name', 'interval'])).toEqual(omit(initialGroup, ['name', 'interval']));
});
});
describe('move rule group', () => {
const initialGroup: PostableRulerRuleGroupDTO = {
name: 'group-1',
interval: '5m',
rules: [],
};
it('should allow updating the group name and interval', () => {
const output = ruleGroupReducer(
initialGroup,
moveRuleGroupAction({ newNamespaceName: 'doesnt-really-matter', groupName: 'group-2', interval: '999m' })
);
expect(output).toHaveProperty('name', 'group-2');
expect(output).toHaveProperty('interval', '999m');
expect(omit(output, ['name', 'interval'])).toEqual(omit(initialGroup, ['name', 'interval']));
});
});
describe('update rule group', () => {
const initialGroup: PostableRulerRuleGroupDTO = {
name: 'group-1',
interval: '5m',
rules: [],
};
it('should allow updating the interval', () => {
const output = ruleGroupReducer(initialGroup, updateRuleGroupAction({ interval: '999m' }));
expect(output).toHaveProperty('interval', '999m');
expect(omit(output, 'interval')).toEqual(omit(initialGroup, 'interval'));
});
});
describe('unknown actions', () => {
it('should throw for unknown actions', () => {
expect(() => {
const initialGroup: PostableRulerRuleGroupDTO = {
name: 'group-1',
interval: '5m',
rules: [],
};
const unknownAction = createAction('unkown');
// @ts-expect-error
ruleGroupReducer(initialGroup, unknownAction);
}).toThrow('Unknown action');
});
});
describe('reorder and swap', () => {
it('should reorder arrays', () => {
const original = [1, 2, 3];
const operations = [
[1, 2],
[0, 2],
] satisfies SwapOperation[];
const expected = [3, 2, 1];
expect(reorder(original, operations)).toEqual(expected);
expect(original).toEqual(expected); // make sure it mutates so we can use it in produce functions
});
it('inverse swaps should cancel out', () => {
const original = [1, 2, 3];
const operations = [
[1, 2],
[2, 1],
] satisfies SwapOperation[];
expect(reorder(original, operations)).toEqual(original);
});
it('should throw when swapping out of bounds', () => {
expect(() => {
swapItems([], [-1, 3]);
}).toThrow('out of bounds');
});
});