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

Configure Feed

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

at 557ff54b2b435e5f1e789c6a8a4e1bebf2d7deb6 267 lines 9.9 kB view raw
1import _ from 'lodash'; 2 3import getBottle, { 4 type Dependencies, 5 type PublicInterface, 6} from '../../iocContainer/index.js'; 7import { type Rule as TRule } from '../../models/rules/RuleModel.js'; 8import { type NotificationsService } from '../../services/notificationsService/notificationsService.js'; 9import { type GetCurrentPeriodRuleAlarmStatuses } from '../../services/ruleAnomalyDetectionService/getCurrentPeriodRuleAlarmStatuses.js'; 10import createOrg from '../../test/fixtureHelpers/createOrg.js'; 11import createRule from '../../test/fixtureHelpers/createRule.js'; 12import createUser from '../../test/fixtureHelpers/createUser.js'; 13import { mocked, type Mocked } from '../../test/mockHelpers/jestMocks.js'; 14import { RuleAlarmStatus } from '../moderationConfigService/index.js'; 15import DetectRulePassRateAnomaliesJob from './detectRulePassRateAnomaliesJob.js'; 16 17describe('Detect Rule Anomalies', () => { 18 describe('worker', () => { 19 let OrgModel: Dependencies['Sequelize']['Org'], 20 deleteMockData: () => Promise<void>, 21 mockDummyRules: Mocked<TRule, 'save'>[], 22 mockRuleModel: Mocked<Dependencies['Sequelize']['Rule'], 'findAll'>, 23 mockGetCurrentPeriodRuleAlarmStatuses: GetCurrentPeriodRuleAlarmStatuses, 24 mockNotificationsService: Mocked< 25 PublicInterface<NotificationsService>, 26 'createNotifications' 27 >; 28 29 beforeAll(async () => { 30 /* eslint-disable better-mutation/no-mutation */ 31 const { 32 Sequelize: models, 33 ModerationConfigService, 34 ApiKeyService, 35 } = (await getBottle()).container; 36 OrgModel = models.Org; 37 38 // make some fake rules (w/ stable ids so we can match them in a snapshot) 39 // in different initial alarm statuses, to test all 9 combinations [i.e., 40 // starting and ending at one of (OK, ALARM, or INSUFFICENT_DATA), where 41 // the start and end states can be the same]. 42 const { org } = await createOrg(models, ModerationConfigService, ApiKeyService); 43 const { org: org2 } = await createOrg( 44 models, 45 ModerationConfigService, 46 ApiKeyService, 47 undefined, 48 { onCallAlertEmail: 'test@gmail.com' }, 49 ); 50 const { user: ruleOwner } = await createUser(models, org.id, { 51 id: 'cb34377bcc3', 52 }); 53 const { user: ruleOwner2 } = await createUser(models, org.id, { 54 id: 'cb34377bcc4', 55 }); 56 const fakeRules = await Promise.all([ 57 createRule(models, org.id, { 58 alarmStatus: RuleAlarmStatus.ALARM, 59 id: '9d237a650c1', 60 creator: ruleOwner, 61 }), 62 createRule(models, org.id, { 63 alarmStatus: RuleAlarmStatus.ALARM, 64 id: '386da8abc3b', 65 creator: ruleOwner, 66 }), 67 createRule(models, org.id, { 68 alarmStatus: RuleAlarmStatus.ALARM, 69 id: 'd237a650c13', 70 creator: ruleOwner, 71 }), 72 73 createRule(models, org.id, { 74 alarmStatus: RuleAlarmStatus.OK, 75 id: '86da8abc3b6', 76 creator: ruleOwner, 77 }), 78 createRule(models, org.id, { 79 alarmStatus: RuleAlarmStatus.OK, 80 id: 'fdb4ee86f93', 81 creator: ruleOwner, 82 }), 83 createRule(models, org.id, { 84 alarmStatus: RuleAlarmStatus.OK, 85 id: '237a650c134', 86 creator: ruleOwner, 87 }), 88 89 createRule(models, org2.id, { 90 alarmStatus: RuleAlarmStatus.INSUFFICIENT_DATA, 91 id: 'db4ee86f938', 92 creator: ruleOwner2, 93 }), 94 createRule(models, org2.id, { 95 alarmStatus: RuleAlarmStatus.INSUFFICIENT_DATA, 96 id: '37a650c1342', 97 creator: ruleOwner2, 98 }), 99 createRule(models, org2.id, { 100 alarmStatus: RuleAlarmStatus.INSUFFICIENT_DATA, 101 id: 'b4ee86f9386', 102 creator: ruleOwner2, 103 }), 104 ]); 105 106 mockDummyRules = fakeRules.map((it) => mocked(it, ['save'])); 107 mockGetCurrentPeriodRuleAlarmStatuses = async () => { 108 const newAlarmStatusByRule = 109 // eslint-disable-next-line @typescript-eslint/consistent-type-assertions 110 {} as Awaited<ReturnType<GetCurrentPeriodRuleAlarmStatuses>>; 111 112 fakeRules.forEach((rule, i) => { 113 newAlarmStatusByRule[rule.id] = { 114 status: 115 i % 3 === 0 116 ? RuleAlarmStatus.ALARM 117 : i % 3 === 1 118 ? RuleAlarmStatus.OK 119 : RuleAlarmStatus.INSUFFICIENT_DATA, 120 meta: { lastPeriodPassRate: 0.5, secondToLastPeriodPassRate: 0.4 }, 121 }; 122 }); 123 return newAlarmStatusByRule; 124 }; 125 126 mockNotificationsService = mocked( 127 { 128 async createNotifications(_it: any) {}, 129 async getNotificationsForUser() { 130 return []; 131 }, 132 }, 133 ['createNotifications'], 134 ); 135 136 mockRuleModel = mocked(models.Rule, ['findAll']); 137 mockRuleModel.findAll.mockResolvedValue(mockDummyRules); 138 139 // It might be nice if we could create the mock data at the start of a 140 // transaction, run the tests with that transaction open, and then just 141 // roll it back at the end to automatically delete and leave the db in a 142 // consistent/clean state. That's a little tricky, though, as it requires 143 // feeding the transacation object (or keeping a managed transaction 144 // callback open) all the way into calling the worker. So, instead, we 145 // settle for manually defining this compensating transacaction, which we 146 // call at the end. 147 deleteMockData = async () => { 148 await Promise.all(fakeRules.map(async (it) => it.destroy())); 149 await Promise.all([ruleOwner.destroy(), ruleOwner2.destroy()]); 150 await Promise.all([org.destroy(), org2.destroy()]); 151 await models.sequelize.close(); 152 }; 153 /* eslint-enable better-mutation/no-mutation */ 154 }); 155 156 afterAll(async () => { 157 return deleteMockData(); 158 }); 159 160 test('should generate the proper notifications + update rules', async () => { 161 const worker = DetectRulePassRateAnomaliesJob( 162 mockRuleModel, 163 OrgModel, 164 mockNotificationsService, 165 mockGetCurrentPeriodRuleAlarmStatuses, 166 jest.fn<() => Promise<void>>(), 167 ); 168 await worker.run(); 169 170 // We should've sent 4 notifications: one for each of the two rules that 171 // was in alarm and transitioned to 'not alarm' (ok or insufficient data), 172 // and one for each of the rules that was in 'not alarm' and went to alarm. 173 const mockCreateNotifications = 174 mockNotificationsService.createNotifications; 175 176 expect(mockCreateNotifications).toHaveBeenCalledTimes(1); 177 expect( 178 mockCreateNotifications.mock.calls[0][0] 179 .slice(0) 180 .sort((a, b) => a.data.ruleId.localeCompare(b.data.ruleId)), 181 ).toMatchInlineSnapshot(` 182 [ 183 { 184 "data": { 185 "lastPeriodPassRate": 0.5, 186 "ruleId": "386da8abc3b", 187 "ruleName": "Dummy_Rule_Name_386da8abc3b", 188 "secondToLastPeriodPassRate": 0.4, 189 }, 190 "message": "[Alarm Cleared - Live Rule] Dummy_Rule_Name_386da8abc3b has stopped passing at an anomalous rate.", 191 "recipients": [ 192 { 193 "type": "user_id", 194 "value": "cb34377bcc3", 195 }, 196 ], 197 "type": "RULE_PASS_RATE_INCREASE_ANOMALY_END", 198 }, 199 { 200 "data": { 201 "lastPeriodPassRate": 0.5, 202 "ruleId": "86da8abc3b6", 203 "ruleName": "Dummy_Rule_Name_86da8abc3b6", 204 "secondToLastPeriodPassRate": 0.4, 205 }, 206 "message": "[Alarm Triggered - Live Rule] Dummy_Rule_Name_86da8abc3b6 has started passing at an anomalous rate.", 207 "recipients": [ 208 { 209 "type": "user_id", 210 "value": "cb34377bcc3", 211 }, 212 ], 213 "type": "RULE_PASS_RATE_INCREASE_ANOMALY_START", 214 }, 215 { 216 "data": { 217 "lastPeriodPassRate": 0.5, 218 "ruleId": "d237a650c13", 219 "ruleName": "Dummy_Rule_Name_d237a650c13", 220 "secondToLastPeriodPassRate": 0.4, 221 }, 222 "message": "[Alarm Cleared - Live Rule] Dummy_Rule_Name_d237a650c13 has stopped passing at an anomalous rate.", 223 "recipients": [ 224 { 225 "type": "user_id", 226 "value": "cb34377bcc3", 227 }, 228 ], 229 "type": "RULE_PASS_RATE_INCREASE_ANOMALY_END", 230 }, 231 { 232 "data": { 233 "lastPeriodPassRate": 0.5, 234 "ruleId": "db4ee86f938", 235 "ruleName": "Dummy_Rule_Name_db4ee86f938", 236 "secondToLastPeriodPassRate": 0.4, 237 }, 238 "message": "[Alarm Triggered - Live Rule] Dummy_Rule_Name_db4ee86f938 has started passing at an anomalous rate.", 239 "recipients": [ 240 { 241 "type": "user_id", 242 "value": "cb34377bcc4", 243 }, 244 { 245 "type": "email_address", 246 "value": "test@gmail.com", 247 }, 248 ], 249 "type": "RULE_PASS_RATE_INCREASE_ANOMALY_START", 250 }, 251 ] 252 `); 253 254 // Except for the rules that stayed the same state (indexes 0, 4, 8), all 255 // the rules should've been saved with their new state. 256 const newRuleStatuses = await mockGetCurrentPeriodRuleAlarmStatuses(); 257 mockDummyRules.forEach((rule, i) => { 258 if (![0, 4, 8].includes(i)) { 259 expect(rule.save).toHaveBeenCalledTimes(1); 260 expect(rule.alarmStatus).toEqual(newRuleStatuses[rule.id].status); 261 } else { 262 expect(rule.save).toHaveBeenCalledTimes(0); 263 } 264 }); 265 }); 266 }); 267});