@oneuptime/common 11.7.1 → 11.7.3

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 (285) hide show
  1. package/Models/AnalyticsModels/KubernetesCostAllocation.ts +103 -0
  2. package/Models/DatabaseModels/DashboardDomain.ts +1 -0
  3. package/Models/DatabaseModels/Incident.ts +2 -0
  4. package/Models/DatabaseModels/IncidentEpisode.ts +1 -0
  5. package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +1 -0
  6. package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +1 -0
  7. package/Models/DatabaseModels/IncidentPublicNote.ts +1 -0
  8. package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
  9. package/Models/DatabaseModels/Monitor.ts +1 -0
  10. package/Models/DatabaseModels/MonitorStatusTimeline.ts +1 -0
  11. package/Models/DatabaseModels/NetworkSite.ts +1 -0
  12. package/Models/DatabaseModels/NetworkSiteStatusTimeline.ts +1 -0
  13. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +1 -0
  14. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +2 -0
  15. package/Models/DatabaseModels/Probe.ts +13 -13
  16. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -0
  17. package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +1 -0
  18. package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +1 -0
  19. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -0
  20. package/Models/DatabaseModels/StatusPage.ts +1 -0
  21. package/Models/DatabaseModels/StatusPageAnnouncement.ts +1 -0
  22. package/Models/DatabaseModels/StatusPageDomain.ts +1 -0
  23. package/Models/DatabaseModels/TableView.ts +40 -0
  24. package/Models/DatabaseModels/WorkspaceNotificationSummary.ts +1 -0
  25. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +11 -0
  26. package/Server/API/BaseAPI.ts +17 -1
  27. package/Server/API/CommonAPI.ts +32 -0
  28. package/Server/API/MicrosoftTeamsAPI.ts +99 -2
  29. package/Server/API/ProbeAPI.ts +2 -0
  30. package/Server/API/SlackAPI.ts +62 -0
  31. package/Server/API/StatusPageAPI.ts +157 -192
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.ts +89 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.ts +61 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.ts +280 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.ts +13 -0
  36. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  37. package/Server/Services/DatabaseService.ts +11 -3
  38. package/Server/Services/MonitorGroupService.ts +145 -0
  39. package/Server/Services/MonitorService.ts +231 -30
  40. package/Server/Services/ProbeService.ts +319 -16
  41. package/Server/Services/StatusPageService.ts +72 -42
  42. package/Server/Services/WorkspaceNotificationRuleService.ts +232 -2
  43. package/Server/Utils/Database/PostgresErrorTranslator.ts +131 -0
  44. package/Server/Utils/Monitor/MonitorResource.ts +88 -35
  45. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +21 -1
  46. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +158 -0
  47. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +706 -6
  48. package/Tests/Models/AnalyticsModels/KubernetesCostAllocation.test.ts +16 -0
  49. package/Tests/Models/DatabaseModels/ProbeColumnAccessControl.test.ts +103 -0
  50. package/Tests/Server/API/BaseAPIUpdateNoOp.test.ts +135 -0
  51. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +317 -0
  52. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +228 -0
  53. package/Tests/Server/API/ProbeAPI.test.ts +6 -0
  54. package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +167 -0
  55. package/Tests/Server/Services/AddHotQueryIndexesMigration.test.ts +255 -0
  56. package/Tests/Server/Services/AddHotQueryIndexesSecondPassMigration.test.ts +258 -0
  57. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +1 -1
  58. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +121 -0
  59. package/Tests/Server/Services/HeartbeatWriteFastPathSafety.test.ts +200 -0
  60. package/Tests/Server/Services/HookFreeWriteSafetySecondPass.test.ts +181 -0
  61. package/Tests/Server/Services/MonitorGroupServiceBatchedStatuses.test.ts +513 -0
  62. package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +492 -0
  63. package/Tests/Server/Services/MonitorServiceProbeSelection.test.ts +401 -0
  64. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +453 -0
  65. package/Tests/Server/Services/ProbeAuthKeyCacheAndHeartbeat.test.ts +679 -0
  66. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +3 -3
  67. package/Tests/Server/Services/StatusPageDomainResolutionCache.test.ts +245 -0
  68. package/Tests/Server/Services/StatusPageOverviewTimelineWindow.test.ts +283 -0
  69. package/Tests/Server/Services/StatusPageServiceMcp.test.ts +17 -1
  70. package/Tests/Server/Services/StatusPageServiceMonitorGroupBatching.test.ts +331 -0
  71. package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +12 -12
  72. package/Tests/Server/Services/WorkspaceNotificationRuleChats.test.ts +1390 -0
  73. package/Tests/Server/Utils/AI/Insights/InsightFixRouting.test.ts +4 -4
  74. package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +2 -2
  75. package/Tests/Server/Utils/AI/Insights/InsightStore.test.ts +1 -3
  76. package/Tests/Server/Utils/AI/Insights/InsightStoreHardening.test.ts +1 -3
  77. package/Tests/Server/Utils/AI/Insights/InsightTriage.test.ts +1 -1
  78. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +5 -5
  79. package/Tests/Server/Utils/Alert/AlertPrivacyFilter.test.ts +187 -0
  80. package/Tests/Server/Utils/AlertEpisode/AlertEpisodePrivacyFilter.test.ts +170 -0
  81. package/Tests/Server/Utils/Database/PostgresErrorTranslator.test.ts +125 -0
  82. package/Tests/Server/Utils/Incident/IncidentPrivacyFilter.test.ts +264 -0
  83. package/Tests/Server/Utils/IncidentEpisode/IncidentEpisodePrivacyFilter.test.ts +175 -0
  84. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +181 -0
  85. package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +1 -1
  86. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +2 -2
  87. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +341 -0
  88. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +2175 -0
  89. package/Tests/Server/Utils/Workspace/SlackChannelsList.test.ts +585 -0
  90. package/Tests/Types/Billing/MeteredPlan.test.ts +46 -0
  91. package/Tests/Types/JSONFunctions.test.ts +103 -1
  92. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +219 -1
  93. package/Tests/UI/Components/BulkUpdateForm.test.tsx +174 -0
  94. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +315 -0
  95. package/Tests/UI/Components/ModelDetailSelect.test.tsx +229 -0
  96. package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1258 -0
  97. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +679 -0
  98. package/Tests/UI/Components/ModelTable/ColumnCustomizationModal.test.tsx +572 -0
  99. package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +1678 -0
  100. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +1094 -0
  101. package/Tests/UI/Components/ModelTableExportFromColumns.test.ts +153 -0
  102. package/Tests/UI/Components/MonacoLoader.test.ts +67 -0
  103. package/Tests/UI/Components/MonacoRuntime.test.ts +121 -0
  104. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +212 -3
  105. package/Tests/UI/Utils/Project.test.ts +809 -0
  106. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +360 -0
  107. package/Tests/Utils/Alerts/AlertMetricType.test.ts +143 -0
  108. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +219 -0
  109. package/Tests/Utils/Incident/IncidentMetricType.test.ts +174 -0
  110. package/Tests/Utils/Monitor/MonitorProbeSelectionUtil.test.ts +145 -0
  111. package/Tests/Utils/StatusPage/ResourceUptime.test.ts +263 -0
  112. package/Tests/Utils/UserPreferences.test.ts +563 -0
  113. package/Types/JSONFunctions.ts +7 -2
  114. package/Types/Kubernetes/KubernetesCostIngest.ts +37 -1
  115. package/Types/Workspace/NotificationRules/BaseNotificationRule.ts +4 -0
  116. package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +25 -10
  117. package/Types/Workspace/WorkspaceMessagePayload.ts +1 -0
  118. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +63 -8
  119. package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
  120. package/UI/Components/CodeEditor/MonacoLoader.ts +36 -0
  121. package/UI/Components/Forms/ModelForm.tsx +41 -18
  122. package/UI/Components/MasterPage/MasterPage.tsx +65 -0
  123. package/UI/Components/ModelDetail/CardModelDetail.tsx +23 -1
  124. package/UI/Components/ModelDetail/ModelDetail.tsx +46 -1
  125. package/UI/Components/ModelTable/BaseModelTable.tsx +687 -34
  126. package/UI/Components/ModelTable/Column.ts +29 -0
  127. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +420 -0
  128. package/UI/Components/ModelTable/ColumnPreference.ts +482 -0
  129. package/UI/Components/ModelTable/CustomFieldColumns.tsx +326 -0
  130. package/UI/Components/ModelTable/ExportFromColumns.ts +57 -0
  131. package/UI/Components/ModelTable/TableView.tsx +24 -2
  132. package/UI/Components/ModelTable/useCustomFieldColumns.ts +150 -0
  133. package/UI/Components/Navbar/NavBar.tsx +66 -1
  134. package/UI/Components/SideMenu/SideMenu.tsx +24 -4
  135. package/UI/Components/Table/Table.tsx +66 -14
  136. package/UI/Components/Table/TableRow.tsx +180 -175
  137. package/UI/Components/Table/Types/Column.ts +20 -0
  138. package/UI/Utils/Project.ts +214 -18
  139. package/UI/Utils/TableColumnsToCsv.ts +86 -6
  140. package/UI/esbuild-config.js +34 -0
  141. package/Utils/Monitor/MonitorProbeSelectionUtil.ts +72 -0
  142. package/Utils/UserPreferences.ts +53 -0
  143. package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js +86 -0
  144. package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js.map +1 -1
  145. package/build/dist/Models/DatabaseModels/DashboardDomain.js +1 -0
  146. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  147. package/build/dist/Models/DatabaseModels/Incident.js +2 -0
  148. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  149. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +1 -0
  150. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +1 -0
  152. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +1 -0
  154. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
  155. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +1 -0
  156. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  157. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
  158. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  159. package/build/dist/Models/DatabaseModels/Monitor.js +1 -0
  160. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  161. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
  162. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  163. package/build/dist/Models/DatabaseModels/NetworkSite.js +1 -0
  164. package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
  165. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js +1 -0
  166. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js.map +1 -1
  167. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -0
  168. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  169. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +2 -0
  170. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  171. package/build/dist/Models/DatabaseModels/Probe.js +13 -13
  172. package/build/dist/Models/DatabaseModels/Probe.js.map +1 -1
  173. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
  174. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  175. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +1 -0
  176. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
  177. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +1 -0
  178. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
  179. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
  180. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  181. package/build/dist/Models/DatabaseModels/StatusPage.js +1 -0
  182. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  183. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +1 -0
  184. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  185. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +1 -0
  186. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  187. package/build/dist/Models/DatabaseModels/TableView.js +40 -0
  188. package/build/dist/Models/DatabaseModels/TableView.js.map +1 -1
  189. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js +1 -0
  190. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js.map +1 -1
  191. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  192. package/build/dist/Server/API/BaseAPI.js +12 -1
  193. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  194. package/build/dist/Server/API/CommonAPI.js +21 -0
  195. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  196. package/build/dist/Server/API/MicrosoftTeamsAPI.js +72 -2
  197. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  198. package/build/dist/Server/API/ProbeAPI.js +2 -0
  199. package/build/dist/Server/API/ProbeAPI.js.map +1 -1
  200. package/build/dist/Server/API/SlackAPI.js +44 -0
  201. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  202. package/build/dist/Server/API/StatusPageAPI.js +98 -152
  203. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  204. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js +36 -0
  205. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js.map +1 -0
  206. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js +26 -0
  207. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js.map +1 -0
  208. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js +254 -0
  209. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js.map +1 -0
  210. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.js +12 -0
  211. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.js.map +1 -0
  212. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  213. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  214. package/build/dist/Server/Services/DatabaseService.js +10 -2
  215. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  216. package/build/dist/Server/Services/MonitorGroupService.js +118 -0
  217. package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
  218. package/build/dist/Server/Services/MonitorService.js +198 -24
  219. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  220. package/build/dist/Server/Services/ProbeService.js +238 -9
  221. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  222. package/build/dist/Server/Services/StatusPageService.js +46 -39
  223. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  224. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +178 -3
  225. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  226. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +80 -0
  227. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -0
  228. package/build/dist/Server/Utils/Monitor/MonitorResource.js +67 -29
  229. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  230. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +31 -14
  231. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  232. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +117 -0
  233. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -0
  234. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +549 -7
  235. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  236. package/build/dist/Types/JSONFunctions.js +8 -3
  237. package/build/dist/Types/JSONFunctions.js.map +1 -1
  238. package/build/dist/Types/Kubernetes/KubernetesCostIngest.js.map +1 -1
  239. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +17 -7
  240. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
  241. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +37 -11
  242. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  243. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +2 -0
  244. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  245. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +34 -0
  246. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +1 -0
  247. package/build/dist/UI/Components/Forms/ModelForm.js +37 -17
  248. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  249. package/build/dist/UI/Components/MasterPage/MasterPage.js +50 -1
  250. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  251. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +22 -1
  252. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  253. package/build/dist/UI/Components/ModelDetail/ModelDetail.js +31 -1
  254. package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
  255. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +472 -27
  256. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  257. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +189 -0
  258. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -0
  259. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +258 -0
  260. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -0
  261. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +168 -0
  262. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -0
  263. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js +30 -0
  264. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js.map +1 -0
  265. package/build/dist/UI/Components/ModelTable/TableView.js +13 -2
  266. package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
  267. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +84 -0
  268. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -0
  269. package/build/dist/UI/Components/Navbar/NavBar.js +39 -1
  270. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  271. package/build/dist/UI/Components/SideMenu/SideMenu.js +15 -5
  272. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  273. package/build/dist/UI/Components/Table/Table.js +46 -13
  274. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  275. package/build/dist/UI/Components/Table/TableRow.js +11 -6
  276. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  277. package/build/dist/UI/Utils/Project.js +161 -15
  278. package/build/dist/UI/Utils/Project.js.map +1 -1
  279. package/build/dist/UI/Utils/TableColumnsToCsv.js +67 -6
  280. package/build/dist/UI/Utils/TableColumnsToCsv.js.map +1 -1
  281. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js +52 -0
  282. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js.map +1 -0
  283. package/build/dist/Utils/UserPreferences.js +33 -0
  284. package/build/dist/Utils/UserPreferences.js.map +1 -1
  285. package/package.json +4 -3
