@oneuptime/common 12.0.13 → 12.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (376) hide show
  1. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +14 -0
  2. package/Models/DatabaseModels/UserCall.ts +94 -3
  3. package/Models/DatabaseModels/UserEmail.ts +94 -3
  4. package/Models/DatabaseModels/UserIncomingCallNumber.ts +94 -3
  5. package/Models/DatabaseModels/UserPush.ts +37 -0
  6. package/Models/DatabaseModels/UserSMS.ts +94 -3
  7. package/Models/DatabaseModels/UserWhatsApp.ts +93 -2
  8. package/Server/API/BaseAPI.ts +79 -14
  9. package/Server/API/UserCallAPI.ts +53 -45
  10. package/Server/API/UserEmailAPI.ts +53 -44
  11. package/Server/API/UserIncomingCallNumberAPI.ts +48 -38
  12. package/Server/API/UserNotificationMethodAdminAPI.ts +481 -0
  13. package/Server/API/UserPushAPI.ts +126 -2
  14. package/Server/API/UserSmsAPI.ts +53 -44
  15. package/Server/API/UserWhatsAppAPI.ts +53 -50
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.ts +154 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.ts +17 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.ts +45 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  20. package/Server/Middleware/VerificationCodeRateLimit.ts +586 -0
  21. package/Server/Services/AnalyticsDatabaseService.ts +169 -0
  22. package/Server/Services/DatabaseService.ts +101 -0
  23. package/Server/Services/LogAggregationService.ts +5 -0
  24. package/Server/Services/PushNotificationService.ts +127 -12
  25. package/Server/Services/TeamMemberService.ts +53 -13
  26. package/Server/Services/UserCallService.ts +55 -24
  27. package/Server/Services/UserEmailService.ts +83 -17
  28. package/Server/Services/UserIncomingCallNumberService.ts +49 -85
  29. package/Server/Services/UserNotificationMethodAdminService.ts +1527 -0
  30. package/Server/Services/UserNotificationRuleService.ts +26 -0
  31. package/Server/Services/UserPushService.ts +89 -0
  32. package/Server/Services/UserSmsService.ts +50 -17
  33. package/Server/Services/UserWhatsAppService.ts +48 -15
  34. package/Server/Types/Database/QueryHelper.ts +30 -0
  35. package/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.ts +59 -0
  36. package/Server/Utils/ChannelVerification.ts +510 -0
  37. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +13 -16
  38. package/Server/Utils/Monitor/MonitorAlert.ts +24 -21
  39. package/Server/Utils/Monitor/MonitorIncident.ts +21 -18
  40. package/Server/Utils/Monitor/MonitorResourceContext.ts +107 -0
  41. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +442 -0
  42. package/Server/Utils/Monitor/SeriesResourceLabels.ts +3 -3
  43. package/Server/Utils/Monitor/SeriesResourceLinker.ts +96 -28
  44. package/Server/Utils/TotpAuth.ts +137 -9
  45. package/Server/Utils/UserRegistrationToken.ts +182 -0
  46. package/Server/Utils/VerificationCode.ts +134 -0
  47. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +173 -94
  48. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +1151 -0
  49. package/Tests/App/Dashboard/AdminUserOnCallPages.test.tsx +905 -0
  50. package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +338 -0
  51. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +1 -1
  52. package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +28 -0
  53. package/Tests/Models/UserPushCriticalAlertColumn.test.ts +172 -0
  54. package/Tests/Server/API/BaseAPIGetListParallel.test.ts +519 -0
  55. package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +543 -0
  56. package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +108 -18
  57. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +963 -0
  58. package/Tests/Server/API/UserPushCriticalAlertsApi.test.ts +114 -0
  59. package/Tests/Server/API/UserSmsApi.test.ts +107 -14
  60. package/Tests/Server/API/UserTotpAuthAPI.test.ts +427 -0
  61. package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +315 -0
  62. package/Tests/Server/Middleware/VerificationCodeRateLimit.test.ts +775 -0
  63. package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +10 -62
  64. package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +33 -13
  65. package/Tests/Server/Services/AnalyticsDatabaseSortKeyBoundary.test.ts +658 -0
  66. package/Tests/Server/Services/CriticalOnCallAlertDelivery.test.ts +458 -0
  67. package/Tests/Server/Services/CriticalPushAlertPayload.test.ts +301 -0
  68. package/Tests/Server/Services/DatabaseServiceAtomicIncrement.test.ts +143 -0
  69. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1 -0
  70. package/Tests/Server/Services/LogAggregationScanMemory.test.ts +421 -0
  71. package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
  72. package/Tests/Server/Services/NotificationChannelCodeIssuance.test.ts +401 -0
  73. package/Tests/Server/Services/PushNotificationDeliveryOptions.test.ts +204 -0
  74. package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +21 -1
  75. package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +290 -0
  76. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +948 -0
  77. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1 -0
  78. package/Tests/Server/Services/UserPushCriticalAlertToggle.test.ts +255 -0
  79. package/Tests/Server/Services/UserTotpAuthEnrolment.test.ts +218 -0
  80. package/Tests/Server/TestingUtils/AuthenticatorApp.ts +248 -0
  81. package/Tests/Server/Types/Database/Permissions/NotificationChannelVerificationColumns.test.ts +135 -0
  82. package/Tests/Server/Types/Database/Permissions/OnCallScheduleRelationSelect.test.ts +504 -0
  83. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAnyOf.test.ts +91 -0
  84. package/Tests/Server/Types/Workflow/Components/JsonToText.test.ts +384 -0
  85. package/Tests/Server/Types/Workflow/Components/MergeJson.test.ts +403 -0
  86. package/Tests/Server/Utils/Browser.test.ts +162 -0
  87. package/Tests/Server/Utils/ChannelVerification.test.ts +689 -0
  88. package/Tests/Server/Utils/Express.test.ts +371 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/CustomCodeMonitorCriteria.test.ts +707 -0
  90. package/Tests/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.test.ts +698 -0
  91. package/Tests/Server/Utils/Monitor/Criteria/IncomingEmailBodyCriteria.test.ts +243 -0
  92. package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestBodyCriteria.test.ts +278 -0
  93. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +160 -20
  94. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +13 -8
  95. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +162 -18
  96. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +349 -0
  97. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +690 -0
  98. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +7 -2
  99. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +92 -18
  100. package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +267 -0
  101. package/Tests/Server/Utils/Telemetry/OneuptimeLabel.test.ts +439 -0
  102. package/Tests/Server/Utils/TotpAuth.test.ts +719 -0
  103. package/Tests/Server/Utils/UserRegistrationToken.test.ts +322 -0
  104. package/Tests/Server/Utils/VerificationCode.test.ts +296 -0
  105. package/Tests/Types/AutoRemediation/AutoRemediationSuggestionStatus.test.ts +192 -0
  106. package/Tests/Types/Call/CallRequest.test.ts +189 -0
  107. package/Tests/Types/Dashboard/DashboardSize.test.ts +226 -0
  108. package/Tests/Types/Incident/IncidentSlaStatus.test.ts +191 -0
  109. package/Tests/Types/Metrics/MetricDashboardMetricType.test.ts +278 -0
  110. package/Tests/Types/Metrics/MetricPipelineRuleType.test.ts +517 -0
  111. package/Tests/Types/Monitor/IncomingMonitorDefaultCriteria.test.ts +470 -0
  112. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +6 -4
  113. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +262 -5
  114. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +1 -1
  115. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +1 -1
  116. package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +905 -0
  117. package/Tests/Types/Service/ServiceLanguage.test.ts +255 -0
  118. package/Tests/Types/TextRandomGeneration.test.ts +211 -0
  119. package/Tests/UI/Components/Button.test.tsx +175 -1
  120. package/Tests/UI/Components/CodeBlockLanguages.test.tsx +236 -0
  121. package/Tests/UI/Components/CommandPalette/CommandPalette.test.tsx +568 -0
  122. package/Tests/UI/Components/CommandPalette/CommandPaletteKeyboard.test.tsx +239 -0
  123. package/Tests/UI/Components/CommandPalette/CommandPaletteProviders.test.tsx +400 -0
  124. package/Tests/UI/Components/CommandPalette/PaletteFilter.test.ts +234 -0
  125. package/Tests/UI/Components/CommandPalette/RecentCommands.test.ts +97 -0
  126. package/Tests/UI/Components/CountModelSideMenuItemGuard.test.tsx +198 -0
  127. package/Tests/UI/Components/FeedItemSafeMode.test.tsx +1 -1
  128. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +267 -0
  129. package/Tests/UI/Components/List.test.tsx +9 -2
  130. package/Tests/UI/Components/ListLoadingStates.test.tsx +173 -0
  131. package/Tests/UI/Components/MarkdownLazy.test.tsx +113 -0
  132. package/Tests/UI/Components/MarkdownMermaidRetry.test.tsx +110 -0
  133. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +8 -0
  134. package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +692 -0
  135. package/Tests/UI/Components/MoreMenuMotion.test.tsx +118 -0
  136. package/Tests/UI/Components/NavBarCommandKGating.test.tsx +178 -0
  137. package/Tests/UI/Components/OrderedStatesList.test.tsx +19 -2
  138. package/Tests/UI/Components/SideOverMotion.test.tsx +109 -0
  139. package/Tests/UI/Components/Skeleton.test.tsx +71 -0
  140. package/Tests/UI/Components/TableLoadingStates.test.tsx +279 -0
  141. package/Tests/UI/Components/Tabs.test.tsx +24 -0
  142. package/Tests/UI/Components/Toast.test.tsx +130 -7
  143. package/Tests/UI/Components/ToastStacking.test.tsx +198 -0
  144. package/Tests/UI/Utils/Dropdown.test.ts +423 -0
  145. package/Tests/UI/Utils/ModelAPITenantHeader.test.ts +122 -0
  146. package/Tests/UI/Utils/ModelListCache.test.ts +338 -0
  147. package/Tests/UI/Utils/ProjectListCacheInvalidation.test.ts +225 -0
  148. package/Tests/Utils/API.test.ts +16 -2
  149. package/Tests/Utils/Dashboard/Components/DashboardKubernetesResourceListShared.test.ts +347 -0
  150. package/Tests/Utils/Dashboard/Components/DashboardListSharedArgs.test.ts +259 -0
  151. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +466 -0
  152. package/Tests/Utils/Rum/UrlScrubberSegments.test.ts +362 -0
  153. package/Types/Email/EmailTemplateType.ts +1 -0
  154. package/Types/Monitor/MonitorCriteriaInstance.ts +41 -20
  155. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +131 -8
  156. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +48 -8
  157. package/Types/Monitor/ServiceAlertTemplates.ts +1352 -0
  158. package/Types/Permission.ts +35 -0
  159. package/Types/PushNotification/AndroidNotificationChannel.ts +26 -0
  160. package/Types/PushNotification/PushNotificationMessage.ts +15 -0
  161. package/Types/PushNotification/PushNotificationRequest.ts +8 -16
  162. package/Types/Service/ServiceLanguage.ts +144 -0
  163. package/Types/Text.ts +111 -19
  164. package/UI/Components/Button/Button.tsx +27 -14
  165. package/UI/Components/CodeBlock/CodeBlock.tsx +54 -7
  166. package/UI/Components/CodeBlock/LanguageRegistry.ts +103 -0
  167. package/UI/Components/CommandPalette/CommandPalette.tsx +790 -0
  168. package/UI/Components/CommandPalette/PaletteFilter.ts +196 -0
  169. package/UI/Components/CommandPalette/PaletteRow.tsx +173 -0
  170. package/UI/Components/CommandPalette/RecentCommands.ts +57 -0
  171. package/UI/Components/CommandPalette/Types.ts +49 -0
  172. package/UI/Components/CommandPalette/UseProviderSearch.ts +129 -0
  173. package/UI/Components/Dropdown/Dropdown.tsx +6 -1
  174. package/UI/Components/Feed/FeedItem.tsx +1 -1
  175. package/UI/Components/List/List.tsx +37 -5
  176. package/UI/Components/List/ListSkeleton.tsx +54 -0
  177. package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +6 -1
  178. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +71 -26
  179. package/UI/Components/Modal/Modal.tsx +2 -2
  180. package/UI/Components/ModelTable/BaseModelTable.tsx +1 -1
  181. package/UI/Components/ModelTable/useCustomFieldColumns.ts +11 -5
  182. package/UI/Components/MoreMenu/MoreMenu.tsx +39 -1
  183. package/UI/Components/Navbar/NavBar.tsx +14 -1
  184. package/UI/Components/OrderedStatesList/OrderedStatesList.tsx +50 -5
  185. package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +40 -1
  186. package/UI/Components/SideOver/SideOver.tsx +37 -2
  187. package/UI/Components/Skeleton/Skeleton.tsx +53 -0
  188. package/UI/Components/Table/Table.tsx +74 -44
  189. package/UI/Components/Table/TableSkeletonRows.tsx +141 -0
  190. package/UI/Components/Tabs/Tab.tsx +1 -1
  191. package/UI/Components/Toast/Toast.tsx +118 -53
  192. package/UI/Components/Toast/ToastInit.tsx +82 -32
  193. package/UI/Styles/Theme.css +40 -0
  194. package/UI/Utils/ModelAPI/ModelAPI.ts +20 -10
  195. package/UI/Utils/ModelListCache.ts +188 -0
  196. package/UI/Utils/Project.ts +29 -0
  197. package/Utils/API.ts +6 -1
  198. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +14 -0
  199. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  200. package/build/dist/Models/DatabaseModels/UserCall.js +101 -3
  201. package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
  202. package/build/dist/Models/DatabaseModels/UserEmail.js +101 -3
  203. package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
  204. package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js +101 -3
  205. package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js.map +1 -1
  206. package/build/dist/Models/DatabaseModels/UserPush.js +38 -0
  207. package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
  208. package/build/dist/Models/DatabaseModels/UserSMS.js +101 -3
  209. package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
  210. package/build/dist/Models/DatabaseModels/UserWhatsApp.js +100 -2
  211. package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
  212. package/build/dist/Server/API/BaseAPI.js +69 -13
  213. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  214. package/build/dist/Server/API/UserCallAPI.js +42 -38
  215. package/build/dist/Server/API/UserCallAPI.js.map +1 -1
  216. package/build/dist/Server/API/UserEmailAPI.js +42 -38
  217. package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
  218. package/build/dist/Server/API/UserIncomingCallNumberAPI.js +37 -32
  219. package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
  220. package/build/dist/Server/API/UserNotificationMethodAdminAPI.js +341 -0
  221. package/build/dist/Server/API/UserNotificationMethodAdminAPI.js.map +1 -0
  222. package/build/dist/Server/API/UserPushAPI.js +95 -2
  223. package/build/dist/Server/API/UserPushAPI.js.map +1 -1
  224. package/build/dist/Server/API/UserSmsAPI.js +42 -38
  225. package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
  226. package/build/dist/Server/API/UserWhatsAppAPI.js +42 -40
  227. package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
  228. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js +89 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js.map +1 -0
  230. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js +12 -0
  231. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js.map +1 -0
  232. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js +40 -0
  233. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js.map +1 -0
  234. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  235. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  236. package/build/dist/Server/Middleware/VerificationCodeRateLimit.js +393 -0
  237. package/build/dist/Server/Middleware/VerificationCodeRateLimit.js.map +1 -0
  238. package/build/dist/Server/Services/AnalyticsDatabaseService.js +136 -0
  239. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  240. package/build/dist/Server/Services/DatabaseService.js +74 -0
  241. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  242. package/build/dist/Server/Services/LogAggregationService.js +5 -0
  243. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  244. package/build/dist/Server/Services/PushNotificationService.js +69 -30
  245. package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
  246. package/build/dist/Server/Services/TeamMemberService.js +43 -7
  247. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  248. package/build/dist/Server/Services/UserCallService.js +54 -22
  249. package/build/dist/Server/Services/UserCallService.js.map +1 -1
  250. package/build/dist/Server/Services/UserEmailService.js +78 -17
  251. package/build/dist/Server/Services/UserEmailService.js.map +1 -1
  252. package/build/dist/Server/Services/UserIncomingCallNumberService.js +45 -74
  253. package/build/dist/Server/Services/UserIncomingCallNumberService.js.map +1 -1
  254. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1117 -0
  255. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -0
  256. package/build/dist/Server/Services/UserNotificationRuleService.js +20 -0
  257. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  258. package/build/dist/Server/Services/UserPushService.js +77 -0
  259. package/build/dist/Server/Services/UserPushService.js.map +1 -1
  260. package/build/dist/Server/Services/UserSmsService.js +50 -17
  261. package/build/dist/Server/Services/UserSmsService.js.map +1 -1
  262. package/build/dist/Server/Services/UserWhatsAppService.js +48 -15
  263. package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
  264. package/build/dist/Server/Types/Database/QueryHelper.js +27 -0
  265. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  266. package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js +46 -0
  267. package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js.map +1 -1
  268. package/build/dist/Server/Utils/ChannelVerification.js +348 -0
  269. package/build/dist/Server/Utils/ChannelVerification.js.map +1 -0
  270. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +13 -12
  271. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  272. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +20 -17
  273. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  274. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +17 -14
  275. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  276. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +95 -0
  277. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -0
  278. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +313 -0
  279. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -0
  280. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +3 -3
  281. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +67 -26
  282. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  283. package/build/dist/Server/Utils/TotpAuth.js +127 -8
  284. package/build/dist/Server/Utils/TotpAuth.js.map +1 -1
  285. package/build/dist/Server/Utils/UserRegistrationToken.js +170 -0
  286. package/build/dist/Server/Utils/UserRegistrationToken.js.map +1 -0
  287. package/build/dist/Server/Utils/VerificationCode.js +120 -0
  288. package/build/dist/Server/Utils/VerificationCode.js.map +1 -0
  289. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  290. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  291. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +42 -21
  292. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  293. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +77 -5
  294. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  295. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +18 -6
  296. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  297. package/build/dist/Types/Monitor/ServiceAlertTemplates.js +1028 -0
  298. package/build/dist/Types/Monitor/ServiceAlertTemplates.js.map +1 -0
  299. package/build/dist/Types/Permission.js +33 -0
  300. package/build/dist/Types/Permission.js.map +1 -1
  301. package/build/dist/Types/PushNotification/AndroidNotificationChannel.js +27 -0
  302. package/build/dist/Types/PushNotification/AndroidNotificationChannel.js.map +1 -0
  303. package/build/dist/Types/Service/ServiceLanguage.js +96 -0
  304. package/build/dist/Types/Service/ServiceLanguage.js.map +1 -0
  305. package/build/dist/Types/Text.js +82 -17
  306. package/build/dist/Types/Text.js.map +1 -1
  307. package/build/dist/UI/Components/Button/Button.js +22 -14
  308. package/build/dist/UI/Components/Button/Button.js.map +1 -1
  309. package/build/dist/UI/Components/CodeBlock/CodeBlock.js +35 -3
  310. package/build/dist/UI/Components/CodeBlock/CodeBlock.js.map +1 -1
  311. package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js +57 -0
  312. package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js.map +1 -0
  313. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +439 -0
  314. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -0
  315. package/build/dist/UI/Components/CommandPalette/PaletteFilter.js +135 -0
  316. package/build/dist/UI/Components/CommandPalette/PaletteFilter.js.map +1 -0
  317. package/build/dist/UI/Components/CommandPalette/PaletteRow.js +83 -0
  318. package/build/dist/UI/Components/CommandPalette/PaletteRow.js.map +1 -0
  319. package/build/dist/UI/Components/CommandPalette/RecentCommands.js +47 -0
  320. package/build/dist/UI/Components/CommandPalette/RecentCommands.js.map +1 -0
  321. package/build/dist/UI/Components/CommandPalette/Types.js +2 -0
  322. package/build/dist/UI/Components/CommandPalette/Types.js.map +1 -0
  323. package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js +88 -0
  324. package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js.map +1 -0
  325. package/build/dist/UI/Components/Dropdown/Dropdown.js +6 -1
  326. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  327. package/build/dist/UI/Components/Feed/FeedItem.js +1 -1
  328. package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
  329. package/build/dist/UI/Components/List/List.js +19 -5
  330. package/build/dist/UI/Components/List/List.js.map +1 -1
  331. package/build/dist/UI/Components/List/ListSkeleton.js +21 -0
  332. package/build/dist/UI/Components/List/ListSkeleton.js.map +1 -0
  333. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
  334. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +52 -25
  335. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  336. package/build/dist/UI/Components/Modal/Modal.js +2 -2
  337. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +1 -1
  338. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  339. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +9 -2
  340. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  341. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +32 -1
  342. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  343. package/build/dist/UI/Components/Navbar/NavBar.js +5 -1
  344. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  345. package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js +29 -5
  346. package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js.map +1 -1
  347. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +23 -1
  348. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
  349. package/build/dist/UI/Components/SideOver/SideOver.js +23 -2
  350. package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
  351. package/build/dist/UI/Components/Skeleton/Skeleton.js +23 -0
  352. package/build/dist/UI/Components/Skeleton/Skeleton.js.map +1 -0
  353. package/build/dist/UI/Components/Table/Table.js +35 -24
  354. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  355. package/build/dist/UI/Components/Table/TableSkeletonRows.js +52 -0
  356. package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -0
  357. package/build/dist/UI/Components/Tabs/Tab.js +1 -1
  358. package/build/dist/UI/Components/Tabs/Tab.js.map +1 -1
  359. package/build/dist/UI/Components/Toast/Toast.js +77 -23
  360. package/build/dist/UI/Components/Toast/Toast.js.map +1 -1
  361. package/build/dist/UI/Components/Toast/ToastInit.js +37 -15
  362. package/build/dist/UI/Components/Toast/ToastInit.js.map +1 -1
  363. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +18 -9
  364. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  365. package/build/dist/UI/Utils/ModelListCache.js +126 -0
  366. package/build/dist/UI/Utils/ModelListCache.js.map +1 -0
  367. package/build/dist/UI/Utils/Project.js +24 -1
  368. package/build/dist/UI/Utils/Project.js.map +1 -1
  369. package/build/dist/Utils/API.js +6 -1
  370. package/build/dist/Utils/API.js.map +1 -1
  371. package/jest.config.json +1 -1
  372. package/package.json +2 -3
  373. package/Server/Utils/Monitor/MonitorClusterContext.ts +0 -182
  374. package/Tests/__mocks__/otpauth.js +0 -30
  375. package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js +0 -141
  376. package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js.map +0 -1
