Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { type Dependencies } from '../../iocContainer/index.js';
2import { type MockedFn } from '../../test/mockHelpers/jestMocks.js';
3import makeGetRuleAnomalyDetectionStatistics from './getRuleAnomalyDetectionStatistics.js';
4
5/**
6 * @fileoverview The testing strategy here is to just snapshot the queries
7 * that our service function is generating, and run those manually to verify
8 * that they work. If we update the warehouse structure, we can update the
9 * snapshots and manually re-run the new queries. But having these snapshots at
10 * least makes sure we can't change inadvertently change the generated queries.
11 */
12describe('getRuleAnomalyDetectionStatistics', () => {
13 let queryMock: MockedFn<(query: string, tracer: any, binds?: readonly unknown[]) => Promise<unknown[]>>;
14 let getRulePassStatistics: Dependencies['getRuleAnomalyDetectionStatistics'];
15
16 beforeAll(() => {
17 const queryResult = [
18 {
19 RULE_ID: 'a',
20 NUM_PASSES: 2,
21 NUM_DISTINCT_USERS: 1,
22 NUM_RUNS: 4,
23 TS_START_INCLUSIVE: '2022-05-07 23:00:00.00',
24 RULE_VERSION: '2022-05-01T12:10:00.00305Z',
25 },
26 ];
27
28 // Scope of this is just the test suite, so mutation should be ok.
29 // eslint-disable-next-line better-mutation/no-mutation
30 queryMock = jest.fn() as any;
31 queryMock.mockResolvedValue(queryResult);
32
33 const dataWarehouseMock = {
34 query: queryMock,
35 transaction: jest.fn(),
36 start: jest.fn(),
37 close: jest.fn(),
38 getProvider: () => 'clickhouse' as const,
39 };
40
41 // Scope of this is just the test suite, so mutation should be ok.
42 // eslint-disable-next-line better-mutation/no-mutation
43 getRulePassStatistics = makeGetRuleAnomalyDetectionStatistics(
44 dataWarehouseMock,
45 {} as any,
46 );
47 });
48
49 beforeEach(() => {
50 // This is only safe while we're not running tests concurrently.
51 // Consider using the `makeTestWithFixture` helper instead to make
52 // a local copy of this state for each test.
53 queryMock.mockClear();
54 });
55
56 test('should return the result from the warehouse, properly formatted', async () => {
57 const result = await getRulePassStatistics();
58 expect(result).toEqual([
59 {
60 ruleId: 'a',
61 passCount: 2,
62 runsCount: 4,
63 passingUsersCount: 1,
64 windowStart: new Date('2022-05-07 23:00:00.00'),
65 approxRuleVersion: new Date('2022-05-01T12:10:00.00305Z'),
66 },
67 ]);
68 });
69
70 test('should generate the proper query when given no filters', async () => {
71 await getRulePassStatistics();
72
73 expect(queryMock).toHaveBeenCalledTimes(1);
74 expect(queryMock.mock.calls[0]).toMatchInlineSnapshot(`
75 [
76 "
77 SELECT
78 rule_id,
79 rule_version,
80 num_passes,
81 num_runs,
82 array_size(passes_distinct_user_ids) as num_distinct_users,
83 ts_start_inclusive
84 FROM RULE_ANOMALY_DETECTION_SERVICE.RULE_EXECUTION_STATISTICS
85 WHERE ts_end_exclusive <= SYSDATE()
86 ORDER BY ts_start_inclusive DESC;",
87 {},
88 [],
89 ]
90 `);
91 });
92
93 test('should generate the proper query when given a start time filter', async () => {
94 await getRulePassStatistics({ startTime: new Date('2022-05-02') });
95
96 expect(queryMock).toHaveBeenCalledTimes(1);
97 expect(queryMock.mock.calls[0]).toMatchInlineSnapshot(`
98 [
99 "
100 SELECT
101 rule_id,
102 rule_version,
103 num_passes,
104 num_runs,
105 array_size(passes_distinct_user_ids) as num_distinct_users,
106 ts_start_inclusive
107 FROM RULE_ANOMALY_DETECTION_SERVICE.RULE_EXECUTION_STATISTICS
108 WHERE ts_end_exclusive <= SYSDATE() AND ts_start_inclusive >= ?
109 ORDER BY ts_start_inclusive DESC;",
110 {},
111 [
112 2022-05-02T00:00:00.000Z,
113 ],
114 ]
115 `);
116 });
117
118 test('should generate the proper query when given a ruleIds filter', async () => {
119 await getRulePassStatistics({ ruleIds: ['1', '2'] });
120
121 expect(queryMock).toHaveBeenCalledTimes(1);
122 expect(queryMock.mock.calls[0]).toMatchInlineSnapshot(`
123 [
124 "
125 SELECT
126 rule_id,
127 rule_version,
128 num_passes,
129 num_runs,
130 array_size(passes_distinct_user_ids) as num_distinct_users,
131 ts_start_inclusive
132 FROM RULE_ANOMALY_DETECTION_SERVICE.RULE_EXECUTION_STATISTICS
133 WHERE ts_end_exclusive <= SYSDATE() AND rule_id IN (?,?)
134 ORDER BY ts_start_inclusive DESC;",
135 {},
136 [
137 "1",
138 "2",
139 ],
140 ]
141 `);
142 await getRulePassStatistics({ ruleIds: ['1'] });
143
144 expect(queryMock).toHaveBeenCalledTimes(2);
145 expect(queryMock.mock.calls[1]).toMatchInlineSnapshot(`
146 [
147 "
148 SELECT
149 rule_id,
150 rule_version,
151 num_passes,
152 num_runs,
153 array_size(passes_distinct_user_ids) as num_distinct_users,
154 ts_start_inclusive
155 FROM RULE_ANOMALY_DETECTION_SERVICE.RULE_EXECUTION_STATISTICS
156 WHERE ts_end_exclusive <= SYSDATE() AND rule_id IN (?)
157 ORDER BY ts_start_inclusive DESC;",
158 {},
159 [
160 "1",
161 ],
162 ]
163 `);
164 });
165
166 test('should generate the proper query when given both filters', async () => {
167 await getRulePassStatistics({
168 ruleIds: ['1', '2'],
169 startTime: new Date('2022-05-02 23:00:00.00-04'),
170 });
171
172 expect(queryMock).toHaveBeenCalledTimes(1);
173 expect(queryMock.mock.calls[0]).toMatchInlineSnapshot(`
174 [
175 "
176 SELECT
177 rule_id,
178 rule_version,
179 num_passes,
180 num_runs,
181 array_size(passes_distinct_user_ids) as num_distinct_users,
182 ts_start_inclusive
183 FROM RULE_ANOMALY_DETECTION_SERVICE.RULE_EXECUTION_STATISTICS
184 WHERE ts_end_exclusive <= SYSDATE() AND ts_start_inclusive >= ? AND rule_id IN (?,?)
185 ORDER BY ts_start_inclusive DESC;",
186 {},
187 [
188 2022-05-03T03:00:00.000Z,
189 "1",
190 "2",
191 ],
192 ]
193 `);
194 });
195});