@@ -0,0 +1,1390 @@
1
+ import WorkspaceNotificationRuleService, {
2
+ MessageBlocksByWorkspaceType,
3
+ NotificationFor,
4
+ } from "../../../Server/Services/WorkspaceNotificationRuleService";
5
+ import WorkspaceProjectAuthTokenService from "../../../Server/Services/WorkspaceProjectAuthTokenService";
6
+ import WorkspaceUserAuthTokenService from "../../../Server/Services/WorkspaceUserAuthTokenService";
7
+ import WorkspaceNotificationLogService from "../../../Server/Services/WorkspaceNotificationLogService";
8
+ import WorkspaceUtil from "../../../Server/Utils/Workspace/Workspace";
9
+ import {
10
+ WorkspaceChannel,
11
+ WorkspaceSendMessageResponse,
12
+ } from "../../../Server/Utils/Workspace/WorkspaceBase";
13
+ import WorkspaceNotificationRule from "../../../Models/DatabaseModels/WorkspaceNotificationRule";
14
+ import WorkspaceProjectAuthToken, {
15
+ MicrosoftTeamsChat,
16
+ MicrosoftTeamsMiscData,
17
+ } from "../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
18
+ import WorkspaceUserAuthToken from "../../../Models/DatabaseModels/WorkspaceUserAuthToken";
19
+ import WorkspaceNotificationLog from "../../../Models/DatabaseModels/WorkspaceNotificationLog";
20
+ import BaseNotificationRule from "../../../Types/Workspace/NotificationRules/BaseNotificationRule";
21
+ import NotificationRuleEventType from "../../../Types/Workspace/NotificationRules/EventType";
22
+ import FilterCondition from "../../../Types/Filter/FilterCondition";
23
+ import WorkspaceType from "../../../Types/Workspace/WorkspaceType";
24
+ import WorkspaceNotificationStatus from "../../../Types/Workspace/WorkspaceNotificationStatus";
25
+ import WorkspaceNotificationActionType from "../../../Types/Workspace/WorkspaceNotificationActionType";
26
+ import WorkspaceMessagePayload, {
27
+ WorkspacePayloadMarkdown,
28
+ } from "../../../Types/Workspace/WorkspaceMessagePayload";
29
+ import ObjectID from "../../../Types/ObjectID";
30
+ import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
31
+
32
+ /*
33
+ * Tests for the Microsoft Teams CHAT (group chat / personal 1:1 chat) support
34
+ * in WorkspaceNotificationRuleService:
35
+ *
36
+ * - getExistingChatIdsFromNotificationRules: pure extractor for chat ids from
37
+ * notification rules (toggle + dedupe + empty-id filtering).
38
+ * - getExistingChatIdsBasedOnEventType: Teams-only guard + rule matching.
39
+ * - getConnectedMicrosoftTeamsChats: reads miscData.availableChats off the
40
+ * project auth token.
41
+ * - sendWorkspaceMarkdownNotification: builds one payload per chat id with
42
+ * channelNames/channelIds empty and chatIds=[id] — Teams only.
43
+ * - testRule: the chat block at the end (validation, payload, logging and
44
+ * error wrapping).
45
+ */
46
+
47
+ type BaseRuleOverrides = Partial<BaseNotificationRule> & {
48
+ shouldCreateNewChannel?: boolean;
49
+ };
50
+
51
+ function makeBaseRule(overrides: BaseRuleOverrides): BaseNotificationRule {
52
+ return {
53
+ _type: "NotificationRule",
54
+ filterCondition: FilterCondition.Any,
55
+ filters: [],
56
+ shouldPostToExistingChannel: false,
57
+ existingChannelNames: "",
58
+ shouldCreateNewChannel: false,
59
+ ...overrides,
60
+ } as BaseNotificationRule;
61
+ }
62
+
63
+ function makeWorkspaceRule(data: {
64
+ workspaceType: WorkspaceType;
65
+ notificationRule: BaseNotificationRule;
66
+ name?: string | undefined;
67
+ }): WorkspaceNotificationRule {
68
+ const rule: WorkspaceNotificationRule = new WorkspaceNotificationRule();
69
+ rule.id = ObjectID.generate();
70
+ rule.projectId = ObjectID.generate();
71
+ rule.workspaceType = data.workspaceType;
72
+ rule.eventType = NotificationRuleEventType.Incident;
73
+ rule.name = data.name || "Chat Rule";
74
+ rule.notificationRule = data.notificationRule;
75
+ return rule;
76
+ }
77
+
78
+ function makeChat(id: string): MicrosoftTeamsChat {
79
+ return {
80
+ id: id,
81
+ name: `Chat ${id}`,
82
+ chatType: "groupChat",
83
+ };
84
+ }
85
+
86
+ function makeTeamsMiscData(
87
+ availableChats: Record<string, MicrosoftTeamsChat> | undefined,
88
+ ): MicrosoftTeamsMiscData {
89
+ const miscData: MicrosoftTeamsMiscData = {
90
+ tenantId: "tenant-1",
91
+ teamId: "team-1",
92
+ teamName: "Team One",
93
+ botId: "bot-1",
94
+ };
95
+
96
+ if (availableChats) {
97
+ miscData.availableChats = availableChats;
98
+ }
99
+
100
+ return miscData;
101
+ }
102
+
103
+ function makeChannel(data: {
104
+ id: string;
105
+ name: string;
106
+ workspaceType: WorkspaceType;
107
+ teamId?: string | undefined;
108
+ }): WorkspaceChannel {
109
+ const channel: WorkspaceChannel = {
110
+ id: data.id,
111
+ name: data.name,
112
+ workspaceType: data.workspaceType,
113
+ };
114
+
115
+ if (data.teamId) {
116
+ channel.teamId = data.teamId;
117
+ }
118
+
119
+ return channel;
120
+ }
121
+
122
+ function makeSendResponse(data: {
123
+ workspaceType: WorkspaceType;
124
+ threads: Array<{ channelId: string; channelName: string; threadId: string }>;
125
+ errors?:
126
+ | Array<{ channelId: string; channelName: string; error: string }>
127
+ | undefined;
128
+ }): WorkspaceSendMessageResponse {
129
+ const response: WorkspaceSendMessageResponse = {
130
+ workspaceType: data.workspaceType,
131
+ threads: data.threads.map(
132
+ (thread: {
133
+ channelId: string;
134
+ channelName: string;
135
+ threadId: string;
136
+ }) => {
137
+ return {
138
+ channel: makeChannel({
139
+ id: thread.channelId,
140
+ name: thread.channelName,
141
+ workspaceType: data.workspaceType,
142
+ }),
143
+ threadId: thread.threadId,
144
+ };
145
+ },
146
+ ),
147
+ };
148
+
149
+ if (data.errors) {
150
+ response.errors = data.errors.map(
151
+ (error: { channelId: string; channelName: string; error: string }) => {
152
+ return {
153
+ channel: makeChannel({
154
+ id: error.channelId,
155
+ name: error.channelName,
156
+ workspaceType: data.workspaceType,
157
+ }),
158
+ error: error.error,
159
+ };
160
+ },
161
+ );
162
+ }
163
+
164
+ return response;
165
+ }
166
+
167
+ function markdownBlock(text: string): WorkspacePayloadMarkdown {
168
+ return {
169
+ _type: "WorkspacePayloadMarkdown",
170
+ text: text,
171
+ };
172
+ }
173
+
174
+ afterEach(() => {
175
+ jest.restoreAllMocks();
176
+ });
177
+
178
+ describe("WorkspaceNotificationRuleService.getExistingChatIdsFromNotificationRules", () => {
179
+ test("collects chat ids from a rule with the chat toggle on", () => {
180
+ const result: Array<string> =
181
+ WorkspaceNotificationRuleService.getExistingChatIdsFromNotificationRules({
182
+ notificationRules: [
183
+ makeBaseRule({
184
+ shouldPostToExistingChat: true,
185
+ existingChatIds: ["chat-1", "chat-2"],
186
+ }),
187
+ ],
188
+ });
189
+
190
+ expect(result).toEqual(["chat-1", "chat-2"]);
191
+ });
192
+
193
+ test("skips rules where the chat toggle is explicitly false", () => {
194
+ const result: Array<string> =
195
+ WorkspaceNotificationRuleService.getExistingChatIdsFromNotificationRules({
196
+ notificationRules: [
197
+ makeBaseRule({
198
+ shouldPostToExistingChat: false,
199
+ existingChatIds: ["chat-1"],
200
+ }),
201
+ ],
202
+ });
203
+
204
+ expect(result).toEqual([]);
205
+ });
206
+
207
+ test("skips rules where the chat toggle is undefined (legacy rules)", () => {
208
+ const result: Array<string> =
209
+ WorkspaceNotificationRuleService.getExistingChatIdsFromNotificationRules({
210
+ notificationRules: [
211
+ makeBaseRule({
212
+ existingChatIds: ["chat-1"],
213
+ }),
214
+ ],
215
+ });
216
+
217
+ expect(result).toEqual([]);
218
+ });
219
+
220
+ test("dedupes chat ids across multiple rules", () => {
221
+ const result: Array<string> =
222
+ WorkspaceNotificationRuleService.getExistingChatIdsFromNotificationRules({
223
+ notificationRules: [
224
+ makeBaseRule({
225
+ shouldPostToExistingChat: true,
226
+ existingChatIds: ["chat-1", "chat-2"],
227
+ }),
228
+ makeBaseRule({
229
+ shouldPostToExistingChat: true,
230
+ existingChatIds: ["chat-2", "chat-3"],
231
+ }),
232
+ ],
233
+ });
234
+
235
+ expect(result).toEqual(["chat-1", "chat-2", "chat-3"]);
236
+ });
237
+
238
+ test("dedupes chat ids repeated within a single rule", () => {
239
+ const result: Array<string> =
240
+ WorkspaceNotificationRuleService.getExistingChatIdsFromNotificationRules({
241
+ notificationRules: [
242
+ makeBaseRule({
243
+ shouldPostToExistingChat: true,
244
+ existingChatIds: ["chat-1", "chat-1", "chat-2"],
245
+ }),
246
+ ],
247
+ });
248
+
249
+ expect(result).toEqual(["chat-1", "chat-2"]);
250
+ });
251
+
252
+ test("skips empty-string chat ids", () => {
253
+ const result: Array<string> =
254
+ WorkspaceNotificationRuleService.getExistingChatIdsFromNotificationRules({
255
+ notificationRules: [
256
+ makeBaseRule({
257
+ shouldPostToExistingChat: true,
258
+ existingChatIds: ["", "chat-1", ""],
259
+ }),
260
+ ],
261
+ });
262
+
263
+ expect(result).toEqual(["chat-1"]);
264
+ });
265
+
266
+ test("handles a toggled-on rule with undefined existingChatIds", () => {
267
+ const result: Array<string> =
268
+ WorkspaceNotificationRuleService.getExistingChatIdsFromNotificationRules({
269
+ notificationRules: [
270
+ makeBaseRule({
271
+ shouldPostToExistingChat: true,
272
+ }),
273
+ ],
274
+ });
275
+
276
+ expect(result).toEqual([]);
277
+ });
278
+
279
+ test("preserves first-seen order across rules", () => {
280
+ const result: Array<string> =
281
+ WorkspaceNotificationRuleService.getExistingChatIdsFromNotificationRules({
282
+ notificationRules: [
283
+ makeBaseRule({
284
+ shouldPostToExistingChat: true,
285
+ existingChatIds: ["z-chat", "a-chat"],
286
+ }),
287
+ makeBaseRule({
288
+ shouldPostToExistingChat: true,
289
+ existingChatIds: ["m-chat", "a-chat"],
290
+ }),
291
+ ],
292
+ });
293
+
294
+ expect(result).toEqual(["z-chat", "a-chat", "m-chat"]);
295
+ });
296
+
297
+ test("returns an empty array for an empty rules array", () => {
298
+ const result: Array<string> =
299
+ WorkspaceNotificationRuleService.getExistingChatIdsFromNotificationRules({
300
+ notificationRules: [],
301
+ });
302
+
303
+ expect(result).toEqual([]);
304
+ });
305
+ });
306
+
307
+ describe("WorkspaceNotificationRuleService.getExistingChatIdsBasedOnEventType", () => {
308
+ test("returns [] for Slack and never evaluates rules (short-circuit)", async () => {
309
+ const matchSpy: jest.SpyInstance = jest
310
+ .spyOn(WorkspaceNotificationRuleService, "getMatchingNotificationRules")
311
+ .mockResolvedValue([]);
312
+
313
+ const notificationFor: NotificationFor = {
314
+ incidentId: ObjectID.generate(),
315
+ };
316
+
317
+ const result: Array<string> =
318
+ await WorkspaceNotificationRuleService.getExistingChatIdsBasedOnEventType(
319
+ {
320
+ projectId: ObjectID.generate(),
321
+ workspaceType: WorkspaceType.Slack,
322
+ notificationRuleEventType: NotificationRuleEventType.Incident,
323
+ notificationFor: notificationFor,
324
+ },
325
+ );
326
+
327
+ expect(result).toEqual([]);
328
+ expect(matchSpy).not.toHaveBeenCalled();
329
+ });
330
+
331
+ test("for Microsoft Teams, calls getMatchingNotificationRules with the exact arguments", async () => {
332
+ const matchSpy: jest.SpyInstance = jest
333
+ .spyOn(WorkspaceNotificationRuleService, "getMatchingNotificationRules")
334
+ .mockResolvedValue([]);
335
+
336
+ const projectId: ObjectID = ObjectID.generate();
337
+ const notificationFor: NotificationFor = {
338
+ incidentId: ObjectID.generate(),
339
+ };
340
+
341
+ await WorkspaceNotificationRuleService.getExistingChatIdsBasedOnEventType({
342
+ projectId: projectId,
343
+ workspaceType: WorkspaceType.MicrosoftTeams,
344
+ notificationRuleEventType: NotificationRuleEventType.Incident,
345
+ notificationFor: notificationFor,
346
+ });
347
+
348
+ expect(matchSpy).toHaveBeenCalledTimes(1);
349
+ expect(matchSpy).toHaveBeenCalledWith({
350
+ projectId: projectId,
351
+ workspaceType: WorkspaceType.MicrosoftTeams,
352
+ notificationRuleEventType: NotificationRuleEventType.Incident,
353
+ notificationFor: notificationFor,
354
+ });
355
+ });
356
+
357
+ test("maps matching rules through the extractor and merges + dedupes chat ids", async () => {
358
+ const ruleA: WorkspaceNotificationRule = makeWorkspaceRule({
359
+ workspaceType: WorkspaceType.MicrosoftTeams,
360
+ notificationRule: makeBaseRule({
361
+ shouldPostToExistingChat: true,
362
+ existingChatIds: ["chat-1", "chat-2"],
363
+ }),
364
+ });
365
+ const ruleB: WorkspaceNotificationRule = makeWorkspaceRule({
366
+ workspaceType: WorkspaceType.MicrosoftTeams,
367
+ notificationRule: makeBaseRule({
368
+ shouldPostToExistingChat: true,
369
+ existingChatIds: ["chat-2", "chat-3"],
370
+ }),
371
+ });
372
+
373
+ jest
374
+ .spyOn(WorkspaceNotificationRuleService, "getMatchingNotificationRules")
375
+ .mockResolvedValue([ruleA, ruleB]);
376
+
377
+ const result: Array<string> =
378
+ await WorkspaceNotificationRuleService.getExistingChatIdsBasedOnEventType(
379
+ {
380
+ projectId: ObjectID.generate(),
381
+ workspaceType: WorkspaceType.MicrosoftTeams,
382
+ notificationRuleEventType: NotificationRuleEventType.Incident,
383
+ notificationFor: { incidentId: ObjectID.generate() },
384
+ },
385
+ );
386
+
387
+ expect(result).toEqual(["chat-1", "chat-2", "chat-3"]);
388
+ });
389
+
390
+ test("matching rules with the chat toggle off contribute no chat ids", async () => {
391
+ const ruleOn: WorkspaceNotificationRule = makeWorkspaceRule({
392
+ workspaceType: WorkspaceType.MicrosoftTeams,
393
+ notificationRule: makeBaseRule({
394
+ shouldPostToExistingChat: true,
395
+ existingChatIds: ["chat-1"],
396
+ }),
397
+ });
398
+ const ruleOff: WorkspaceNotificationRule = makeWorkspaceRule({
399
+ workspaceType: WorkspaceType.MicrosoftTeams,
400
+ notificationRule: makeBaseRule({
401
+ shouldPostToExistingChat: false,
402
+ existingChatIds: ["chat-9"],
403
+ }),
404
+ });
405
+
406
+ jest
407
+ .spyOn(WorkspaceNotificationRuleService, "getMatchingNotificationRules")
408
+ .mockResolvedValue([ruleOn, ruleOff]);
409
+
410
+ const result: Array<string> =
411
+ await WorkspaceNotificationRuleService.getExistingChatIdsBasedOnEventType(
412
+ {
413
+ projectId: ObjectID.generate(),
414
+ workspaceType: WorkspaceType.MicrosoftTeams,
415
+ notificationRuleEventType: NotificationRuleEventType.Incident,
416
+ notificationFor: { incidentId: ObjectID.generate() },
417
+ },
418
+ );
419
+
420
+ expect(result).toEqual(["chat-1"]);
421
+ });
422
+
423
+ test("returns [] for Microsoft Teams when no rules match", async () => {
424
+ jest
425
+ .spyOn(WorkspaceNotificationRuleService, "getMatchingNotificationRules")
426
+ .mockResolvedValue([]);
427
+
428
+ const result: Array<string> =
429
+ await WorkspaceNotificationRuleService.getExistingChatIdsBasedOnEventType(
430
+ {
431
+ projectId: ObjectID.generate(),
432
+ workspaceType: WorkspaceType.MicrosoftTeams,
433
+ notificationRuleEventType: NotificationRuleEventType.Incident,
434
+ notificationFor: { incidentId: ObjectID.generate() },
435
+ },
436
+ );
437
+
438
+ expect(result).toEqual([]);
439
+ });
440
+ });
441
+
442
+ describe("WorkspaceNotificationRuleService.getConnectedMicrosoftTeamsChats", () => {
443
+ test("returns {} when no project auth token exists", async () => {
444
+ jest
445
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
446
+ .mockResolvedValue(null);
447
+
448
+ const result: Record<string, MicrosoftTeamsChat> =
449
+ await WorkspaceNotificationRuleService.getConnectedMicrosoftTeamsChats({
450
+ projectId: ObjectID.generate(),
451
+ });
452
+
453
+ expect(result).toEqual({});
454
+ });
455
+
456
+ test("returns {} when the project auth token has no miscData", async () => {
457
+ const projectAuth: WorkspaceProjectAuthToken =
458
+ new WorkspaceProjectAuthToken();
459
+
460
+ jest
461
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
462
+ .mockResolvedValue(projectAuth);
463
+
464
+ const result: Record<string, MicrosoftTeamsChat> =
465
+ await WorkspaceNotificationRuleService.getConnectedMicrosoftTeamsChats({
466
+ projectId: ObjectID.generate(),
467
+ });
468
+
469
+ expect(result).toEqual({});
470
+ });
471
+
472
+ test("returns {} when miscData exists but has no availableChats", async () => {
473
+ const projectAuth: WorkspaceProjectAuthToken =
474
+ new WorkspaceProjectAuthToken();
475
+ projectAuth.miscData = makeTeamsMiscData(undefined);
476
+
477
+ jest
478
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
479
+ .mockResolvedValue(projectAuth);
480
+
481
+ const result: Record<string, MicrosoftTeamsChat> =
482
+ await WorkspaceNotificationRuleService.getConnectedMicrosoftTeamsChats({
483
+ projectId: ObjectID.generate(),
484
+ });
485
+
486
+ expect(result).toEqual({});
487
+ });
488
+
489
+ test("returns the availableChats map and queries auth for Microsoft Teams", async () => {
490
+ const availableChats: Record<string, MicrosoftTeamsChat> = {
491
+ "chat-1": makeChat("chat-1"),
492
+ "chat-2": {
493
+ id: "chat-2",
494
+ name: "Personal chat",
495
+ chatType: "personal",
496
+ serviceUrl: "https://smba.trafficmanager.net/amer/",
497
+ addedAt: "2026-07-27T00:00:00.000Z",
498
+ },
499
+ };
500
+
501
+ const projectAuth: WorkspaceProjectAuthToken =
502
+ new WorkspaceProjectAuthToken();
503
+ projectAuth.miscData = makeTeamsMiscData(availableChats);
504
+
505
+ const getProjectAuthSpy: jest.SpyInstance = jest
506
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
507
+ .mockResolvedValue(projectAuth);
508
+
509
+ const projectId: ObjectID = ObjectID.generate();
510
+
511
+ const result: Record<string, MicrosoftTeamsChat> =
512
+ await WorkspaceNotificationRuleService.getConnectedMicrosoftTeamsChats({
513
+ projectId: projectId,
514
+ });
515
+
516
+ expect(result).toEqual(availableChats);
517
+ expect(getProjectAuthSpy).toHaveBeenCalledWith({
518
+ projectId: projectId,
519
+ workspaceType: WorkspaceType.MicrosoftTeams,
520
+ });
521
+ });
522
+ });
523
+
524
+ describe("WorkspaceNotificationRuleService.sendWorkspaceMarkdownNotification (chat payloads)", () => {
525
+ let projectId: ObjectID;
526
+ let notificationFor: NotificationFor;
527
+
528
+ interface MarkdownSendMocks {
529
+ postSpy: jest.SpyInstance;
530
+ createLogSpy: jest.SpyInstance;
531
+ chatIdsSpy: jest.SpyInstance;
532
+ existingChannelsSpy: jest.SpyInstance;
533
+ monitorChannelsSpy: jest.SpyInstance;
534
+ }
535
+
536
+ beforeEach(() => {
537
+ projectId = ObjectID.generate();
538
+ notificationFor = { incidentId: ObjectID.generate() };
539
+ });
540
+
541
+ function mockMarkdownSendDeps(data: {
542
+ blocks: Array<MessageBlocksByWorkspaceType>;
543
+ existingChannelsByWorkspaceType: Partial<
544
+ Record<WorkspaceType, Array<WorkspaceChannel>>
545
+ >;
546
+ monitorChannelsByWorkspaceType: Partial<
547
+ Record<WorkspaceType, Array<WorkspaceChannel>>
548
+ >;
549
+ chatIdsByWorkspaceType: Partial<Record<WorkspaceType, Array<string>>>;
550
+ responses: Array<WorkspaceSendMessageResponse>;
551
+ }): MarkdownSendMocks {
552
+ jest
553
+ .spyOn(WorkspaceUtil, "getMessageBlocksByMarkdown")
554
+ .mockResolvedValue(data.blocks);
555
+
556
+ const existingChannelsSpy: jest.SpyInstance = jest
557
+ .spyOn(
558
+ WorkspaceNotificationRuleService,
559
+ "getExistingChannelNamesBasedOnEventType",
560
+ )
561
+ .mockImplementation(
562
+ (args: {
563
+ projectId: ObjectID;
564
+ workspaceType: WorkspaceType;
565
+ notificationRuleEventType: NotificationRuleEventType;
566
+ notificationFor: NotificationFor;
567
+ }): Promise<Array<WorkspaceChannel>> => {
568
+ return Promise.resolve(
569
+ data.existingChannelsByWorkspaceType[args.workspaceType] || [],
570
+ );
571
+ },
572
+ );
573
+
574
+ const monitorChannelsSpy: jest.SpyInstance = jest
575
+ .spyOn(
576
+ WorkspaceNotificationRuleService as any,
577
+ "getWorkspaceChannelsByNotificationFor",
578
+ )
579
+ .mockImplementation(
580
+ (...args: Array<unknown>): Promise<Array<WorkspaceChannel>> => {
581
+ const callData: { workspaceType: WorkspaceType } = args[0] as {
582
+ workspaceType: WorkspaceType;
583
+ };
584
+ return Promise.resolve(
585
+ data.monitorChannelsByWorkspaceType[callData.workspaceType] || [],
586
+ );
587
+ },
588
+ );
589
+
590
+ const chatIdsSpy: jest.SpyInstance = jest
591
+ .spyOn(
592
+ WorkspaceNotificationRuleService,
593
+ "getExistingChatIdsBasedOnEventType",
594
+ )
595
+ .mockImplementation(
596
+ (args: {
597
+ projectId: ObjectID;
598
+ workspaceType: WorkspaceType;
599
+ notificationRuleEventType: NotificationRuleEventType;
600
+ notificationFor: NotificationFor;
601
+ }): Promise<Array<string>> => {
602
+ return Promise.resolve(
603
+ data.chatIdsByWorkspaceType[args.workspaceType] || [],
604
+ );
605
+ },
606
+ );
607
+
608
+ const postSpy: jest.SpyInstance = jest
609
+ .spyOn(WorkspaceUtil, "postMessageToAllWorkspaceChannelsAsBot")
610
+ .mockResolvedValue(data.responses);
611
+
612
+ const createLogSpy: jest.SpyInstance = jest
613
+ .spyOn(WorkspaceNotificationLogService, "create")
614
+ .mockResolvedValue(new WorkspaceNotificationLog());
615
+
616
+ return {
617
+ postSpy,
618
+ createLogSpy,
619
+ chatIdsSpy,
620
+ existingChannelsSpy,
621
+ monitorChannelsSpy,
622
+ };
623
+ }
624
+
625
+ function getSentPayloads(
626
+ postSpy: jest.SpyInstance,
627
+ ): Array<WorkspaceMessagePayload> {
628
+ expect(postSpy).toHaveBeenCalledTimes(1);
629
+ const callArg: {
630
+ projectId: ObjectID;
631
+ messagePayloadsByWorkspace: Array<WorkspaceMessagePayload>;
632
+ } = postSpy.mock.calls[0]?.[0] as {
633
+ projectId: ObjectID;
634
+ messagePayloadsByWorkspace: Array<WorkspaceMessagePayload>;
635
+ };
636
+ return callArg.messagePayloadsByWorkspace;
637
+ }
638
+
639
+ test("builds one chat payload per chat id with empty channelNames/channelIds", async () => {
640
+ const teamsBlocks: MessageBlocksByWorkspaceType = {
641
+ workspaceType: WorkspaceType.MicrosoftTeams,
642
+ messageBlocks: [markdownBlock("Incident update")],
643
+ };
644
+
645
+ const mocks: MarkdownSendMocks = mockMarkdownSendDeps({
646
+ blocks: [teamsBlocks],
647
+ existingChannelsByWorkspaceType: {},
648
+ monitorChannelsByWorkspaceType: {},
649
+ chatIdsByWorkspaceType: {
650
+ [WorkspaceType.MicrosoftTeams]: ["chat-a", "chat-b"],
651
+ },
652
+ responses: [],
653
+ });
654
+
655
+ await WorkspaceNotificationRuleService.sendWorkspaceMarkdownNotification({
656
+ projectId: projectId,
657
+ notificationFor: notificationFor,
658
+ feedInfoInMarkdown: "Incident update",
659
+ workspaceNotification: {
660
+ sendWorkspaceNotification: true,
661
+ },
662
+ });
663
+
664
+ const payloads: Array<WorkspaceMessagePayload> = getSentPayloads(
665
+ mocks.postSpy,
666
+ );
667
+
668
+ expect(payloads).toHaveLength(2);
669
+
670
+ expect(payloads[0]).toEqual({
671
+ _type: "WorkspaceMessagePayload",
672
+ workspaceType: WorkspaceType.MicrosoftTeams,
673
+ messageBlocks: teamsBlocks.messageBlocks,
674
+ channelNames: [],
675
+ channelIds: [],
676
+ chatIds: ["chat-a"],
677
+ });
678
+
679
+ expect(payloads[1]).toEqual({
680
+ _type: "WorkspaceMessagePayload",
681
+ workspaceType: WorkspaceType.MicrosoftTeams,
682
+ messageBlocks: teamsBlocks.messageBlocks,
683
+ channelNames: [],
684
+ channelIds: [],
685
+ chatIds: ["chat-b"],
686
+ });
687
+ });
688
+
689
+ test("chat payloads coexist with unchanged channel payloads (persisted -> channelIds, named -> channelNames)", async () => {
690
+ const teamsBlocks: MessageBlocksByWorkspaceType = {
691
+ workspaceType: WorkspaceType.MicrosoftTeams,
692
+ messageBlocks: [markdownBlock("Incident update")],
693
+ };
694
+
695
+ const persistedChannel: WorkspaceChannel = makeChannel({
696
+ id: "chan-123",
697
+ name: "persisted-channel",
698
+ workspaceType: WorkspaceType.MicrosoftTeams,
699
+ teamId: "team-1",
700
+ });
701
+
702
+ const namedChannel: WorkspaceChannel = makeChannel({
703
+ id: "",
704
+ name: "named-channel",
705
+ workspaceType: WorkspaceType.MicrosoftTeams,
706
+ teamId: "team-1",
707
+ });
708
+
709
+ const mocks: MarkdownSendMocks = mockMarkdownSendDeps({
710
+ blocks: [teamsBlocks],
711
+ existingChannelsByWorkspaceType: {
712
+ [WorkspaceType.MicrosoftTeams]: [namedChannel],
713
+ },
714
+ monitorChannelsByWorkspaceType: {
715
+ [WorkspaceType.MicrosoftTeams]: [persistedChannel],
716
+ },
717
+ chatIdsByWorkspaceType: {
718
+ [WorkspaceType.MicrosoftTeams]: ["chat-a"],
719
+ },
720
+ responses: [],
721
+ });
722
+
723
+ await WorkspaceNotificationRuleService.sendWorkspaceMarkdownNotification({
724
+ projectId: projectId,
725
+ notificationFor: notificationFor,
726
+ feedInfoInMarkdown: "Incident update",
727
+ workspaceNotification: {
728
+ sendWorkspaceNotification: true,
729
+ },
730
+ });
731
+
732
+ const payloads: Array<WorkspaceMessagePayload> = getSentPayloads(
733
+ mocks.postSpy,
734
+ );
735
+
736
+ expect(payloads).toHaveLength(3);
737
+
738
+ // Persisted channel payload: by channel id, unchanged by the chat feature.
739
+ expect(payloads[0]).toEqual({
740
+ _type: "WorkspaceMessagePayload",
741
+ workspaceType: WorkspaceType.MicrosoftTeams,
742
+ messageBlocks: teamsBlocks.messageBlocks,
743
+ channelNames: [],
744
+ channelIds: ["chan-123"],
745
+ teamId: "team-1",
746
+ });
747
+
748
+ // Named channel payload: by channel name, unchanged by the chat feature.
749
+ expect(payloads[1]).toEqual({
750
+ _type: "WorkspaceMessagePayload",
751
+ workspaceType: WorkspaceType.MicrosoftTeams,
752
+ messageBlocks: teamsBlocks.messageBlocks,
753
+ channelNames: ["named-channel"],
754
+ channelIds: [],
755
+ teamId: "team-1",
756
+ });
757
+
758
+ // Chat payload comes last and carries no teamId.
759
+ expect(payloads[2]).toEqual({
760
+ _type: "WorkspaceMessagePayload",
761
+ workspaceType: WorkspaceType.MicrosoftTeams,
762
+ messageBlocks: teamsBlocks.messageBlocks,
763
+ channelNames: [],
764
+ channelIds: [],
765
+ chatIds: ["chat-a"],
766
+ });
767
+ expect(payloads[2]).not.toHaveProperty("teamId");
768
+ });
769
+
770
+ test("with Slack + Teams blocks, chat payloads are built ONLY for Teams", async () => {
771
+ const teamsBlocks: MessageBlocksByWorkspaceType = {
772
+ workspaceType: WorkspaceType.MicrosoftTeams,
773
+ messageBlocks: [markdownBlock("Teams text")],
774
+ };
775
+ const slackBlocks: MessageBlocksByWorkspaceType = {
776
+ workspaceType: WorkspaceType.Slack,
777
+ messageBlocks: [markdownBlock("Slack text")],
778
+ };
779
+
780
+ const slackChannel: WorkspaceChannel = makeChannel({
781
+ id: "slack-chan-1",
782
+ name: "slack-channel",
783
+ workspaceType: WorkspaceType.Slack,
784
+ });
785
+
786
+ const mocks: MarkdownSendMocks = mockMarkdownSendDeps({
787
+ blocks: [slackBlocks, teamsBlocks],
788
+ existingChannelsByWorkspaceType: {},
789
+ monitorChannelsByWorkspaceType: {
790
+ [WorkspaceType.Slack]: [slackChannel],
791
+ },
792
+ chatIdsByWorkspaceType: {
793
+ [WorkspaceType.MicrosoftTeams]: ["chat-a", "chat-b"],
794
+ [WorkspaceType.Slack]: [],
795
+ },
796
+ responses: [],
797
+ });
798
+
799
+ await WorkspaceNotificationRuleService.sendWorkspaceMarkdownNotification({
800
+ projectId: projectId,
801
+ notificationFor: notificationFor,
802
+ feedInfoInMarkdown: "Update",
803
+ workspaceNotification: {
804
+ sendWorkspaceNotification: true,
805
+ },
806
+ });
807
+
808
+ const payloads: Array<WorkspaceMessagePayload> = getSentPayloads(
809
+ mocks.postSpy,
810
+ );
811
+
812
+ const chatPayloads: Array<WorkspaceMessagePayload> = payloads.filter(
813
+ (payload: WorkspaceMessagePayload) => {
814
+ return payload.chatIds && payload.chatIds.length > 0;
815
+ },
816
+ );
817
+
818
+ expect(chatPayloads).toHaveLength(2);
819
+ for (const chatPayload of chatPayloads) {
820
+ expect(chatPayload.workspaceType).toBe(WorkspaceType.MicrosoftTeams);
821
+ }
822
+
823
+ const slackPayloads: Array<WorkspaceMessagePayload> = payloads.filter(
824
+ (payload: WorkspaceMessagePayload) => {
825
+ return payload.workspaceType === WorkspaceType.Slack;
826
+ },
827
+ );
828
+
829
+ expect(slackPayloads).toHaveLength(1);
830
+ expect(slackPayloads[0]?.channelIds).toEqual(["slack-chan-1"]);
831
+ expect(slackPayloads[0]?.chatIds).toBeUndefined();
832
+ });
833
+
834
+ test("queries chat ids with the correct event type and workspace type per block", async () => {
835
+ const teamsBlocks: MessageBlocksByWorkspaceType = {
836
+ workspaceType: WorkspaceType.MicrosoftTeams,
837
+ messageBlocks: [markdownBlock("Teams text")],
838
+ };
839
+ const slackBlocks: MessageBlocksByWorkspaceType = {
840
+ workspaceType: WorkspaceType.Slack,
841
+ messageBlocks: [markdownBlock("Slack text")],
842
+ };
843
+
844
+ const mocks: MarkdownSendMocks = mockMarkdownSendDeps({
845
+ blocks: [slackBlocks, teamsBlocks],
846
+ existingChannelsByWorkspaceType: {},
847
+ monitorChannelsByWorkspaceType: {},
848
+ chatIdsByWorkspaceType: {},
849
+ responses: [],
850
+ });
851
+
852
+ await WorkspaceNotificationRuleService.sendWorkspaceMarkdownNotification({
853
+ projectId: projectId,
854
+ notificationFor: notificationFor,
855
+ feedInfoInMarkdown: "Update",
856
+ workspaceNotification: {
857
+ sendWorkspaceNotification: true,
858
+ },
859
+ });
860
+
861
+ expect(mocks.chatIdsSpy).toHaveBeenCalledTimes(2);
862
+ expect(mocks.chatIdsSpy).toHaveBeenCalledWith({
863
+ projectId: projectId,
864
+ notificationRuleEventType: NotificationRuleEventType.Incident,
865
+ workspaceType: WorkspaceType.Slack,
866
+ notificationFor: notificationFor,
867
+ });
868
+ expect(mocks.chatIdsSpy).toHaveBeenCalledWith({
869
+ projectId: projectId,
870
+ notificationRuleEventType: NotificationRuleEventType.Incident,
871
+ workspaceType: WorkspaceType.MicrosoftTeams,
872
+ notificationFor: notificationFor,
873
+ });
874
+ });
875
+
876
+ test("posts once with all payloads and no chat payloads when no chats are configured", async () => {
877
+ const teamsBlocks: MessageBlocksByWorkspaceType = {
878
+ workspaceType: WorkspaceType.MicrosoftTeams,
879
+ messageBlocks: [markdownBlock("Teams text")],
880
+ };
881
+
882
+ const mocks: MarkdownSendMocks = mockMarkdownSendDeps({
883
+ blocks: [teamsBlocks],
884
+ existingChannelsByWorkspaceType: {},
885
+ monitorChannelsByWorkspaceType: {},
886
+ chatIdsByWorkspaceType: {},
887
+ responses: [],
888
+ });
889
+
890
+ await WorkspaceNotificationRuleService.sendWorkspaceMarkdownNotification({
891
+ projectId: projectId,
892
+ notificationFor: notificationFor,
893
+ feedInfoInMarkdown: "Update",
894
+ workspaceNotification: {
895
+ sendWorkspaceNotification: true,
896
+ },
897
+ });
898
+
899
+ const payloads: Array<WorkspaceMessagePayload> = getSentPayloads(
900
+ mocks.postSpy,
901
+ );
902
+
903
+ expect(payloads).toEqual([]);
904
+ });
905
+
906
+ test("logs a Success notification with channelId = chat id when the response thread is a chat", async () => {
907
+ const teamsBlocks: MessageBlocksByWorkspaceType = {
908
+ workspaceType: WorkspaceType.MicrosoftTeams,
909
+ messageBlocks: [markdownBlock("Incident update")],
910
+ };
911
+
912
+ const response: WorkspaceSendMessageResponse = makeSendResponse({
913
+ workspaceType: WorkspaceType.MicrosoftTeams,
914
+ threads: [
915
+ {
916
+ channelId: "chat-a",
917
+ channelName: "Chat chat-a",
918
+ threadId: "thread-1",
919
+ },
920
+ ],
921
+ });
922
+
923
+ const mocks: MarkdownSendMocks = mockMarkdownSendDeps({
924
+ blocks: [teamsBlocks],
925
+ existingChannelsByWorkspaceType: {},
926
+ monitorChannelsByWorkspaceType: {},
927
+ chatIdsByWorkspaceType: {
928
+ [WorkspaceType.MicrosoftTeams]: ["chat-a"],
929
+ },
930
+ responses: [response],
931
+ });
932
+
933
+ await WorkspaceNotificationRuleService.sendWorkspaceMarkdownNotification({
934
+ projectId: projectId,
935
+ notificationFor: notificationFor,
936
+ feedInfoInMarkdown: "Incident update",
937
+ workspaceNotification: {
938
+ sendWorkspaceNotification: true,
939
+ },
940
+ });
941
+
942
+ expect(mocks.createLogSpy).toHaveBeenCalledTimes(1);
943
+
944
+ const createdLog: WorkspaceNotificationLog = mocks.createLogSpy.mock
945
+ .calls[0]?.[0]?.data as WorkspaceNotificationLog;
946
+
947
+ expect(createdLog.channelId).toBe("chat-a");
948
+ expect(createdLog.channelName).toBe("Chat chat-a");
949
+ expect(createdLog.threadId).toBe("thread-1");
950
+ expect(createdLog.status).toBe(WorkspaceNotificationStatus.Success);
951
+ expect(createdLog.workspaceType).toBe(WorkspaceType.MicrosoftTeams);
952
+ expect(createdLog.message).toBe("Incident update");
953
+ expect(createdLog.incidentId).toBe(notificationFor.incidentId);
954
+ expect(createdLog.actionType).toBe(
955
+ WorkspaceNotificationActionType.SendMessage,
956
+ );
957
+ });
958
+
959
+ test("creates no logs when the send response has zero threads", async () => {
960
+ const teamsBlocks: MessageBlocksByWorkspaceType = {
961
+ workspaceType: WorkspaceType.MicrosoftTeams,
962
+ messageBlocks: [markdownBlock("Incident update")],
963
+ };
964
+
965
+ const response: WorkspaceSendMessageResponse = makeSendResponse({
966
+ workspaceType: WorkspaceType.MicrosoftTeams,
967
+ threads: [],
968
+ });
969
+
970
+ const mocks: MarkdownSendMocks = mockMarkdownSendDeps({
971
+ blocks: [teamsBlocks],
972
+ existingChannelsByWorkspaceType: {},
973
+ monitorChannelsByWorkspaceType: {},
974
+ chatIdsByWorkspaceType: {
975
+ [WorkspaceType.MicrosoftTeams]: ["chat-a"],
976
+ },
977
+ responses: [response],
978
+ });
979
+
980
+ await WorkspaceNotificationRuleService.sendWorkspaceMarkdownNotification({
981
+ projectId: projectId,
982
+ notificationFor: notificationFor,
983
+ feedInfoInMarkdown: "Incident update",
984
+ workspaceNotification: {
985
+ sendWorkspaceNotification: true,
986
+ },
987
+ });
988
+
989
+ expect(mocks.createLogSpy).not.toHaveBeenCalled();
990
+ });
991
+
992
+ test("logs an Error notification for each per-destination failure in the send response", async () => {
993
+ const teamsBlocks: MessageBlocksByWorkspaceType = {
994
+ workspaceType: WorkspaceType.MicrosoftTeams,
995
+ messageBlocks: [markdownBlock("Incident update")],
996
+ };
997
+
998
+ const response: WorkspaceSendMessageResponse = makeSendResponse({
999
+ workspaceType: WorkspaceType.MicrosoftTeams,
1000
+ threads: [
1001
+ {
1002
+ channelId: "chat-a",
1003
+ channelName: "Chat chat-a",
1004
+ threadId: "thread-1",
1005
+ },
1006
+ ],
1007
+ errors: [
1008
+ {
1009
+ channelId: "chat-b",
1010
+ channelName: "Chat chat-b",
1011
+ error:
1012
+ "This chat is not connected to OneUptime. Please add the OneUptime app to the chat in Microsoft Teams and try again.",
1013
+ },
1014
+ ],
1015
+ });
1016
+
1017
+ const mocks: MarkdownSendMocks = mockMarkdownSendDeps({
1018
+ blocks: [teamsBlocks],
1019
+ existingChannelsByWorkspaceType: {},
1020
+ monitorChannelsByWorkspaceType: {},
1021
+ chatIdsByWorkspaceType: {
1022
+ [WorkspaceType.MicrosoftTeams]: ["chat-a", "chat-b"],
1023
+ },
1024
+ responses: [response],
1025
+ });
1026
+
1027
+ await WorkspaceNotificationRuleService.sendWorkspaceMarkdownNotification({
1028
+ projectId: projectId,
1029
+ notificationFor: notificationFor,
1030
+ feedInfoInMarkdown: "Incident update",
1031
+ workspaceNotification: {
1032
+ sendWorkspaceNotification: true,
1033
+ },
1034
+ });
1035
+
1036
+ expect(mocks.createLogSpy).toHaveBeenCalledTimes(2);
1037
+
1038
+ const successLog: WorkspaceNotificationLog = mocks.createLogSpy.mock
1039
+ .calls[0]?.[0]?.data as WorkspaceNotificationLog;
1040
+ expect(successLog.status).toBe(WorkspaceNotificationStatus.Success);
1041
+ expect(successLog.channelId).toBe("chat-a");
1042
+
1043
+ const errorLog: WorkspaceNotificationLog = mocks.createLogSpy.mock
1044
+ .calls[1]?.[0]?.data as WorkspaceNotificationLog;
1045
+ expect(errorLog.status).toBe(WorkspaceNotificationStatus.Error);
1046
+ expect(errorLog.channelId).toBe("chat-b");
1047
+ expect(errorLog.channelName).toBe("Chat chat-b");
1048
+ expect(errorLog.statusMessage).toContain(
1049
+ "This chat is not connected to OneUptime",
1050
+ );
1051
+ expect(errorLog.incidentId).toBe(notificationFor.incidentId);
1052
+ });
1053
+ });
1054
+
1055
+ describe("WorkspaceNotificationRuleService.testRule (Microsoft Teams chat path)", () => {
1056
+ const testByUserId: ObjectID = ObjectID.generate();
1057
+
1058
+ interface TestRuleMocks {
1059
+ postSpy: jest.SpyInstance;
1060
+ createLogSpy: jest.SpyInstance;
1061
+ getProjectAuthSpy: jest.SpyInstance;
1062
+ }
1063
+
1064
+ function mockTestRuleDeps(data: {
1065
+ rule: WorkspaceNotificationRule;
1066
+ userAuth?: WorkspaceUserAuthToken | null | undefined;
1067
+ projectAuth?: WorkspaceProjectAuthToken | null | undefined;
1068
+ availableChats?: Record<string, MicrosoftTeamsChat> | undefined;
1069
+ responses?: Array<WorkspaceSendMessageResponse> | undefined;
1070
+ }): TestRuleMocks {
1071
+ jest
1072
+ .spyOn(WorkspaceNotificationRuleService, "findOneById")
1073
+ .mockResolvedValue(data.rule);
1074
+
1075
+ let userAuth: WorkspaceUserAuthToken | null;
1076
+ if (data.userAuth === undefined) {
1077
+ userAuth = new WorkspaceUserAuthToken();
1078
+ userAuth.workspaceUserId = "teams-user-1";
1079
+ } else {
1080
+ userAuth = data.userAuth;
1081
+ }
1082
+
1083
+ jest
1084
+ .spyOn(WorkspaceUserAuthTokenService, "findOneBy")
1085
+ .mockResolvedValue(userAuth);
1086
+
1087
+ let projectAuth: WorkspaceProjectAuthToken | null;
1088
+ if (data.projectAuth === undefined) {
1089
+ projectAuth = new WorkspaceProjectAuthToken();
1090
+ projectAuth.authToken = "project-auth-token";
1091
+ projectAuth.workspaceType = data.rule.workspaceType!;
1092
+ } else {
1093
+ projectAuth = data.projectAuth;
1094
+ }
1095
+
1096
+ jest
1097
+ .spyOn(WorkspaceProjectAuthTokenService, "findOneBy")
1098
+ .mockResolvedValue(projectAuth);
1099
+
1100
+ const authWithChats: WorkspaceProjectAuthToken =
1101
+ new WorkspaceProjectAuthToken();
1102
+ authWithChats.miscData = makeTeamsMiscData(data.availableChats || {});
1103
+
1104
+ const getProjectAuthSpy: jest.SpyInstance = jest
1105
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
1106
+ .mockResolvedValue(authWithChats);
1107
+
1108
+ const postSpy: jest.SpyInstance = jest
1109
+ .spyOn(WorkspaceUtil, "postMessageToAllWorkspaceChannelsAsBot")
1110
+ .mockResolvedValue(data.responses || []);
1111
+
1112
+ const createLogSpy: jest.SpyInstance = jest
1113
+ .spyOn(WorkspaceNotificationLogService, "create")
1114
+ .mockResolvedValue(new WorkspaceNotificationLog());
1115
+
1116
+ return { postSpy, createLogSpy, getProjectAuthSpy };
1117
+ }
1118
+
1119
+ function makeChatRule(
1120
+ overrides: BaseRuleOverrides,
1121
+ ): WorkspaceNotificationRule {
1122
+ return makeWorkspaceRule({
1123
+ workspaceType: WorkspaceType.MicrosoftTeams,
1124
+ notificationRule: makeBaseRule({
1125
+ shouldPostToExistingChat: true,
1126
+ existingChatIds: ["chat-1"],
1127
+ ...overrides,
1128
+ }),
1129
+ name: "Chat Rule",
1130
+ });
1131
+ }
1132
+
1133
+ async function runTestRule(rule: WorkspaceNotificationRule): Promise<void> {
1134
+ await WorkspaceNotificationRuleService.testRule({
1135
+ ruleId: rule.id!,
1136
+ projectId: ObjectID.generate(), // testRule overwrites this with rule.projectId.
1137
+ testByUserId: testByUserId,
1138
+ props: { isRoot: true },
1139
+ });
1140
+ }
1141
+
1142
+ test("happy path: posts a single payload with chatIds and logs a chat Success", async () => {
1143
+ const rule: WorkspaceNotificationRule = makeChatRule({});
1144
+
1145
+ const response: WorkspaceSendMessageResponse = makeSendResponse({
1146
+ workspaceType: WorkspaceType.MicrosoftTeams,
1147
+ threads: [
1148
+ {
1149
+ channelId: "chat-1",
1150
+ channelName: "Chat chat-1",
1151
+ threadId: "thread-9",
1152
+ },
1153
+ ],
1154
+ });
1155
+
1156
+ const mocks: TestRuleMocks = mockTestRuleDeps({
1157
+ rule: rule,
1158
+ availableChats: { "chat-1": makeChat("chat-1") },
1159
+ responses: [response],
1160
+ });
1161
+
1162
+ await runTestRule(rule);
1163
+
1164
+ expect(mocks.postSpy).toHaveBeenCalledTimes(1);
1165
+
1166
+ const callArg: {
1167
+ projectId: ObjectID;
1168
+ messagePayloadsByWorkspace: Array<WorkspaceMessagePayload>;
1169
+ } = mocks.postSpy.mock.calls[0]?.[0] as {
1170
+ projectId: ObjectID;
1171
+ messagePayloadsByWorkspace: Array<WorkspaceMessagePayload>;
1172
+ };
1173
+
1174
+ expect(callArg.projectId).toBe(rule.projectId);
1175
+ expect(callArg.messagePayloadsByWorkspace).toHaveLength(1);
1176
+ expect(callArg.messagePayloadsByWorkspace[0]).toEqual({
1177
+ _type: "WorkspaceMessagePayload",
1178
+ workspaceType: WorkspaceType.MicrosoftTeams,
1179
+ messageBlocks: [
1180
+ {
1181
+ _type: "WorkspacePayloadMarkdown",
1182
+ text: "This is a test message for rule **Chat Rule**",
1183
+ },
1184
+ ],
1185
+ channelNames: [],
1186
+ channelIds: [],
1187
+ chatIds: ["chat-1"],
1188
+ });
1189
+
1190
+ expect(mocks.createLogSpy).toHaveBeenCalledTimes(1);
1191
+ const createdLog: WorkspaceNotificationLog = mocks.createLogSpy.mock
1192
+ .calls[0]?.[0]?.data as WorkspaceNotificationLog;
1193
+
1194
+ expect(createdLog.channelId).toBe("chat-1");
1195
+ expect(createdLog.channelName).toBe("Chat chat-1");
1196
+ expect(createdLog.threadId).toBe("thread-9");
1197
+ expect(createdLog.status).toBe(WorkspaceNotificationStatus.Success);
1198
+ expect(createdLog.statusMessage).toBe(
1199
+ "Test message posted to workspace chat",
1200
+ );
1201
+ expect(createdLog.message).toBe(
1202
+ "This is a test message for rule **Chat Rule**",
1203
+ );
1204
+ expect(createdLog.userId).toBe(testByUserId);
1205
+ expect(createdLog.actionType).toBe(
1206
+ WorkspaceNotificationActionType.SendMessage,
1207
+ );
1208
+ expect(createdLog.projectId).toBe(rule.projectId);
1209
+ });
1210
+
1211
+ test("posts a single payload containing ALL selected chat ids and logs each thread", async () => {
1212
+ const rule: WorkspaceNotificationRule = makeChatRule({
1213
+ existingChatIds: ["chat-1", "chat-2"],
1214
+ });
1215
+
1216
+ const response: WorkspaceSendMessageResponse = makeSendResponse({
1217
+ workspaceType: WorkspaceType.MicrosoftTeams,
1218
+ threads: [
1219
+ {
1220
+ channelId: "chat-1",
1221
+ channelName: "Chat chat-1",
1222
+ threadId: "thread-1",
1223
+ },
1224
+ {
1225
+ channelId: "chat-2",
1226
+ channelName: "Chat chat-2",
1227
+ threadId: "thread-2",
1228
+ },
1229
+ ],
1230
+ });
1231
+
1232
+ const mocks: TestRuleMocks = mockTestRuleDeps({
1233
+ rule: rule,
1234
+ availableChats: {
1235
+ "chat-1": makeChat("chat-1"),
1236
+ "chat-2": makeChat("chat-2"),
1237
+ },
1238
+ responses: [response],
1239
+ });
1240
+
1241
+ await runTestRule(rule);
1242
+
1243
+ expect(mocks.postSpy).toHaveBeenCalledTimes(1);
1244
+
1245
+ const callArg: {
1246
+ messagePayloadsByWorkspace: Array<WorkspaceMessagePayload>;
1247
+ } = mocks.postSpy.mock.calls[0]?.[0] as {
1248
+ messagePayloadsByWorkspace: Array<WorkspaceMessagePayload>;
1249
+ };
1250
+
1251
+ expect(callArg.messagePayloadsByWorkspace).toHaveLength(1);
1252
+ expect(callArg.messagePayloadsByWorkspace[0]?.chatIds).toEqual([
1253
+ "chat-1",
1254
+ "chat-2",
1255
+ ]);
1256
+
1257
+ expect(mocks.createLogSpy).toHaveBeenCalledTimes(2);
1258
+ const firstLog: WorkspaceNotificationLog = mocks.createLogSpy.mock
1259
+ .calls[0]?.[0]?.data as WorkspaceNotificationLog;
1260
+ const secondLog: WorkspaceNotificationLog = mocks.createLogSpy.mock
1261
+ .calls[1]?.[0]?.data as WorkspaceNotificationLog;
1262
+ expect(firstLog.channelId).toBe("chat-1");
1263
+ expect(secondLog.channelId).toBe("chat-2");
1264
+ });
1265
+
1266
+ test("throws when a selected chat is no longer connected", async () => {
1267
+ const rule: WorkspaceNotificationRule = makeChatRule({});
1268
+
1269
+ const mocks: TestRuleMocks = mockTestRuleDeps({
1270
+ rule: rule,
1271
+ availableChats: { "some-other-chat": makeChat("some-other-chat") },
1272
+ });
1273
+
1274
+ await expect(runTestRule(rule)).rejects.toThrow(/no longer connected/);
1275
+ expect(mocks.postSpy).not.toHaveBeenCalled();
1276
+ expect(mocks.createLogSpy).not.toHaveBeenCalled();
1277
+ });
1278
+
1279
+ test("throws when the chat toggle is on but no chats are selected", async () => {
1280
+ const rule: WorkspaceNotificationRule = makeChatRule({
1281
+ existingChatIds: [],
1282
+ });
1283
+
1284
+ const mocks: TestRuleMocks = mockTestRuleDeps({
1285
+ rule: rule,
1286
+ availableChats: { "chat-1": makeChat("chat-1") },
1287
+ });
1288
+
1289
+ await expect(runTestRule(rule)).rejects.toThrow(/select at least one chat/);
1290
+ expect(mocks.getProjectAuthSpy).not.toHaveBeenCalled();
1291
+ expect(mocks.postSpy).not.toHaveBeenCalled();
1292
+ });
1293
+
1294
+ test("throws 'Failed to send test message to some chats' when the send response has errors", async () => {
1295
+ const rule: WorkspaceNotificationRule = makeChatRule({});
1296
+
1297
+ const response: WorkspaceSendMessageResponse = makeSendResponse({
1298
+ workspaceType: WorkspaceType.MicrosoftTeams,
1299
+ threads: [],
1300
+ errors: [
1301
+ {
1302
+ channelId: "chat-1",
1303
+ channelName: "Chat chat-1",
1304
+ error: "Bot was removed from the chat",
1305
+ },
1306
+ ],
1307
+ });
1308
+
1309
+ const mocks: TestRuleMocks = mockTestRuleDeps({
1310
+ rule: rule,
1311
+ availableChats: { "chat-1": makeChat("chat-1") },
1312
+ responses: [response],
1313
+ });
1314
+
1315
+ await expect(runTestRule(rule)).rejects.toThrow(
1316
+ /Failed to send test message to some chats/,
1317
+ );
1318
+ expect(mocks.createLogSpy).not.toHaveBeenCalled();
1319
+ });
1320
+
1321
+ test("throws when the testing user is not connected to the workspace", async () => {
1322
+ const rule: WorkspaceNotificationRule = makeChatRule({});
1323
+
1324
+ const mocks: TestRuleMocks = mockTestRuleDeps({
1325
+ rule: rule,
1326
+ userAuth: null,
1327
+ availableChats: { "chat-1": makeChat("chat-1") },
1328
+ });
1329
+
1330
+ await expect(runTestRule(rule)).rejects.toThrow(
1331
+ /This account is not connected to MicrosoftTeams/,
1332
+ );
1333
+ expect(mocks.postSpy).not.toHaveBeenCalled();
1334
+ });
1335
+
1336
+ test("throws when the project is not connected to the workspace", async () => {
1337
+ const rule: WorkspaceNotificationRule = makeChatRule({});
1338
+
1339
+ const mocks: TestRuleMocks = mockTestRuleDeps({
1340
+ rule: rule,
1341
+ projectAuth: null,
1342
+ availableChats: { "chat-1": makeChat("chat-1") },
1343
+ });
1344
+
1345
+ await expect(runTestRule(rule)).rejects.toThrow(
1346
+ /This project is not connected to MicrosoftTeams/,
1347
+ );
1348
+ expect(mocks.postSpy).not.toHaveBeenCalled();
1349
+ });
1350
+
1351
+ test("sends nothing when chat, channel and create-channel toggles are all off", async () => {
1352
+ const rule: WorkspaceNotificationRule = makeChatRule({
1353
+ shouldPostToExistingChat: false,
1354
+ shouldPostToExistingChannel: false,
1355
+ shouldCreateNewChannel: false,
1356
+ });
1357
+
1358
+ const mocks: TestRuleMocks = mockTestRuleDeps({
1359
+ rule: rule,
1360
+ availableChats: { "chat-1": makeChat("chat-1") },
1361
+ });
1362
+
1363
+ await runTestRule(rule);
1364
+
1365
+ expect(mocks.postSpy).not.toHaveBeenCalled();
1366
+ expect(mocks.createLogSpy).not.toHaveBeenCalled();
1367
+ });
1368
+
1369
+ test("Slack rules never enter the chat block even with the chat toggle on", async () => {
1370
+ const rule: WorkspaceNotificationRule = makeWorkspaceRule({
1371
+ workspaceType: WorkspaceType.Slack,
1372
+ notificationRule: makeBaseRule({
1373
+ shouldPostToExistingChat: true,
1374
+ existingChatIds: ["chat-1"],
1375
+ }),
1376
+ name: "Slack Rule",
1377
+ });
1378
+
1379
+ const mocks: TestRuleMocks = mockTestRuleDeps({
1380
+ rule: rule,
1381
+ availableChats: {},
1382
+ });
1383
+
1384
+ await runTestRule(rule);
1385
+
1386
+ expect(mocks.postSpy).not.toHaveBeenCalled();
1387
+ expect(mocks.getProjectAuthSpy).not.toHaveBeenCalled();
1388
+ expect(mocks.createLogSpy).not.toHaveBeenCalled();
1389
+ });
1390
+ });