1import { ClassicCondition, ExpressionQuery } from 'app/features/expressions/types';
2import { AlertQuery } from 'app/types/unified-alerting-dto';
3import { checkForPathSeparator, queriesWithUpdatedReferences, updateMathExpressionRefs } from './util';
4import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend';
5
6describe('rule-editor', () => {
7  const dataSource: AlertQuery = {
8    refId: 'A',
9    datasourceUid: 'abc123',
10    queryType: '',
11    relativeTimeRange: {
12      from: 600,
13      to: 0,
14    },
15    model: {
16      refId: 'A',
17    },
18  };
19
20  const classicCondition = {
21    refId: 'B',
22    datasourceUid: '-100',
23    queryType: '',
24    model: {
25      refId: 'B',
26      type: 'classic_conditions',
27      datasource: ExpressionDatasourceRef,
28      conditions: [
29        {
30          type: 'query',
31          evaluator: {
32            params: [3],
33            type: 'gt',
34          },
35          operator: {
36            type: 'and',
37          },
38          query: {
39            params: ['A'],
40          },
41          reducer: {
42            params: [],
43            type: 'last',
44          },
45        },
46      ],
47    },
48  };
49
50  const mathExpression = {
51    refId: 'B',
52    datasourceUid: '-100',
53    queryType: '',
54    model: {
55      refId: 'B',
56      type: 'math',
57      datasource: ExpressionDatasourceRef,
58      conditions: [],
59      expression: 'abs($A) + $A',
60    },
61  };
62
63  const reduceExpression = {
64    refId: 'B',
65    datasourceUid: '-100',
66    queryType: '',
67    model: {
68      refId: 'B',
69      type: 'reduce',
70      datasource: ExpressionDatasourceRef,
71      conditions: [],
72      reducer: 'mean',
73      expression: 'A',
74    },
75  };
76
77  const resampleExpression = {
78    refId: 'A',
79    datasourceUid: '-100',
80    model: {
81      refId: 'A',
82      type: 'resample',
83      datasource: {
84        type: '__expr__',
85        uid: '__expr__',
86      },
87      conditions: [],
88      downsampler: 'mean',
89      upsampler: 'fillna',
90      expression: 'A',
91      window: '30m',
92    },
93    queryType: '',
94  };
95
96  describe('rewires query names', () => {
97    it('should rewire classic expressions', () => {
98      const queries: AlertQuery[] = [dataSource, classicCondition];
99      const rewiredQueries = queriesWithUpdatedReferences(queries, 'A', 'C');
100
101      const queryModel = rewiredQueries[1].model as ExpressionQuery;
102
103      const checkConditionParams = (condition: ClassicCondition) => {
104        return expect(condition.query.params).toEqual(['C']);
105      };
106
107      expect(queryModel.conditions?.every(checkConditionParams));
108    });
109
110    it('should rewire math expressions', () => {
111      const queries: AlertQuery[] = [dataSource, mathExpression];
112      const rewiredQueries = queriesWithUpdatedReferences(queries, 'A', 'Query A');
113
114      const queryModel = rewiredQueries[1].model as ExpressionQuery;
115
116      expect(queryModel.expression).toBe('abs(${Query A}) + ${Query A}');
117    });
118
119    it('should rewire reduce expressions', () => {
120      const queries: AlertQuery[] = [dataSource, reduceExpression];
121      const rewiredQueries = queriesWithUpdatedReferences(queries, 'A', 'C');
122
123      const queryModel = rewiredQueries[1].model as ExpressionQuery;
124      expect(queryModel.expression).toBe('C');
125    });
126
127    it('should rewire resample expressions', () => {
128      const queries: AlertQuery[] = [dataSource, resampleExpression];
129      const rewiredQueries = queriesWithUpdatedReferences(queries, 'A', 'C');
130
131      const queryModel = rewiredQueries[1].model as ExpressionQuery;
132      expect(queryModel.expression).toBe('C');
133    });
134
135    it('should rewire multiple expressions', () => {
136      const queries: AlertQuery[] = [dataSource, mathExpression, resampleExpression];
137      const rewiredQueries = queriesWithUpdatedReferences(queries, 'A', 'C');
138
139      expect(rewiredQueries[1].model as ExpressionQuery).toHaveProperty('expression', 'abs(${C}) + ${C}');
140      expect(rewiredQueries[2].model as ExpressionQuery).toHaveProperty('expression', 'C');
141    });
142
143    it('should skip if refs are identical', () => {
144      const queries: AlertQuery[] = [dataSource, reduceExpression, mathExpression];
145      const rewiredQueries = queriesWithUpdatedReferences(queries, 'A', 'A');
146
147      expect(rewiredQueries[0]).toEqual(queries[0]);
148      expect(rewiredQueries[1]).toEqual(queries[1]);
149      expect(rewiredQueries[2]).toEqual(queries[2]);
150    });
151
152    it('should not rewire non-referencing expressions', () => {
153      const dataSource1 = { ...dataSource, refId: 'Q1' };
154      const dataSource2 = { ...dataSource, refId: 'Q2' };
155      const condition1 = {
156        ...classicCondition,
157        refId: 'A',
158        model: {
159          ...classicCondition.model,
160          conditions: [
161            {
162              ...classicCondition.model.conditions[0],
163              query: { params: ['Q1'] },
164            },
165          ],
166        },
167      };
168      const condition2 = { ...reduceExpression, refId: 'B', model: { ...reduceExpression.model, expression: 'Q1' } };
169      const condition3 = { ...mathExpression, refId: 'C', model: { ...mathExpression.model, expression: '${Q1}' } };
170
171      const queries: AlertQuery[] = [dataSource1, dataSource2, condition1, condition2, condition3];
172      const rewiredQueries = queriesWithUpdatedReferences(queries, 'Q2', 'Q3');
173
174      expect(rewiredQueries[0]).toEqual(queries[0]);
175      expect(rewiredQueries[1]).toEqual(queries[1]);
176      expect(rewiredQueries[2]).toEqual(queries[2]);
177      expect(rewiredQueries[3]).toEqual(queries[3]);
178      expect(rewiredQueries[4]).toEqual(queries[4]);
179    });
180  });
181
182  describe('updateMathExpressionRefs', () => {
183    it('should rewire refs without brackets', () => {
184      expect(updateMathExpressionRefs('abs($Foo) + $Foo', 'Foo', 'Bar')).toBe('abs(${Bar}) + ${Bar}');
185    });
186    it('should rewire refs with brackets', () => {
187      expect(updateMathExpressionRefs('abs(${Foo}) + $Foo', 'Foo', 'Bar')).toBe('abs(${Bar}) + ${Bar}');
188    });
189    it('should not rewire refs with partial variable match', () => {
190      expect(updateMathExpressionRefs('$A3 + $B', 'A', 'C')).toBe('$A3 + $B');
191    });
192  });
193});
194
195describe('checkForPathSeparator', () => {
196  it('should not allow strings with /', () => {
197    expect(checkForPathSeparator('foo / bar')).not.toBe(true);
198    expect(typeof checkForPathSeparator('foo / bar')).toBe('string');
199  });
200  it('should not allow strings with \\', () => {
201    expect(checkForPathSeparator('foo \\ bar')).not.toBe(true);
202    expect(typeof checkForPathSeparator('foo \\ bar')).toBe('string');
203  });
204  it('should allow anything without / or \\', () => {
205    expect(checkForPathSeparator('foo bar')).toBe(true);
206  });
207});
208