@@ -0,0 +1,905 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
11
+ import React, { ReactElement } from "react";
12
+ import {
13
+ Location,
14
+ MemoryRouter,
15
+ Outlet,
16
+ Route as RouterRoute,
17
+ Routes as RouterRoutes,
18
+ } from "react-router-dom";
19
+ import getJestMockFunction, { MockFunction } from "../../MockType";
20
+
21
+ /*
22
+ * THE SPLIT ITSELF.
23
+ *
24
+ * Users > View > Notification Rules was one route that rendered, in one scroll:
25
+ * a readiness summary with four stat tiles and a prose consequence, a masked
26
+ * list of the person's notification methods, a coverage grid, and four rule
27
+ * types that each expand to one card per severity band. On a project with six
28
+ * incident and six alert severities that is around fifty cards under a
29
+ * diagnosis nobody could still see by the time they reached the controls.
30
+ *
31
+ * It is six pages now, and the failures worth writing down about a page split
32
+ * are not "does the new page render" — the suites next door already assert what
33
+ * each page contains. They are the ones that make a split silently WORSE than
34
+ * what it replaced:
35
+ *
36
+ * - A DEAD BOOKMARK. The old URL is in tickets, chat scrollback and at least
37
+ * one runbook. It has to keep landing somewhere useful, and "somewhere
38
+ * useful" is the overview, not a 404 and not the middle of the rule pages.
39
+ *
40
+ * - A PAGE THAT STILL RENDERS EVERYTHING. Splitting a route four ways buys
41
+ * nothing if each of the four still mounts all four rule types. The
42
+ * assertion is per-route and counts the tables that actually mounted.
43
+ *
44
+ * - THE WRONG SEVERITY AXIS ON ONE ROUTE. A rule is tied to its band by
45
+ * `incidentSeverityId` or `alertSeverityId`, and the severity MODEL does
46
+ * not follow from the rule type the way the names suggest: an alert episode
47
+ * is banded by AlertSeverity, an incident episode by IncidentSeverity.
48
+ * Splitting four tables across four routes is exactly the edit in which one
49
+ * of them gets the other's props, and nothing throws — the table just lists
50
+ * every severity's rules at once. The two severity models here return
51
+ * DISJOINT id sets, which is the only fixture shape in which that is
52
+ * visible.
53
+ *
54
+ * - A SECTION THAT REFETCHES PER PAGE. The six pages share one layout that
55
+ * loads the target user and their readiness once. If a page reached for its
56
+ * own copy, moving between them would issue a fresh pair of reads each time
57
+ * and — much worse — two pages could disagree about WHO they are editing,
58
+ * which is the failure this whole surface is written to avoid.
59
+ *
60
+ * - AN UNREACHABLE PAGE. Six routes are worth nothing if the menu names four,
61
+ * so the side menu is asserted against the route table rather than against
62
+ * a list of strings copied out of the component.
63
+ */
64
+
65
+ const PROJECT_ID_STRING: string = "10000000-0000-4000-8000-000000000001";
66
+ const SIGNED_IN_USER_ID_STRING: string = "20000000-0000-4000-8000-000000000002";
67
+ const TARGET_USER_ID_STRING: string = "30000000-0000-4000-8000-000000000003";
68
+
69
+ const INCIDENT_SEVERITY_ONE_ID: string = "41111111-1111-4111-8111-111111111111";
70
+ const INCIDENT_SEVERITY_TWO_ID: string = "42222222-2222-4222-8222-222222222222";
71
+ const ALERT_SEVERITY_ONE_ID: string = "51111111-1111-4111-8111-111111111111";
72
+ const ALERT_SEVERITY_TWO_ID: string = "52222222-2222-4222-8222-222222222222";
73
+
74
+ const TARGET_USER_NAME: string = "Jane Ops";
75
+ const TARGET_LOGIN_EMAIL: string = "jane.ops@example.com";
76
+
77
+ const getListMock: MockFunction = getJestMockFunction();
78
+ const getItemMock: MockFunction = getJestMockFunction();
79
+ const getCommonHeadersMock: MockFunction = getJestMockFunction();
80
+ const apiGetMock: MockFunction = getJestMockFunction();
81
+ const navigateMock: MockFunction = getJestMockFunction();
82
+
83
+ let pendingRequestCount: number = 0;
84
+
85
+ type TrackRequestFunction = (result: unknown) => unknown;
86
+
87
+ const trackRequest: TrackRequestFunction = (result: unknown): unknown => {
88
+ if (!(result instanceof Promise)) {
89
+ return result;
90
+ }
91
+
92
+ pendingRequestCount++;
93
+
94
+ return result.finally((): void => {
95
+ pendingRequestCount--;
96
+ });
97
+ };
98
+
99
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
100
+ return {
101
+ __esModule: true,
102
+ default: {
103
+ getList: (...args: Array<any>) => {
104
+ return trackRequest(getListMock(...args));
105
+ },
106
+ getItem: (...args: Array<any>) => {
107
+ return trackRequest(getItemMock(...args));
108
+ },
109
+ getCommonHeaders: (...args: Array<any>) => {
110
+ return getCommonHeadersMock(...args);
111
+ },
112
+ },
113
+ };
114
+ });
115
+
116
+ jest.mock("../../../UI/Utils/API/API", () => {
117
+ return {
118
+ __esModule: true,
119
+ default: {
120
+ get: (...args: Array<any>) => {
121
+ return trackRequest(apiGetMock(...args));
122
+ },
123
+ getFriendlyMessage: (error: unknown) => {
124
+ return (
125
+ ((error as { message?: unknown } | null)?.message as string) ||
126
+ "Could not load"
127
+ );
128
+ },
129
+ getFriendlyErrorMessage: (error: unknown) => {
130
+ return (
131
+ ((error as { message?: unknown } | null)?.message as string) ||
132
+ "Could not load"
133
+ );
134
+ },
135
+ },
136
+ };
137
+ });
138
+
139
+ jest.mock("react-i18next", () => {
140
+ return {
141
+ useTranslation: () => {
142
+ return {
143
+ t: (key: string, options?: { defaultValue?: string }): string => {
144
+ return options?.defaultValue ?? key;
145
+ },
146
+ };
147
+ },
148
+ };
149
+ });
150
+
151
+ /*
152
+ * The rule tables are stubbed down to the two facts this file is about: which
153
+ * rule type a table filters on, and which severity band it was mounted for.
154
+ * Rendering the real ModelTable would drag in the pager and the facet bar, none
155
+ * of which says anything about how the pages were split.
156
+ */
157
+ interface CapturedTableProps {
158
+ query: Record<string, ObjectID | NotificationRuleType | undefined>;
159
+ userPreferencesKey: string;
160
+ }
161
+
162
+ let capturedTables: Array<CapturedTableProps> = [];
163
+
164
+ type GetCapturedTablesFunction = () => Array<CapturedTableProps>;
165
+
166
+ /*
167
+ * The captured list is REPLACED between tests rather than emptied in place, so
168
+ * a closure built inside the `for` loops below would otherwise read whichever
169
+ * array happened to be bound when the loop ran. Going through a stable function
170
+ * makes each test see the list as it stands when that test executes, which is
171
+ * also what stops eslint's no-loop-func from being right about it.
172
+ */
173
+ const getCapturedTables: GetCapturedTablesFunction =
174
+ (): Array<CapturedTableProps> => {
175
+ return capturedTables;
176
+ };
177
+
178
+ type TableIdentityFunction = (props: CapturedTableProps) => string;
179
+
180
+ const tableIdentity: TableIdentityFunction = (
181
+ props: CapturedTableProps,
182
+ ): string => {
183
+ const severityId: ObjectID | NotificationRuleType | undefined =
184
+ props.query["incidentSeverityId"] ?? props.query["alertSeverityId"];
185
+
186
+ return `${String(props.query["ruleType"])}::${
187
+ severityId ? severityId.toString() : "no-severity"
188
+ }`;
189
+ };
190
+
191
+ jest.mock("../../../UI/Components/ModelTable/ModelTable", () => {
192
+ return {
193
+ __esModule: true,
194
+ default: (props: CapturedTableProps) => {
195
+ const identity: string = tableIdentity(props);
196
+
197
+ const existingIndex: number = capturedTables.findIndex(
198
+ (candidate: CapturedTableProps): boolean => {
199
+ return tableIdentity(candidate) === identity;
200
+ },
201
+ );
202
+
203
+ if (existingIndex === -1) {
204
+ capturedTables.push(props);
205
+ } else {
206
+ capturedTables[existingIndex] = props;
207
+ }
208
+
209
+ return null;
210
+ },
211
+ };
212
+ });
213
+
214
+ import UserViewNotificationRulesRedirect from "../../../../App/FeatureSet/Dashboard/src/Pages/Users/View/NotificationRules";
215
+ import UserViewSideMenu from "../../../../App/FeatureSet/Dashboard/src/Pages/Users/View/SideMenu";
216
+ import UserViewOnCallLayout from "../../../../App/FeatureSet/Dashboard/src/Pages/Users/View/OnCall/Layout";
217
+ import UserViewNotificationMethods from "../../../../App/FeatureSet/Dashboard/src/Pages/Users/View/OnCall/NotificationMethods";
218
+ import UserViewOnCallReadiness from "../../../../App/FeatureSet/Dashboard/src/Pages/Users/View/OnCall/Readiness";
219
+ import UserViewOnCallRules, {
220
+ ALERT_EPISODE_RULES_PROPS,
221
+ ALERT_RULES_PROPS,
222
+ INCIDENT_EPISODE_RULES_PROPS,
223
+ INCIDENT_RULES_PROPS,
224
+ } from "../../../../App/FeatureSet/Dashboard/src/Pages/Users/View/OnCall/Rules";
225
+ import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
226
+ import PageMap from "../../../../App/FeatureSet/Dashboard/src/Utils/PageMap";
227
+ import RouteMap, {
228
+ RouteUtil,
229
+ } from "../../../../App/FeatureSet/Dashboard/src/Utils/RouteMap";
230
+ import { getUsersBreadcrumbs } from "../../../../App/FeatureSet/Dashboard/src/Utils/Breadcrumbs/UsersBreadcrumbs";
231
+ import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
232
+ import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
233
+ import Project from "../../../Models/DatabaseModels/Project";
234
+ import TeamMember from "../../../Models/DatabaseModels/TeamMember";
235
+ import User from "../../../Models/DatabaseModels/User";
236
+ import HTTPResponse from "../../../Types/API/HTTPResponse";
237
+ import Route from "../../../Types/API/Route";
238
+ import Email from "../../../Types/Email";
239
+ import { JSONObject } from "../../../Types/JSON";
240
+ import Link from "../../../Types/Link";
241
+ import Name from "../../../Types/Name";
242
+ import NotificationRuleType from "../../../Types/NotificationRule/NotificationRuleType";
243
+ import ObjectID from "../../../Types/ObjectID";
244
+ import Permission from "../../../Types/Permission";
245
+ import Navigation from "../../../UI/Utils/Navigation";
246
+ import PermissionUtil from "../../../UI/Utils/Permission";
247
+ import ProjectUtil from "../../../UI/Utils/Project";
248
+ import UserUtil from "../../../UI/Utils/User";
249
+
250
+ const PROJECT_ID: ObjectID = new ObjectID(PROJECT_ID_STRING);
251
+ const SIGNED_IN_USER_ID: ObjectID = new ObjectID(SIGNED_IN_USER_ID_STRING);
252
+ const TARGET_USER_ID: ObjectID = new ObjectID(TARGET_USER_ID_STRING);
253
+
254
+ const pageProps: PageComponentProps = {
255
+ pageRoute: new Route("/users"),
256
+ currentProject: null,
257
+ hasPaymentMethod: false,
258
+ };
259
+
260
+ interface SeveritySpec {
261
+ id: string;
262
+ name: string;
263
+ }
264
+
265
+ const INCIDENT_SEVERITY_SPECS: Array<SeveritySpec> = [
266
+ { id: INCIDENT_SEVERITY_ONE_ID, name: "Sev One" },
267
+ { id: INCIDENT_SEVERITY_TWO_ID, name: "Sev Two" },
268
+ ];
269
+
270
+ const ALERT_SEVERITY_SPECS: Array<SeveritySpec> = [
271
+ { id: ALERT_SEVERITY_ONE_ID, name: "Alert One" },
272
+ { id: ALERT_SEVERITY_TWO_ID, name: "Alert Two" },
273
+ ];
274
+
275
+ type BuildSeverities = (
276
+ modelType: { new (): IncidentSeverity | AlertSeverity },
277
+ specs: Array<SeveritySpec>,
278
+ ) => Array<IncidentSeverity | AlertSeverity>;
279
+
280
+ const buildSeverities: BuildSeverities = (
281
+ modelType: { new (): IncidentSeverity | AlertSeverity },
282
+ specs: Array<SeveritySpec>,
283
+ ): Array<IncidentSeverity | AlertSeverity> => {
284
+ return specs.map((spec: SeveritySpec) => {
285
+ const severity: IncidentSeverity | AlertSeverity = new modelType();
286
+ severity._id = spec.id;
287
+ severity.name = spec.name;
288
+ return severity;
289
+ });
290
+ };
291
+
292
+ type BuildTeamMemberFunction = () => TeamMember;
293
+
294
+ const buildTeamMember: BuildTeamMemberFunction = (): TeamMember => {
295
+ const user: User = new User();
296
+ user._id = TARGET_USER_ID_STRING;
297
+ user.name = new Name(TARGET_USER_NAME);
298
+ user.email = new Email(TARGET_LOGIN_EMAIL);
299
+
300
+ const member: TeamMember = new TeamMember();
301
+ member.user = user;
302
+
303
+ return member;
304
+ };
305
+
306
+ const READINESS_PAYLOAD: JSONObject = {
307
+ userId: TARGET_USER_ID_STRING,
308
+ userName: TARGET_USER_NAME,
309
+ userEmail: TARGET_LOGIN_EMAIL,
310
+ status: "PartiallyReady",
311
+ methods: [],
312
+ coverage: [],
313
+ reasons: [],
314
+ reachedVia: ["Team"],
315
+ };
316
+
317
+ /*
318
+ * The six pages of the section, as the ROUTE TABLE spells them, paired with the
319
+ * component each route mounts. Everything below reads this rather than a
320
+ * hand-written list of paths, so a page whose route is renamed is a compile
321
+ * error or a failing render rather than a test that quietly stops covering it.
322
+ */
323
+ interface SectionPage {
324
+ pageMapKey: PageMap;
325
+ menuTitle: string;
326
+ breadcrumbTitle: string;
327
+ element: ReactElement;
328
+ /* The rule type this page is expected to mount tables for, if any. */
329
+ ruleType?: NotificationRuleType | undefined;
330
+ /* The severity ids the tables on this page must be banded by. */
331
+ severityIds?: Array<string> | undefined;
332
+ /* Ids from the OTHER severity model, which must never appear on it. */
333
+ foreignSeverityIds?: Array<string> | undefined;
334
+ }
335
+
336
+ const SECTION_PAGES: Array<SectionPage> = [
337
+ {
338
+ pageMapKey: PageMap.USER_VIEW_ON_CALL_READINESS,
339
+ menuTitle: "Readiness",
340
+ breadcrumbTitle: "On-Call Readiness",
341
+ element: <UserViewOnCallReadiness {...pageProps} />,
342
+ },
343
+ {
344
+ pageMapKey: PageMap.USER_VIEW_NOTIFICATION_METHODS,
345
+ menuTitle: "Notification Methods",
346
+ breadcrumbTitle: "Notification Methods",
347
+ element: <UserViewNotificationMethods {...pageProps} />,
348
+ },
349
+ {
350
+ pageMapKey: PageMap.USER_VIEW_INCIDENT_ON_CALL_RULES,
351
+ menuTitle: "Incident On-Call Rules",
352
+ breadcrumbTitle: "Incident On-Call Rules",
353
+ element: <UserViewOnCallRules {...INCIDENT_RULES_PROPS} />,
354
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
355
+ severityIds: [INCIDENT_SEVERITY_ONE_ID, INCIDENT_SEVERITY_TWO_ID],
356
+ foreignSeverityIds: [ALERT_SEVERITY_ONE_ID, ALERT_SEVERITY_TWO_ID],
357
+ },
358
+ {
359
+ pageMapKey: PageMap.USER_VIEW_INCIDENT_EPISODE_ON_CALL_RULES,
360
+ menuTitle: "Incident Episode On-Call Rules",
361
+ breadcrumbTitle: "Incident Episode On-Call Rules",
362
+ element: <UserViewOnCallRules {...INCIDENT_EPISODE_RULES_PROPS} />,
363
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
364
+ /*
365
+ * The crossed pair: an incident EPISODE is banded by IncidentSeverity while
366
+ * an alert episode is banded by AlertSeverity. Deriving one axis from the
367
+ * other — "is this an episode?" — gets exactly this row wrong.
368
+ */
369
+ severityIds: [INCIDENT_SEVERITY_ONE_ID, INCIDENT_SEVERITY_TWO_ID],
370
+ foreignSeverityIds: [ALERT_SEVERITY_ONE_ID, ALERT_SEVERITY_TWO_ID],
371
+ },
372
+ {
373
+ pageMapKey: PageMap.USER_VIEW_ALERT_ON_CALL_RULES,
374
+ menuTitle: "Alert On-Call Rules",
375
+ breadcrumbTitle: "Alert On-Call Rules",
376
+ element: <UserViewOnCallRules {...ALERT_RULES_PROPS} />,
377
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
378
+ severityIds: [ALERT_SEVERITY_ONE_ID, ALERT_SEVERITY_TWO_ID],
379
+ foreignSeverityIds: [INCIDENT_SEVERITY_ONE_ID, INCIDENT_SEVERITY_TWO_ID],
380
+ },
381
+ {
382
+ pageMapKey: PageMap.USER_VIEW_ALERT_EPISODE_ON_CALL_RULES,
383
+ menuTitle: "Alert Episode On-Call Rules",
384
+ breadcrumbTitle: "Alert Episode On-Call Rules",
385
+ element: <UserViewOnCallRules {...ALERT_EPISODE_RULES_PROPS} />,
386
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
387
+ severityIds: [ALERT_SEVERITY_ONE_ID, ALERT_SEVERITY_TWO_ID],
388
+ foreignSeverityIds: [INCIDENT_SEVERITY_ONE_ID, INCIDENT_SEVERITY_TWO_ID],
389
+ },
390
+ ];
391
+
392
+ type LastPathForFunction = (pageMapKey: PageMap) => string;
393
+
394
+ /*
395
+ * The last URL segment of a page, taken from the app's own route table exactly
396
+ * the way UsersRoutes.tsx takes it. A test that hard-coded "on-call-readiness"
397
+ * would keep passing after somebody renamed the route and broke every link to
398
+ * it.
399
+ */
400
+ const lastPathFor: LastPathForFunction = (pageMapKey: PageMap): string => {
401
+ return RouteUtil.getLastPathForKey(pageMapKey);
402
+ };
403
+
404
+ type RenderPageFunction = (page: SectionPage) => HTMLElement;
405
+
406
+ /*
407
+ * The real nesting: the `:id` parent, the pathless section layout, then the
408
+ * page. The layout reads the target user from `useParams`, so this is also what
409
+ * pins "the section knows whose configuration it is showing" to the URL rather
410
+ * than to a stub.
411
+ */
412
+ const renderPage: RenderPageFunction = (page: SectionPage): HTMLElement => {
413
+ const path: string = lastPathFor(page.pageMapKey);
414
+
415
+ const { container } = render(
416
+ <MemoryRouter
417
+ initialEntries={[
418
+ `/dashboard/${PROJECT_ID_STRING}/users/${TARGET_USER_ID_STRING}/${path}`,
419
+ ]}
420
+ >
421
+ <RouterRoutes>
422
+ <RouterRoute
423
+ path="/dashboard/:projectId/users/:id"
424
+ element={<Outlet />}
425
+ >
426
+ <RouterRoute element={<UserViewOnCallLayout />}>
427
+ <RouterRoute path={path} element={page.element} />
428
+ </RouterRoute>
429
+ </RouterRoute>
430
+ </RouterRoutes>
431
+ </MemoryRouter>,
432
+ );
433
+
434
+ return container;
435
+ };
436
+
437
+ type SettleFunction = () => Promise<void>;
438
+
439
+ /*
440
+ * The two places that read the router's location without being inside one.
441
+ *
442
+ * SideMenu asks Navigation.isOnThisPage to decide which entry is active, and
443
+ * the breadcrumb builder splits the current pathname to work out the levels
444
+ * above. In the app the router pushes that location into Navigation on every
445
+ * navigation; nothing does so in a test, and the getters throw on undefined
446
+ * rather than returning a default — deliberately, since a menu that silently
447
+ * decided nothing was active would be a much quieter bug.
448
+ */
449
+ type GoToFunction = (path: string) => void;
450
+
451
+ const goTo: GoToFunction = (path: string): void => {
452
+ window.history.pushState({}, "", path);
453
+ Navigation.setLocation({
454
+ pathname: path,
455
+ search: "",
456
+ hash: "",
457
+ state: null,
458
+ key: "test",
459
+ } as Location);
460
+ };
461
+
462
+ const settle: SettleFunction = async (): Promise<void> => {
463
+ await waitFor(
464
+ (): void => {
465
+ expect(pendingRequestCount).toBe(0);
466
+ },
467
+ { timeout: 4000 },
468
+ );
469
+ };
470
+
471
+ beforeEach((): void => {
472
+ capturedTables = [];
473
+ pendingRequestCount = 0;
474
+
475
+ getListMock.mockReset();
476
+ getItemMock.mockReset();
477
+ getCommonHeadersMock.mockReset();
478
+ apiGetMock.mockReset();
479
+ navigateMock.mockReset();
480
+
481
+ getListMock.mockImplementation((data: any) => {
482
+ if (data.modelType === IncidentSeverity) {
483
+ return Promise.resolve({
484
+ data: buildSeverities(IncidentSeverity, INCIDENT_SEVERITY_SPECS),
485
+ count: INCIDENT_SEVERITY_SPECS.length,
486
+ skip: 0,
487
+ limit: INCIDENT_SEVERITY_SPECS.length,
488
+ });
489
+ }
490
+
491
+ if (data.modelType === AlertSeverity) {
492
+ return Promise.resolve({
493
+ data: buildSeverities(AlertSeverity, ALERT_SEVERITY_SPECS),
494
+ count: ALERT_SEVERITY_SPECS.length,
495
+ skip: 0,
496
+ limit: ALERT_SEVERITY_SPECS.length,
497
+ });
498
+ }
499
+
500
+ if (data.modelType === TeamMember) {
501
+ const member: TeamMember = buildTeamMember();
502
+ return Promise.resolve({ data: [member], count: 1, skip: 0, limit: 1 });
503
+ }
504
+
505
+ return Promise.resolve({ data: [], count: 0, skip: 0, limit: 0 });
506
+ });
507
+
508
+ const project: Project = new Project();
509
+ project.disableOnCallNotificationFallback = false;
510
+ getItemMock.mockResolvedValue(project as never);
511
+
512
+ getCommonHeadersMock.mockReturnValue({} as never);
513
+ apiGetMock.mockResolvedValue(
514
+ new HTTPResponse<JSONObject>(200, READINESS_PAYLOAD, {}) as never,
515
+ );
516
+
517
+ goTo(
518
+ `/dashboard/${PROJECT_ID_STRING}/users/${TARGET_USER_ID_STRING}/on-call-readiness`,
519
+ );
520
+
521
+ jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
522
+ jest.spyOn(UserUtil, "getUserId").mockReturnValue(SIGNED_IN_USER_ID);
523
+ jest.spyOn(UserUtil, "isMasterAdmin").mockReturnValue(false);
524
+ jest
525
+ .spyOn(PermissionUtil, "getAllPermissions")
526
+ .mockReturnValue([Permission.ProjectAdmin]);
527
+ jest.spyOn(Navigation, "navigate").mockImplementation(((
528
+ ...args: Array<unknown>
529
+ ): void => {
530
+ navigateMock(...args);
531
+ }) as never);
532
+ });
533
+
534
+ afterEach(async (): Promise<void> => {
535
+ cleanup();
536
+
537
+ for (
538
+ let attempt: number = 0;
539
+ pendingRequestCount > 0 && attempt < 100;
540
+ attempt++
541
+ ) {
542
+ await new Promise<void>((resolve: () => void): void => {
543
+ setTimeout(resolve, 0);
544
+ });
545
+ }
546
+
547
+ jest.restoreAllMocks();
548
+ });
549
+
550
+ describe("the old combined route", () => {
551
+ test("still resolves, and lands on the readiness overview", async () => {
552
+ window.history.pushState(
553
+ {},
554
+ "",
555
+ `/dashboard/${PROJECT_ID_STRING}/users/${TARGET_USER_ID_STRING}/notification-rules`,
556
+ );
557
+
558
+ render(<UserViewNotificationRulesRedirect {...pageProps} />);
559
+
560
+ await waitFor((): void => {
561
+ expect(navigateMock).toHaveBeenCalled();
562
+ });
563
+
564
+ const destination: Route = navigateMock.mock.calls[0]![0] as Route;
565
+
566
+ /*
567
+ * Asserted against the route table rather than against a literal, and with
568
+ * the TARGET user's id in it: the id is the second-to-last segment of the
569
+ * old URL, and reading the wrong offset produces a redirect to a page about
570
+ * the literal string "notification-rules".
571
+ */
572
+ expect(destination.toString()).toBe(
573
+ RouteUtil.populateRouteParams(
574
+ RouteMap[PageMap.USER_VIEW_ON_CALL_READINESS] as Route,
575
+ { modelId: TARGET_USER_ID },
576
+ ).toString(),
577
+ );
578
+
579
+ expect(destination.toString()).toContain(TARGET_USER_ID_STRING);
580
+ });
581
+
582
+ test("reads nothing about the user on its way past", async () => {
583
+ window.history.pushState(
584
+ {},
585
+ "",
586
+ `/dashboard/${PROJECT_ID_STRING}/users/${TARGET_USER_ID_STRING}/notification-rules`,
587
+ );
588
+
589
+ render(<UserViewNotificationRulesRedirect {...pageProps} />);
590
+
591
+ await waitFor((): void => {
592
+ expect(navigateMock).toHaveBeenCalled();
593
+ });
594
+
595
+ /*
596
+ * A redirect that also fetches is a redirect that has already paid for the
597
+ * page it is not going to draw — and on this surface the thing being
598
+ * fetched is somebody else's paging configuration.
599
+ */
600
+ expect(apiGetMock).not.toHaveBeenCalled();
601
+ expect(getListMock).not.toHaveBeenCalled();
602
+ });
603
+ });
604
+
605
+ describe("each route renders its own page and nothing else", () => {
606
+ for (const page of SECTION_PAGES) {
607
+ if (!page.ruleType) {
608
+ continue;
609
+ }
610
+
611
+ test(`${page.menuTitle} mounts tables for one rule type only`, async () => {
612
+ renderPage(page);
613
+
614
+ await settle();
615
+
616
+ await waitFor((): void => {
617
+ expect(getCapturedTables().length).toBeGreaterThan(0);
618
+ });
619
+
620
+ /*
621
+ * The point of the split, counted. The route this page replaced mounted
622
+ * eight tables — four rule types times two severity bands — and every one
623
+ * of them was on screen at once.
624
+ */
625
+ expect(getCapturedTables()).toHaveLength(page.severityIds!.length);
626
+
627
+ for (const table of getCapturedTables()) {
628
+ expect(table.query["ruleType"]).toBe(page.ruleType);
629
+ }
630
+ });
631
+
632
+ test(`${page.menuTitle} bands its tables by the right severity model`, async () => {
633
+ renderPage(page);
634
+
635
+ await settle();
636
+
637
+ await waitFor((): void => {
638
+ expect(getCapturedTables()).toHaveLength(page.severityIds!.length);
639
+ });
640
+
641
+ const bandedIds: Array<string> = getCapturedTables().map(
642
+ (table: CapturedTableProps): string => {
643
+ const severityId: ObjectID | NotificationRuleType | undefined =
644
+ table.query["incidentSeverityId"] ?? table.query["alertSeverityId"];
645
+
646
+ return severityId ? severityId.toString() : "";
647
+ },
648
+ );
649
+
650
+ expect(bandedIds.sort()).toEqual([...page.severityIds!].sort());
651
+
652
+ /*
653
+ * The disjoint fixture doing its job: a page handed the other model's
654
+ * props would list these ids instead, and nothing else in the render
655
+ * would look wrong.
656
+ */
657
+ for (const foreignId of page.foreignSeverityIds!) {
658
+ expect(bandedIds).not.toContain(foreignId);
659
+ }
660
+ });
661
+ }
662
+
663
+ test("the readiness page mounts no rule table at all", async () => {
664
+ renderPage(SECTION_PAGES[0]!);
665
+
666
+ await settle();
667
+
668
+ /*
669
+ * The overview diagnoses and does not repair. A readiness page that also
670
+ * drew the rule tables would be the page this split exists to break up,
671
+ * wearing a new name.
672
+ */
673
+ expect(capturedTables).toHaveLength(0);
674
+ });
675
+ });
676
+
677
+ describe("the section shares one load of the person it is about", () => {
678
+ test("a rule page reads identity and readiness exactly once", async () => {
679
+ renderPage(SECTION_PAGES[2]!);
680
+
681
+ await settle();
682
+
683
+ const teamMemberReads: number = getListMock.mock.calls.filter(
684
+ (call: Array<any>): boolean => {
685
+ return call[0].modelType === TeamMember;
686
+ },
687
+ ).length;
688
+
689
+ expect(teamMemberReads).toBe(1);
690
+ expect(apiGetMock).toHaveBeenCalledTimes(1);
691
+ });
692
+
693
+ test("every page asks about the user in the URL, never the signed-in admin", async () => {
694
+ for (const page of SECTION_PAGES) {
695
+ capturedTables = [];
696
+ getListMock.mockClear();
697
+ apiGetMock.mockClear();
698
+
699
+ renderPage(page);
700
+
701
+ await settle();
702
+
703
+ const memberRead: any = getListMock.mock.calls.find(
704
+ (call: Array<any>): boolean => {
705
+ return call[0].modelType === TeamMember;
706
+ },
707
+ );
708
+
709
+ expect(memberRead[0].query.userId.toString()).toBe(TARGET_USER_ID_STRING);
710
+ expect(memberRead[0].query.userId.toString()).not.toBe(
711
+ SIGNED_IN_USER_ID_STRING,
712
+ );
713
+
714
+ /*
715
+ * And the readiness read too. The two travelling together is what stops
716
+ * one page in the section naming one person while another names somebody
717
+ * else — the state this surface must never be in.
718
+ */
719
+ const readinessUrl: string = String(apiGetMock.mock.calls[0]![0].url);
720
+
721
+ expect(readinessUrl).toContain(TARGET_USER_ID_STRING);
722
+
723
+ cleanup();
724
+ }
725
+ });
726
+
727
+ test("the on-behalf-of banner is on every page in the section", async () => {
728
+ for (const page of SECTION_PAGES) {
729
+ const container: HTMLElement = renderPage(page);
730
+
731
+ await settle();
732
+
733
+ /*
734
+ * Rendered by the LAYOUT, so it cannot be forgotten on a page added
735
+ * later. The whole risk of this section is somebody rewriting the wrong
736
+ * person's paging while believing it is their own, and a page reached
737
+ * directly by URL is exactly the one that would arrive without context.
738
+ */
739
+ expect(container.textContent).toContain(
740
+ `You are editing on behalf of ${TARGET_USER_NAME}`,
741
+ );
742
+
743
+ cleanup();
744
+ }
745
+ });
746
+
747
+ test("a reader with no permission is refused on every page, and nothing is fetched", async () => {
748
+ jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue([]);
749
+
750
+ for (const page of SECTION_PAGES) {
751
+ getListMock.mockClear();
752
+ apiGetMock.mockClear();
753
+
754
+ renderPage(page);
755
+
756
+ expect(
757
+ await screen.findByText(/do not have permission to view this user/),
758
+ ).toBeInTheDocument();
759
+
760
+ /*
761
+ * Not merely "nothing drawn": nothing requested either. A refused page
762
+ * that still fetches a colleague's readiness has already disclosed the
763
+ * thing it declined to render — and because the refusal lives in the
764
+ * LAYOUT, one check covers all six rather than five plus whichever one
765
+ * somebody forgets.
766
+ */
767
+ expect(apiGetMock).not.toHaveBeenCalled();
768
+ expect(getListMock).not.toHaveBeenCalled();
769
+
770
+ cleanup();
771
+ }
772
+ });
773
+ });
774
+
775
+ describe("every page is reachable", () => {
776
+ type MenuLinkFunction = () => Array<{ title: string; href: string }>;
777
+
778
+ const renderMenu: MenuLinkFunction = (): Array<{
779
+ title: string;
780
+ href: string;
781
+ }> => {
782
+ render(
783
+ <MemoryRouter>
784
+ <UserViewSideMenu modelId={TARGET_USER_ID} hasCustomFields={false} />
785
+ </MemoryRouter>,
786
+ );
787
+
788
+ return Array.from(document.querySelectorAll("a")).map(
789
+ (anchor: HTMLAnchorElement): { title: string; href: string } => {
790
+ return {
791
+ title: anchor.textContent?.trim() || "",
792
+ href: anchor.getAttribute("href") || "",
793
+ };
794
+ },
795
+ );
796
+ };
797
+
798
+ test("the side menu names all six pages, pointing at the route table's own paths", () => {
799
+ const links: Array<{ title: string; href: string }> = renderMenu();
800
+
801
+ for (const page of SECTION_PAGES) {
802
+ const expectedHref: string = RouteUtil.populateRouteParams(
803
+ RouteMap[page.pageMapKey] as Route,
804
+ { modelId: TARGET_USER_ID },
805
+ ).toString();
806
+
807
+ const link: { title: string; href: string } | undefined = links.find(
808
+ (candidate: { title: string; href: string }): boolean => {
809
+ return candidate.title === page.menuTitle;
810
+ },
811
+ );
812
+
813
+ expect(link).toBeDefined();
814
+ expect(link!.href).toBe(expectedHref);
815
+ }
816
+ });
817
+
818
+ test("the menu hides the section from somebody who may not read it", () => {
819
+ jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue([]);
820
+
821
+ const links: Array<{ title: string; href: string }> = renderMenu();
822
+
823
+ for (const page of SECTION_PAGES) {
824
+ expect(
825
+ links.some((candidate: { title: string; href: string }): boolean => {
826
+ return candidate.title === page.menuTitle;
827
+ }),
828
+ ).toBe(false);
829
+ }
830
+
831
+ /*
832
+ * Hiding is a convenience and never the boundary — the pages repeat the
833
+ * check and the API refuses the reads — but a menu full of entries that all
834
+ * refuse is its own kind of broken.
835
+ */
836
+ expect(
837
+ links.some((candidate: { title: string; href: string }): boolean => {
838
+ return candidate.title === "Profile";
839
+ }),
840
+ ).toBe(true);
841
+ });
842
+
843
+ test("a member walking into their own row keeps the section", () => {
844
+ jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue([]);
845
+ jest.spyOn(UserUtil, "getUserId").mockReturnValue(TARGET_USER_ID);
846
+
847
+ const links: Array<{ title: string; href: string }> = renderMenu();
848
+
849
+ /*
850
+ * Your own configuration needs no grant at all — Permission.CurrentUser
851
+ * already carries it — so an ordinary member reaching their own row sees
852
+ * the same six pages.
853
+ */
854
+ for (const page of SECTION_PAGES) {
855
+ expect(
856
+ links.some((candidate: { title: string; href: string }): boolean => {
857
+ return candidate.title === page.menuTitle;
858
+ }),
859
+ ).toBe(true);
860
+ }
861
+ });
862
+
863
+ test("every page has a breadcrumb of its own", () => {
864
+ for (const page of SECTION_PAGES) {
865
+ const route: Route = RouteMap[page.pageMapKey] as Route;
866
+
867
+ const breadcrumbs: Array<Link> | undefined = getUsersBreadcrumbs(
868
+ route.toString(),
869
+ );
870
+
871
+ expect(breadcrumbs).toBeDefined();
872
+
873
+ const titles: Array<string> = breadcrumbs!.map((link: Link): string => {
874
+ return link.title;
875
+ });
876
+
877
+ /*
878
+ * A page with no breadcrumb entry renders a bare title and no way back up
879
+ * — survivable on one route, six times worse once a single page became a
880
+ * section somebody navigates around inside.
881
+ */
882
+ expect(titles).toContain(page.breadcrumbTitle);
883
+ expect(titles[0]).toBe("Project");
884
+ }
885
+ });
886
+
887
+ test("the six routes are distinct URLs", () => {
888
+ const paths: Array<string> = SECTION_PAGES.map(
889
+ (page: SectionPage): string => {
890
+ return (RouteMap[page.pageMapKey] as Route).toString();
891
+ },
892
+ );
893
+
894
+ /*
895
+ * Two PageMap keys pointing at one path is a copy-paste that produces a
896
+ * menu entry which silently opens somebody else's page.
897
+ */
898
+ expect(new Set(paths).size).toBe(SECTION_PAGES.length);
899
+
900
+ // And none of them collides with the legacy route that now redirects.
901
+ expect(paths).not.toContain(
902
+ (RouteMap[PageMap.USER_VIEW_NOTIFICATION_RULES] as Route).toString(),
903
+ );
904
+ });
905
+ });