Mirror of https://github.com/roostorg/coop github.com/roostorg/coop
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 322 lines 11 kB view raw
1import getBottle, { 2 type Dependencies, 3 type PublicInterface, 4} from '../../iocContainer/index.js'; 5import { type NotificationsService } from '../../services/notificationsService/notificationsService.js'; 6import { type GetCurrentPeriodRuleAlarmStatuses } from '../../services/ruleAnomalyDetectionService/getCurrentPeriodRuleAlarmStatuses.js'; 7import createOrg from '../../test/fixtureHelpers/createOrg.js'; 8import createRule from '../../test/fixtureHelpers/createRule.js'; 9import createUser from '../../test/fixtureHelpers/createUser.js'; 10import { type Mocked } from '../../test/mockHelpers/jestMocks.js'; 11import { RuleAlarmStatus } from '../moderationConfigService/index.js'; 12import DetectRulePassRateAnomaliesJob from './detectRulePassRateAnomaliesJob.js'; 13 14function makeMockKyselyForRules( 15 fakeRules: Array<{ 16 id: string; 17 orgId: string; 18 creatorId: string; 19 name: string; 20 alarmStatus: RuleAlarmStatus; 21 statusIfUnexpired: string; 22 }>, 23 orgRows: Array<{ id: string; on_call_alert_email: string | null }>, 24) { 25 const updateExecute = jest.fn().mockResolvedValue(undefined); 26 const mockDb = { 27 selectFrom: jest.fn((table: string) => { 28 const chain: { 29 select: jest.Mock; 30 where: jest.Mock; 31 execute: jest.Mock; 32 } = { 33 select: jest.fn(), 34 where: jest.fn(), 35 execute: jest.fn(), 36 }; 37 chain.select.mockReturnValue(chain); 38 chain.where.mockReturnValue(chain); 39 chain.execute.mockImplementation(async () => { 40 if (table === 'public.rules') { 41 return fakeRules.map((r) => ({ 42 id: r.id, 43 org_id: r.orgId, 44 creator_id: r.creatorId, 45 name: r.name, 46 alarm_status: r.alarmStatus, 47 status_if_unexpired: r.statusIfUnexpired, 48 })); 49 } 50 if (table === 'public.orgs') { 51 return orgRows; 52 } 53 return []; 54 }); 55 return chain; 56 }), 57 updateTable: jest.fn(() => ({ 58 set: jest.fn().mockReturnValue({ 59 where: jest.fn().mockReturnValue({ 60 execute: updateExecute, 61 }), 62 }), 63 })), 64 __updateExecute: updateExecute, 65 }; 66 return mockDb; 67} 68 69describe('Detect Rule Anomalies', () => { 70 describe('worker', () => { 71 let deleteMockData: () => Promise<void>, 72 mockDummyRules: Array<{ 73 id: string; 74 orgId: string; 75 creatorId: string; 76 name: string; 77 alarmStatus: RuleAlarmStatus; 78 statusIfUnexpired: string; 79 }>, 80 mockKysely: ReturnType<typeof makeMockKyselyForRules>, 81 mockGetCurrentPeriodRuleAlarmStatuses: GetCurrentPeriodRuleAlarmStatuses, 82 mockNotificationsService: Mocked< 83 PublicInterface<NotificationsService>, 84 'createNotifications' 85 >; 86 87 beforeAll(async () => { 88 /* eslint-disable functional/immutable-data */ 89 const { 90 Sequelize: models, 91 ModerationConfigService, 92 ApiKeyService, 93 KyselyPg, 94 } = (await getBottle()).container; 95 96 // make some fake rules (w/ stable ids so we can match them in a snapshot) 97 // in different initial alarm statuses, to test all 9 combinations [i.e., 98 // starting and ending at one of (OK, ALARM, or INSUFFICENT_DATA), where 99 // the start and end states can be the same]. 100 const { org, cleanup: orgCleanup } = await createOrg({ 101 KyselyPg, 102 ModerationConfigService, 103 ApiKeyService, 104 }); 105 const { org: org2, cleanup: org2Cleanup } = await createOrg( 106 { 107 KyselyPg, 108 ModerationConfigService, 109 ApiKeyService, 110 }, 111 undefined, 112 { onCallAlertEmail: 'test@gmail.com' }, 113 ); 114 const { user: ruleOwner } = await createUser(models, org.id, { 115 id: 'cb34377bcc3', 116 }); 117 const { user: ruleOwner2 } = await createUser(models, org.id, { 118 id: 'cb34377bcc4', 119 }); 120 const fakeRules = await Promise.all([ 121 createRule(models, org.id, { 122 alarmStatus: RuleAlarmStatus.ALARM, 123 id: '9d237a650c1', 124 creator: ruleOwner, 125 }), 126 createRule(models, org.id, { 127 alarmStatus: RuleAlarmStatus.ALARM, 128 id: '386da8abc3b', 129 creator: ruleOwner, 130 }), 131 createRule(models, org.id, { 132 alarmStatus: RuleAlarmStatus.ALARM, 133 id: 'd237a650c13', 134 creator: ruleOwner, 135 }), 136 137 createRule(models, org.id, { 138 alarmStatus: RuleAlarmStatus.OK, 139 id: '86da8abc3b6', 140 creator: ruleOwner, 141 }), 142 createRule(models, org.id, { 143 alarmStatus: RuleAlarmStatus.OK, 144 id: 'fdb4ee86f93', 145 creator: ruleOwner, 146 }), 147 createRule(models, org.id, { 148 alarmStatus: RuleAlarmStatus.OK, 149 id: '237a650c134', 150 creator: ruleOwner, 151 }), 152 153 createRule(models, org2.id, { 154 alarmStatus: RuleAlarmStatus.INSUFFICIENT_DATA, 155 id: 'db4ee86f938', 156 creator: ruleOwner2, 157 }), 158 createRule(models, org2.id, { 159 alarmStatus: RuleAlarmStatus.INSUFFICIENT_DATA, 160 id: '37a650c1342', 161 creator: ruleOwner2, 162 }), 163 createRule(models, org2.id, { 164 alarmStatus: RuleAlarmStatus.INSUFFICIENT_DATA, 165 id: 'b4ee86f9386', 166 creator: ruleOwner2, 167 }), 168 ]); 169 170 mockDummyRules = fakeRules.map((r) => ({ 171 id: r.id, 172 orgId: r.orgId, 173 creatorId: r.creatorId, 174 name: r.name, 175 alarmStatus: r.alarmStatus, 176 statusIfUnexpired: r.statusIfUnexpired, 177 })); 178 179 mockGetCurrentPeriodRuleAlarmStatuses = async () => { 180 const newAlarmStatusByRule = 181 // eslint-disable-next-line @typescript-eslint/consistent-type-assertions 182 {} as Awaited<ReturnType<GetCurrentPeriodRuleAlarmStatuses>>; 183 184 fakeRules.forEach((rule, i) => { 185 newAlarmStatusByRule[rule.id] = { 186 status: 187 i % 3 === 0 188 ? RuleAlarmStatus.ALARM 189 : i % 3 === 1 190 ? RuleAlarmStatus.OK 191 : RuleAlarmStatus.INSUFFICIENT_DATA, 192 meta: { lastPeriodPassRate: 0.5, secondToLastPeriodPassRate: 0.4 }, 193 }; 194 }); 195 return newAlarmStatusByRule; 196 }; 197 198 mockNotificationsService = { 199 createNotifications: jest.fn(), 200 getNotificationsForUser: jest.fn(), 201 } as unknown as Mocked< 202 PublicInterface<NotificationsService>, 203 'createNotifications' 204 >; 205 206 mockKysely = makeMockKyselyForRules(mockDummyRules, [ 207 { id: org.id, on_call_alert_email: null }, 208 { id: org2.id, on_call_alert_email: 'test@gmail.com' }, 209 ]); 210 211 deleteMockData = async () => { 212 await Promise.all(fakeRules.map(async (it) => it.destroy())); 213 await Promise.all([ruleOwner.destroy(), ruleOwner2.destroy()]); 214 await orgCleanup(); 215 await org2Cleanup(); 216 await models.sequelize.close(); 217 }; 218 /* eslint-enable functional/immutable-data */ 219 }); 220 221 afterAll(async () => { 222 return deleteMockData(); 223 }); 224 225 test('should generate the proper notifications + update rules', async () => { 226 const worker = DetectRulePassRateAnomaliesJob( 227 mockKysely as unknown as Dependencies['KyselyPg'], 228 mockNotificationsService, 229 mockGetCurrentPeriodRuleAlarmStatuses, 230 jest.fn<() => Promise<void>>(), 231 ); 232 await worker.run(); 233 234 const mockCreateNotifications = 235 mockNotificationsService.createNotifications; 236 237 expect(mockCreateNotifications).toHaveBeenCalledTimes(1); 238 expect( 239 mockCreateNotifications.mock.calls[0][0] 240 .slice(0) 241 .sort((a, b) => a.data.ruleId.localeCompare(b.data.ruleId)), 242 ).toMatchInlineSnapshot(` 243 [ 244 { 245 "data": { 246 "lastPeriodPassRate": 0.5, 247 "ruleId": "386da8abc3b", 248 "ruleName": "Dummy_Rule_Name_386da8abc3b", 249 "secondToLastPeriodPassRate": 0.4, 250 }, 251 "message": "[Alarm Cleared - Live Rule] Dummy_Rule_Name_386da8abc3b has stopped passing at an anomalous rate.", 252 "recipients": [ 253 { 254 "type": "user_id", 255 "value": "cb34377bcc3", 256 }, 257 ], 258 "type": "RULE_PASS_RATE_INCREASE_ANOMALY_END", 259 }, 260 { 261 "data": { 262 "lastPeriodPassRate": 0.5, 263 "ruleId": "86da8abc3b6", 264 "ruleName": "Dummy_Rule_Name_86da8abc3b6", 265 "secondToLastPeriodPassRate": 0.4, 266 }, 267 "message": "[Alarm Triggered - Live Rule] Dummy_Rule_Name_86da8abc3b6 has started passing at an anomalous rate.", 268 "recipients": [ 269 { 270 "type": "user_id", 271 "value": "cb34377bcc3", 272 }, 273 ], 274 "type": "RULE_PASS_RATE_INCREASE_ANOMALY_START", 275 }, 276 { 277 "data": { 278 "lastPeriodPassRate": 0.5, 279 "ruleId": "d237a650c13", 280 "ruleName": "Dummy_Rule_Name_d237a650c13", 281 "secondToLastPeriodPassRate": 0.4, 282 }, 283 "message": "[Alarm Cleared - Live Rule] Dummy_Rule_Name_d237a650c13 has stopped passing at an anomalous rate.", 284 "recipients": [ 285 { 286 "type": "user_id", 287 "value": "cb34377bcc3", 288 }, 289 ], 290 "type": "RULE_PASS_RATE_INCREASE_ANOMALY_END", 291 }, 292 { 293 "data": { 294 "lastPeriodPassRate": 0.5, 295 "ruleId": "db4ee86f938", 296 "ruleName": "Dummy_Rule_Name_db4ee86f938", 297 "secondToLastPeriodPassRate": 0.4, 298 }, 299 "message": "[Alarm Triggered - Live Rule] Dummy_Rule_Name_db4ee86f938 has started passing at an anomalous rate.", 300 "recipients": [ 301 { 302 "type": "user_id", 303 "value": "cb34377bcc4", 304 }, 305 { 306 "type": "email_address", 307 "value": "test@gmail.com", 308 }, 309 ], 310 "type": "RULE_PASS_RATE_INCREASE_ANOMALY_START", 311 }, 312 ] 313 `); 314 315 await mockGetCurrentPeriodRuleAlarmStatuses(); 316 const expectedUpdates = mockDummyRules.filter( 317 (_rule, i) => ![0, 4, 8].includes(i), 318 ).length; 319 expect(mockKysely.updateTable).toHaveBeenCalledTimes(expectedUpdates); 320 }); 321 }); 322});