@oneuptime/common 12.0.6 → 12.0.7

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 (424) hide show
  1. package/Models/DatabaseModels/AIInsight.ts +1 -1
  2. package/Models/DatabaseModels/DatabaseBaseModel/FileModel.ts +2 -3
  3. package/Models/DatabaseModels/DeletedProject.ts +595 -0
  4. package/Models/DatabaseModels/Index.ts +2 -0
  5. package/Models/DatabaseModels/Monitor.ts +111 -0
  6. package/Models/DatabaseModels/Project.ts +3 -3
  7. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -1
  8. package/Server/API/FileAPI.ts +15 -2
  9. package/Server/API/MicrosoftTeamsAPI.ts +6 -1
  10. package/Server/API/ProjectAPI.ts +214 -0
  11. package/Server/API/StatusPageAPI.ts +39 -6
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1786446545142-MigrationName.ts +50 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1786449497966-AddMonitorDependency.ts +67 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1786461136170-AddDeletedProjectTable.ts +39 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  16. package/Server/Services/AIAgentService.ts +20 -0
  17. package/Server/Services/DashboardDomainService.ts +73 -28
  18. package/Server/Services/DatabaseService.ts +1 -0
  19. package/Server/Services/DeletedProjectService.ts +138 -0
  20. package/Server/Services/FileService.ts +61 -0
  21. package/Server/Services/IncidentEpisodePublicNoteService.ts +18 -0
  22. package/Server/Services/IncidentPublicNoteService.ts +18 -0
  23. package/Server/Services/Index.ts +2 -0
  24. package/Server/Services/MonitorService.ts +311 -0
  25. package/Server/Services/ProbeService.ts +20 -0
  26. package/Server/Services/ProjectService.ts +297 -1
  27. package/Server/Services/PushNotificationService.ts +71 -0
  28. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +18 -0
  29. package/Server/Services/StatusPageAnnouncementService.ts +53 -0
  30. package/Server/Services/StatusPageDomainService.ts +73 -28
  31. package/Server/Services/TeamMemberService.ts +117 -12
  32. package/Server/Services/TelemetryIngestionKeyService.ts +1 -0
  33. package/Server/Services/UserWebhookService.ts +34 -109
  34. package/Server/Services/WorkspaceNotificationRuleService.ts +69 -0
  35. package/Server/Services/WorkspaceProjectAuthTokenService.ts +37 -1
  36. package/Server/Types/Database/DeleteById.ts +2 -0
  37. package/Server/Types/Database/DeleteOneBy.ts +6 -0
  38. package/Server/Types/Domain.ts +41 -0
  39. package/Server/Types/Workflow/Components/API/Delete.ts +2 -1
  40. package/Server/Types/Workflow/Components/API/Get.ts +2 -1
  41. package/Server/Types/Workflow/Components/API/Patch.ts +2 -1
  42. package/Server/Types/Workflow/Components/API/Post.ts +2 -1
  43. package/Server/Types/Workflow/Components/API/Put.ts +2 -1
  44. package/Server/Types/Workflow/Components/API/Utils.ts +41 -2
  45. package/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.ts +25 -19
  46. package/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.ts +6 -5
  47. package/Server/Types/Workflow/Components/BaseModel/DeleteManyBaseModel.ts +32 -14
  48. package/Server/Types/Workflow/Components/BaseModel/DeleteOneBaseModel.ts +31 -13
  49. package/Server/Types/Workflow/Components/BaseModel/FindManyBaseModel.ts +29 -20
  50. package/Server/Types/Workflow/Components/BaseModel/FindOneBaseModel.ts +28 -21
  51. package/Server/Types/Workflow/Components/BaseModel/LogComponentError.ts +89 -0
  52. package/Server/Types/Workflow/Components/BaseModel/ModelArguments.ts +208 -0
  53. package/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.ts +11 -0
  54. package/Server/Types/Workflow/Components/BaseModel/UpdateManyBaseModel.ts +41 -18
  55. package/Server/Types/Workflow/Components/BaseModel/UpdateOneBaseModel.ts +45 -19
  56. package/Server/Types/Workflow/Components/Discord/SendMessageToChannel.ts +16 -9
  57. package/Server/Types/Workflow/Components/IncomingWebhookUtils.ts +66 -0
  58. package/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.ts +15 -9
  59. package/Server/Types/Workflow/Components/Slack/SendMessageToChannel.ts +17 -0
  60. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +1 -1
  61. package/Server/Utils/AI/CodeFix/CodeAgentWorkspaceGuard.ts +171 -0
  62. package/Server/Utils/AI/SRE/FixFromIncidentTaskTrigger.ts +3 -3
  63. package/Server/Utils/AI/SRE/Insights/FixRouting.ts +3 -2
  64. package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +1 -1
  65. package/Server/Utils/AI/Toolbox/CodeWriteTools.ts +6 -6
  66. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +43 -5
  67. package/Server/Utils/DataSource/EgressGuard.ts +176 -16
  68. package/Server/Utils/DataSource/HttpFetch.ts +6 -45
  69. package/Server/Utils/Execute.ts +8 -0
  70. package/Server/Utils/InlineImageAccessTokenSync.ts +23 -0
  71. package/Server/Utils/LLM/LLMService.ts +57 -12
  72. package/Server/Utils/Monitor/MonitorAlert.ts +40 -0
  73. package/Server/Utils/Monitor/MonitorDependencySuppression.ts +192 -0
  74. package/Server/Utils/Monitor/MonitorIncident.ts +40 -0
  75. package/Server/Utils/Monitor/MonitorResource.ts +36 -0
  76. package/Server/Utils/Response.ts +142 -6
  77. package/Server/Utils/SSRFProtection.ts +273 -60
  78. package/Server/Utils/StartServer.ts +8 -0
  79. package/Server/Utils/VM/VMRunner.ts +88 -1
  80. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +555 -49
  81. package/Server/Utils/Workspace/Slack/Slack.ts +25 -2
  82. package/Server/Utils/Workspace/WorkspaceBase.ts +6 -0
  83. package/Tests/App/Dashboard/DangerZone.test.tsx +334 -0
  84. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +539 -0
  85. package/Tests/App/Dashboard/DashboardVariableSelector.test.tsx +678 -0
  86. package/Tests/App/Dashboard/DashboardVariablesDiscard.test.tsx +706 -0
  87. package/Tests/App/Dashboard/DashboardVariablesModal.test.tsx +508 -0
  88. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +470 -0
  89. package/Tests/App/StatusPage/StatusPageDetailPagesNotFound.test.tsx +308 -0
  90. package/Tests/Models/DatabaseModels/DeletedProjectModel.test.ts +251 -0
  91. package/Tests/Models/DatabaseModels/SavedViewJSONColumnTuples.test.ts +115 -0
  92. package/Tests/Server/API/FileAPIAccessControl.test.ts +316 -0
  93. package/Tests/Server/API/ProjectAPI.test.ts +299 -1
  94. package/Tests/Server/API/ProjectAPIAdjustBalance.test.ts +459 -0
  95. package/Tests/Server/API/ProjectAPIAdjustBalanceBillingDisabled.test.ts +177 -0
  96. package/Tests/Server/Services/DatabaseServiceDeletionReason.test.ts +123 -0
  97. package/Tests/Server/Services/DeletedProjectService.test.ts +415 -0
  98. package/Tests/Server/Services/DeletedProjectServiceSelfHosted.test.ts +70 -0
  99. package/Tests/Server/Services/DomainSubdomainHooks.test.ts +94 -0
  100. package/Tests/Server/Services/FileIsPublicBooleanMigration.test.ts +244 -0
  101. package/Tests/Server/Services/FileServiceMimeType.test.ts +89 -0
  102. package/Tests/Server/Services/MonitorServiceDependencyValidation.test.ts +702 -0
  103. package/Tests/Server/Services/ProjectServiceAdjustBalance.test.ts +615 -0
  104. package/Tests/Server/Services/ProjectServiceDeleteAudit.test.ts +406 -0
  105. package/Tests/Server/Services/PublishTimeImageVisibility.test.ts +450 -0
  106. package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +669 -0
  107. package/Tests/Server/Services/UserWebhookSSRF.test.ts +174 -0
  108. package/Tests/Server/Services/WebPushEndpointAllowlist.test.ts +133 -0
  109. package/Tests/Server/Services/WorkspaceProjectAuthTokenRepoint.test.ts +618 -0
  110. package/Tests/Server/Types/Workflow/Components/ApiComponentErrorPort.test.ts +26 -1
  111. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +215 -0
  112. package/Tests/Server/Types/Workflow/Components/BaseModelDatabaseComponents.test.ts +742 -0
  113. package/Tests/Server/Types/Workflow/Components/ChatWebhookComponents.test.ts +256 -0
  114. package/Tests/Server/Types/Workflow/Components/ModelArguments.test.ts +371 -0
  115. package/Tests/Server/Utils/AI/CodeAgentCommandGuard.test.ts +204 -0
  116. package/Tests/Server/Utils/AI/CodeAgentWriteLimits.test.ts +213 -0
  117. package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +52 -5
  118. package/Tests/Server/Utils/AI/LLMServiceBaseUrl.test.ts +3 -0
  119. package/Tests/Server/Utils/AI/LLMServiceEgressGuard.test.ts +315 -0
  120. package/Tests/Server/Utils/AI/LLMServiceModelCompatibility.test.ts +3 -0
  121. package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +17 -5
  122. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +14 -1
  123. package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +35 -0
  124. package/Tests/Server/Utils/AI/StubLLMEgressGuard.ts +25 -0
  125. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +62 -7
  126. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +69 -0
  127. package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +330 -0
  128. package/Tests/Server/Utils/GitHubCreatePullRequestWithToken.test.ts +393 -0
  129. package/Tests/Server/Utils/InlineImageAccessTokenSync.test.ts +202 -0
  130. package/Tests/Server/Utils/Monitor/MonitorDependencySuppression.test.ts +811 -0
  131. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +604 -0
  132. package/Tests/Server/Utils/ResponseSendFileResponse.test.ts +192 -0
  133. package/Tests/Server/Utils/SSRFProtectionBypasses.test.ts +406 -0
  134. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +194 -0
  135. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +1169 -0
  136. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +2 -0
  137. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +1605 -0
  138. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTenantResolution.test.ts +1218 -0
  139. package/Tests/Types/AI/AIChatMessageStatus.test.ts +79 -0
  140. package/Tests/Types/AI/AIInsightStatus.test.ts +40 -0
  141. package/Tests/Types/API/HostnameAuthorityParsing.test.ts +157 -0
  142. package/Tests/Types/Dashboard/DashboardTemplateInvariants.test.ts +27 -0
  143. package/Tests/Types/DataSource/DataSourceLimits.test.ts +71 -0
  144. package/Tests/Types/DataSource/DataSourceType.test.ts +181 -0
  145. package/Tests/Types/DomainSubdomainValidation.test.ts +90 -0
  146. package/Tests/Types/JSONFunctions.test.ts +252 -1
  147. package/Tests/Types/Monitor/SnmpMonitor/SnmpAuthProtocol.test.ts +61 -0
  148. package/Tests/Types/Monitor/SnmpMonitor/SnmpPrivProtocol.test.ts +62 -0
  149. package/Tests/Types/Monitor/SnmpMonitor/SnmpSecurityLevel.test.ts +68 -0
  150. package/Tests/Types/Monitor/SnmpMonitor/SnmpVersion.test.ts +51 -0
  151. package/Tests/UI/Components/404.test.tsx +1 -1
  152. package/Tests/UI/Components/Alert.test.tsx +1 -1
  153. package/Tests/UI/Components/Badge.test.tsx +1 -1
  154. package/Tests/UI/Components/BasicForm.test.tsx +39 -10
  155. package/Tests/UI/Components/Button.test.tsx +1 -1
  156. package/Tests/UI/Components/Card.test.tsx +1 -1
  157. package/Tests/UI/Components/Charts/AxisTickColor.test.tsx +1 -1
  158. package/Tests/UI/Components/ColorViewer.test.tsx +1 -1
  159. package/Tests/UI/Components/ComponentsModal.test.tsx +7 -1
  160. package/Tests/UI/Components/DictionaryOfStrings.test.tsx +1 -1
  161. package/Tests/UI/Components/Dropdown.test.tsx +1 -1
  162. package/Tests/UI/Components/FilePicker.test.tsx +1 -1
  163. package/Tests/UI/Components/HiddenText.test.tsx +1 -1
  164. package/Tests/UI/Components/Input.test.tsx +1 -1
  165. package/Tests/UI/Components/Loader.test.tsx +1 -1
  166. package/Tests/UI/Components/LogTimeRangePicker.test.tsx +1 -1
  167. package/Tests/UI/Components/MasterPage.test.tsx +1 -1
  168. package/Tests/UI/Components/ModalPortalDismissal.test.tsx +489 -0
  169. package/Tests/UI/Components/ModelDelete.test.tsx +214 -0
  170. package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1 -1
  171. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +1 -1
  172. package/Tests/UI/Components/ModelTable/BaseModelTableSearchLabels.test.tsx +1 -1
  173. package/Tests/UI/Components/ModelTable/BaseModelTableUrlState.test.tsx +1 -1
  174. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +1 -1
  175. package/Tests/UI/Components/NavBar.test.tsx +1 -1
  176. package/Tests/UI/Components/OperatorSelectorKeyboard.test.tsx +1 -1
  177. package/Tests/UI/Components/Pagination.test.tsx +1 -1
  178. package/Tests/UI/Components/Pill.test.tsx +1 -1
  179. package/Tests/UI/Components/Probe.test.tsx +1 -1
  180. package/Tests/UI/Components/ProgressBar.test.tsx +1 -1
  181. package/Tests/UI/Components/RadioButtons.test.tsx +1 -1
  182. package/Tests/UI/Components/SideMenuItem.test.tsx +1 -1
  183. package/Tests/UI/Components/SideOver.test.tsx +500 -6
  184. package/Tests/UI/Components/Tabs.test.tsx +1 -1
  185. package/Tests/UI/Components/TelemetrySearchBar.test.tsx +1 -1
  186. package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +1 -1
  187. package/Tests/UI/Components/TextArea.test.tsx +1 -1
  188. package/Tests/UI/Components/Toast.test.tsx +1 -1
  189. package/Tests/UI/Components/Toggle.test.tsx +1 -1
  190. package/Tests/UI/Components/TopSection.test.tsx +1 -1
  191. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +1 -1
  192. package/Tests/UI/Rum/ReplayScrubber.test.tsx +1 -1
  193. package/Tests/UI/Rum/ReplayStage.test.tsx +1 -1
  194. package/Tests/UI/Telemetry/TelemetrySnapshotWindowAlert.test.tsx +1 -1
  195. package/Tests/UI/Utils/LocalStorageArrayRoundTrip.test.ts +72 -0
  196. package/Tests/Utils/Dashboard/HtmlWidgetDocument.test.ts +19 -2
  197. package/Tests/Utils/Dashboard/ModelQueryVariableInterpolation.test.ts +44 -0
  198. package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +288 -5
  199. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +42 -0
  200. package/Tests/Utils/DataSource/DataSourceQueryText.test.ts +27 -0
  201. package/Tests/Utils/LegacySerializedArray.test.ts +139 -0
  202. package/Tests/Utils/Monitor/MonitorDependencyRule.test.ts +190 -0
  203. package/Tests/Utils/Realtime.test.ts +78 -0
  204. package/Tests/Utils/Telemetry/SavedViewFilters.test.ts +246 -0
  205. package/Tests/__mocks__/i18next-browser-languagedetector.js +21 -0
  206. package/Types/AI/FixPullRequestCiStatus.ts +1 -1
  207. package/Types/AI/FixVerificationStatus.ts +2 -2
  208. package/Types/API/Hostname.ts +92 -4
  209. package/Types/API/URL.ts +43 -25
  210. package/Types/Billing/BalanceAdjustmentType.ts +14 -0
  211. package/Types/Billing/ProjectBalanceType.ts +13 -0
  212. package/Types/Domain.ts +26 -0
  213. package/Types/File/MimeType.ts +11 -0
  214. package/Types/JSONFunctions.ts +15 -0
  215. package/Types/Telemetry/TelemetrySavedViewState.ts +9 -1
  216. package/Types/Workflow/Components/BaseModel.ts +92 -28
  217. package/Typings/Index.d.ts +12 -0
  218. package/UI/Components/GanttChart/ChartContainer.tsx +4 -3
  219. package/UI/Components/Modal/ConfirmModal.tsx +10 -5
  220. package/UI/Components/Modal/Modal.tsx +27 -47
  221. package/UI/Components/ModelDelete/ModelDelete.tsx +23 -6
  222. package/UI/Components/SideOver/SideOver.tsx +195 -68
  223. package/UI/Utils/PageScrollLock.ts +66 -0
  224. package/Utils/Dashboard/HtmlWidgetDocument.ts +4 -2
  225. package/Utils/Dashboard/VariableInterpolation.ts +19 -1
  226. package/Utils/LegacySerializedArray.ts +60 -0
  227. package/Utils/Monitor/MonitorDependencyRule.ts +88 -0
  228. package/Utils/Telemetry/SavedViewFilters.ts +88 -0
  229. package/build/dist/Models/DatabaseModels/AIInsight.js +1 -1
  230. package/build/dist/Models/DatabaseModels/AIInsight.js.map +1 -1
  231. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/FileModel.js +2 -3
  232. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/FileModel.js.map +1 -1
  233. package/build/dist/Models/DatabaseModels/DeletedProject.js +640 -0
  234. package/build/dist/Models/DatabaseModels/DeletedProject.js.map +1 -0
  235. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  236. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  237. package/build/dist/Models/DatabaseModels/Monitor.js +106 -0
  238. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  239. package/build/dist/Models/DatabaseModels/Project.js +3 -3
  240. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  241. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  242. package/build/dist/Server/API/FileAPI.js +14 -2
  243. package/build/dist/Server/API/FileAPI.js.map +1 -1
  244. package/build/dist/Server/API/MicrosoftTeamsAPI.js +6 -1
  245. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  246. package/build/dist/Server/API/ProjectAPI.js +157 -4
  247. package/build/dist/Server/API/ProjectAPI.js.map +1 -1
  248. package/build/dist/Server/API/StatusPageAPI.js +27 -3
  249. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  250. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786446545142-MigrationName.js +34 -0
  251. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786446545142-MigrationName.js.map +1 -0
  252. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786449497966-AddMonitorDependency.js +30 -0
  253. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786449497966-AddMonitorDependency.js.map +1 -0
  254. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786461136170-AddDeletedProjectTable.js +20 -0
  255. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786461136170-AddDeletedProjectTable.js.map +1 -0
  256. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  257. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  258. package/build/dist/Server/Services/AIAgentService.js +17 -0
  259. package/build/dist/Server/Services/AIAgentService.js.map +1 -1
  260. package/build/dist/Server/Services/DashboardDomainService.js +55 -18
  261. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  262. package/build/dist/Server/Services/DatabaseService.js +1 -0
  263. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  264. package/build/dist/Server/Services/DeletedProjectService.js +124 -0
  265. package/build/dist/Server/Services/DeletedProjectService.js.map +1 -0
  266. package/build/dist/Server/Services/FileService.js +52 -0
  267. package/build/dist/Server/Services/FileService.js.map +1 -1
  268. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +10 -1
  269. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  270. package/build/dist/Server/Services/IncidentPublicNoteService.js +10 -1
  271. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  272. package/build/dist/Server/Services/Index.js +2 -0
  273. package/build/dist/Server/Services/Index.js.map +1 -1
  274. package/build/dist/Server/Services/MonitorService.js +232 -0
  275. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  276. package/build/dist/Server/Services/ProbeService.js +17 -0
  277. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  278. package/build/dist/Server/Services/ProjectService.js +228 -6
  279. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  280. package/build/dist/Server/Services/PushNotificationService.js +56 -0
  281. package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
  282. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +10 -1
  283. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  284. package/build/dist/Server/Services/StatusPageAnnouncementService.js +54 -0
  285. package/build/dist/Server/Services/StatusPageAnnouncementService.js.map +1 -1
  286. package/build/dist/Server/Services/StatusPageDomainService.js +55 -18
  287. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  288. package/build/dist/Server/Services/TeamMemberService.js +97 -6
  289. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  290. package/build/dist/Server/Services/TelemetryIngestionKeyService.js +1 -0
  291. package/build/dist/Server/Services/TelemetryIngestionKeyService.js.map +1 -1
  292. package/build/dist/Server/Services/UserWebhookService.js +31 -87
  293. package/build/dist/Server/Services/UserWebhookService.js.map +1 -1
  294. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +65 -0
  295. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  296. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +23 -0
  297. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  298. package/build/dist/Server/Types/Domain.js +37 -0
  299. package/build/dist/Server/Types/Domain.js.map +1 -1
  300. package/build/dist/Server/Types/Workflow/Components/API/Delete.js +2 -1
  301. package/build/dist/Server/Types/Workflow/Components/API/Delete.js.map +1 -1
  302. package/build/dist/Server/Types/Workflow/Components/API/Get.js +2 -1
  303. package/build/dist/Server/Types/Workflow/Components/API/Get.js.map +1 -1
  304. package/build/dist/Server/Types/Workflow/Components/API/Patch.js +2 -1
  305. package/build/dist/Server/Types/Workflow/Components/API/Patch.js.map +1 -1
  306. package/build/dist/Server/Types/Workflow/Components/API/Post.js +2 -1
  307. package/build/dist/Server/Types/Workflow/Components/API/Post.js.map +1 -1
  308. package/build/dist/Server/Types/Workflow/Components/API/Put.js +2 -1
  309. package/build/dist/Server/Types/Workflow/Components/API/Put.js.map +1 -1
  310. package/build/dist/Server/Types/Workflow/Components/API/Utils.js +35 -2
  311. package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
  312. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js +15 -11
  313. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js.map +1 -1
  314. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js +2 -3
  315. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js.map +1 -1
  316. package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteManyBaseModel.js +21 -10
  317. package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteManyBaseModel.js.map +1 -1
  318. package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteOneBaseModel.js +21 -10
  319. package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteOneBaseModel.js.map +1 -1
  320. package/build/dist/Server/Types/Workflow/Components/BaseModel/FindManyBaseModel.js +18 -13
  321. package/build/dist/Server/Types/Workflow/Components/BaseModel/FindManyBaseModel.js.map +1 -1
  322. package/build/dist/Server/Types/Workflow/Components/BaseModel/FindOneBaseModel.js +18 -13
  323. package/build/dist/Server/Types/Workflow/Components/BaseModel/FindOneBaseModel.js.map +1 -1
  324. package/build/dist/Server/Types/Workflow/Components/BaseModel/LogComponentError.js +43 -0
  325. package/build/dist/Server/Types/Workflow/Components/BaseModel/LogComponentError.js.map +1 -0
  326. package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js +116 -0
  327. package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js.map +1 -0
  328. package/build/dist/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.js +7 -0
  329. package/build/dist/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.js.map +1 -1
  330. package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateManyBaseModel.js +24 -13
  331. package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateManyBaseModel.js.map +1 -1
  332. package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateOneBaseModel.js +27 -13
  333. package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateOneBaseModel.js.map +1 -1
  334. package/build/dist/Server/Types/Workflow/Components/Discord/SendMessageToChannel.js +14 -6
  335. package/build/dist/Server/Types/Workflow/Components/Discord/SendMessageToChannel.js.map +1 -1
  336. package/build/dist/Server/Types/Workflow/Components/IncomingWebhookUtils.js +42 -0
  337. package/build/dist/Server/Types/Workflow/Components/IncomingWebhookUtils.js.map +1 -0
  338. package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js +15 -6
  339. package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js.map +1 -1
  340. package/build/dist/Server/Types/Workflow/Components/Slack/SendMessageToChannel.js +10 -2
  341. package/build/dist/Server/Types/Workflow/Components/Slack/SendMessageToChannel.js.map +1 -1
  342. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +1 -1
  343. package/build/dist/Server/Utils/AI/CodeFix/CodeAgentWorkspaceGuard.js +141 -0
  344. package/build/dist/Server/Utils/AI/CodeFix/CodeAgentWorkspaceGuard.js.map +1 -1
  345. package/build/dist/Server/Utils/AI/SRE/Insights/FixRouting.js.map +1 -1
  346. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +1 -1
  347. package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js +6 -6
  348. package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js.map +1 -1
  349. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +39 -5
  350. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  351. package/build/dist/Server/Utils/DataSource/EgressGuard.js +94 -10
  352. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  353. package/build/dist/Server/Utils/DataSource/HttpFetch.js +1 -19
  354. package/build/dist/Server/Utils/DataSource/HttpFetch.js.map +1 -1
  355. package/build/dist/Server/Utils/Execute.js +2 -2
  356. package/build/dist/Server/Utils/Execute.js.map +1 -1
  357. package/build/dist/Server/Utils/InlineImageAccessTokenSync.js +13 -0
  358. package/build/dist/Server/Utils/InlineImageAccessTokenSync.js.map +1 -1
  359. package/build/dist/Server/Utils/LLM/LLMService.js +43 -12
  360. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  361. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +44 -24
  362. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  363. package/build/dist/Server/Utils/Monitor/MonitorDependencySuppression.js +158 -0
  364. package/build/dist/Server/Utils/Monitor/MonitorDependencySuppression.js.map +1 -0
  365. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +50 -30
  366. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  367. package/build/dist/Server/Utils/Monitor/MonitorResource.js +32 -0
  368. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  369. package/build/dist/Server/Utils/Response.js +112 -5
  370. package/build/dist/Server/Utils/Response.js.map +1 -1
  371. package/build/dist/Server/Utils/SSRFProtection.js +219 -53
  372. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  373. package/build/dist/Server/Utils/StartServer.js +7 -0
  374. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  375. package/build/dist/Server/Utils/VM/VMRunner.js +73 -1
  376. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  377. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +445 -43
  378. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  379. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +18 -1
  380. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  381. package/build/dist/Server/Utils/Workspace/WorkspaceBase.js.map +1 -1
  382. package/build/dist/Types/AI/FixPullRequestCiStatus.js +1 -1
  383. package/build/dist/Types/AI/FixVerificationStatus.js +2 -2
  384. package/build/dist/Types/API/Hostname.js +73 -4
  385. package/build/dist/Types/API/Hostname.js.map +1 -1
  386. package/build/dist/Types/API/URL.js +42 -24
  387. package/build/dist/Types/API/URL.js.map +1 -1
  388. package/build/dist/Types/Billing/BalanceAdjustmentType.js +15 -0
  389. package/build/dist/Types/Billing/BalanceAdjustmentType.js.map +1 -0
  390. package/build/dist/Types/Billing/ProjectBalanceType.js +14 -0
  391. package/build/dist/Types/Billing/ProjectBalanceType.js.map +1 -0
  392. package/build/dist/Types/Domain.js +22 -0
  393. package/build/dist/Types/Domain.js.map +1 -1
  394. package/build/dist/Types/File/MimeType.js +11 -0
  395. package/build/dist/Types/File/MimeType.js.map +1 -1
  396. package/build/dist/Types/JSONFunctions.js +14 -0
  397. package/build/dist/Types/JSONFunctions.js.map +1 -1
  398. package/build/dist/Types/Workflow/Components/BaseModel.js +83 -28
  399. package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
  400. package/build/dist/UI/Components/GanttChart/ChartContainer.js +4 -3
  401. package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +1 -1
  402. package/build/dist/UI/Components/Modal/ConfirmModal.js +3 -1
  403. package/build/dist/UI/Components/Modal/ConfirmModal.js.map +1 -1
  404. package/build/dist/UI/Components/Modal/Modal.js +24 -34
  405. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  406. package/build/dist/UI/Components/ModelDelete/ModelDelete.js +11 -6
  407. package/build/dist/UI/Components/ModelDelete/ModelDelete.js.map +1 -1
  408. package/build/dist/UI/Components/SideOver/SideOver.js +105 -30
  409. package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
  410. package/build/dist/UI/Utils/PageScrollLock.js +52 -0
  411. package/build/dist/UI/Utils/PageScrollLock.js.map +1 -0
  412. package/build/dist/Utils/Dashboard/HtmlWidgetDocument.js +4 -2
  413. package/build/dist/Utils/Dashboard/HtmlWidgetDocument.js.map +1 -1
  414. package/build/dist/Utils/Dashboard/VariableInterpolation.js +19 -1
  415. package/build/dist/Utils/Dashboard/VariableInterpolation.js.map +1 -1
  416. package/build/dist/Utils/LegacySerializedArray.js +51 -0
  417. package/build/dist/Utils/LegacySerializedArray.js.map +1 -0
  418. package/build/dist/Utils/Monitor/MonitorDependencyRule.js +47 -0
  419. package/build/dist/Utils/Monitor/MonitorDependencyRule.js.map +1 -0
  420. package/build/dist/Utils/Telemetry/SavedViewFilters.js +52 -0
  421. package/build/dist/Utils/Telemetry/SavedViewFilters.js.map +1 -0
  422. package/jest.config.json +3 -0
  423. package/package.json +1 -1
  424. package/tsconfig.json +3 -1
@@ -0,0 +1,1169 @@
1
+ import { describe, expect, test, afterEach, beforeEach } from "@jest/globals";
2
+
3
+ /*
4
+ * Tests for the Microsoft Teams CHANNEL delivery path in MicrosoftTeamsUtil.
5
+ *
6
+ * The bug these pin down: channels are DISCOVERED with tenant-wide Graph
7
+ * application permissions (which can see every team in the tenant) but are
8
+ * DELIVERED to over Bot Framework proactive messaging, which only accepts a
9
+ * post into a team the OneUptime app is actually installed in. So a channel
10
+ * could be picked in the dashboard, validate green, and then fail at send time
11
+ * with Microsoft's opaque "The bot is not part of the conversation roster."
12
+ *
13
+ * sendAdaptiveCardToChannel now:
14
+ * - refuses shared channels up front (bots cannot post in them at all),
15
+ * - preflights miscData.installedTeams and refuses before touching the Bot
16
+ * Framework when the app was never installed into the target team — while
17
+ * staying permissive for legacy workspaces whose install map is empty,
18
+ * - uses the serviceUrl captured on install (required for GCC/DoD) instead of
19
+ * always assuming the commercial-cloud endpoint,
20
+ * - translates Microsoft's roster rejection into instructions an admin can act
21
+ * on, and lets every other error through untouched.
22
+ *
23
+ * sendMessage now REPORTS destinations it cannot even resolve (an unknown
24
+ * channel name, a channel id whose lookup throws). Both used to be swallowed,
25
+ * so a typo'd or deleted channel reported a successful send.
26
+ */
27
+
28
+ jest.mock("../../../../Server/EnvironmentConfig", () => {
29
+ return {
30
+ ...(jest.requireActual("../../../../Server/EnvironmentConfig") as Record<
31
+ string,
32
+ unknown
33
+ >),
34
+ MicrosoftTeamsAppClientId: "11111111-2222-3333-4444-555555555555",
35
+ MicrosoftTeamsAppClientSecret: "test-secret",
36
+ MicrosoftTeamsAppTenantId: "test-tenant",
37
+ };
38
+ });
39
+
40
+ /*
41
+ * Same botbuilder module factory as MicrosoftTeamsChats.test.ts — the repo-wide
42
+ * manual mock (Tests/__mocks__/botbuilder.js) does not expose TeamsInfo or
43
+ * MessageFactory.attachment, both of which these code paths use.
44
+ */
45
+ jest.mock("botbuilder", () => {
46
+ return {
47
+ CloudAdapter: class CloudAdapter {},
48
+ ConfigurationBotFrameworkAuthentication: class ConfigurationBotFrameworkAuthentication {},
49
+ TeamsActivityHandler: class TeamsActivityHandler {},
50
+ TurnContext: class TurnContext {},
51
+ ActivityHandler: class ActivityHandler {},
52
+ MessageFactory: {
53
+ text: jest.fn(),
54
+ attachment: jest.fn((attachment: unknown) => {
55
+ return { type: "message", attachments: [attachment] };
56
+ }),
57
+ },
58
+ CardFactory: { heroCard: jest.fn() },
59
+ TeamsInfo: {
60
+ getMembers: jest.fn(),
61
+ getPagedMembers: jest.fn(),
62
+ },
63
+ };
64
+ });
65
+
66
+ import MicrosoftTeamsUtil from "../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
67
+ import WorkspaceProjectAuthTokenService from "../../../../Server/Services/WorkspaceProjectAuthTokenService";
68
+ import WorkspaceProjectAuthToken, {
69
+ MicrosoftTeamsInstalledTeam,
70
+ MicrosoftTeamsMiscData,
71
+ } from "../../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
72
+ import WorkspaceType from "../../../../Types/Workspace/WorkspaceType";
73
+ import ObjectID from "../../../../Types/ObjectID";
74
+ import BadDataException from "../../../../Types/Exception/BadDataException";
75
+ import { JSONObject } from "../../../../Types/JSON";
76
+ import WorkspaceMessagePayload, {
77
+ WorkspacePayloadMarkdown,
78
+ } from "../../../../Types/Workspace/WorkspaceMessagePayload";
79
+ import {
80
+ WorkspaceChannel,
81
+ WorkspaceSendMessageResponse,
82
+ WorkspaceThread,
83
+ } from "../../../../Server/Utils/Workspace/WorkspaceBase";
84
+ import {
85
+ MessageFactory,
86
+ CardFactory,
87
+ TeamsInfo,
88
+ type ConversationReference,
89
+ type TurnContext,
90
+ } from "botbuilder";
91
+
92
+ const MOCK_APP_CLIENT_ID: string = "11111111-2222-3333-4444-555555555555";
93
+ const TENANT_ID: string = "tenant-xyz";
94
+ const TEAM_ID: string = "team-1";
95
+ const CHANNEL_ID: string = "19:general@thread.tacv2";
96
+ const DEFAULT_SERVICE_URL: string = "https://smba.trafficmanager.net/teams/";
97
+ const GCC_SERVICE_URL: string = "https://smba.infra.gov.teams.microsoft.us/";
98
+ const ROSTER_ERROR_TEXT: string =
99
+ "The bot is not part of the conversation roster.";
100
+
101
+ const ADAPTIVE_CARD: JSONObject = {
102
+ type: "AdaptiveCard",
103
+ body: [],
104
+ };
105
+
106
+ type ProactiveCallback = (context: TurnContext) => Promise<void>;
107
+
108
+ interface FakeBotAdapter {
109
+ capturedRefs: Array<ConversationReference>;
110
+ continueConversationAsync: jest.Mock;
111
+ getBotAdapterSpy: jest.SpyInstance;
112
+ }
113
+
114
+ function buildProjectAuthRow(data: {
115
+ id?: ObjectID | undefined;
116
+ workspaceProjectId?: string | undefined;
117
+ miscData?: MicrosoftTeamsMiscData | undefined;
118
+ }): WorkspaceProjectAuthToken {
119
+ return {
120
+ id: data.id || ObjectID.generate(),
121
+ workspaceProjectId: data.workspaceProjectId,
122
+ miscData: data.miscData,
123
+ } as unknown as WorkspaceProjectAuthToken;
124
+ }
125
+
126
+ function baseMiscData(
127
+ overrides?: Partial<MicrosoftTeamsMiscData>,
128
+ ): MicrosoftTeamsMiscData {
129
+ return {
130
+ tenantId: TENANT_ID,
131
+ teamId: TEAM_ID,
132
+ teamName: "Engineering",
133
+ botId: "bot-1",
134
+ ...(overrides || {}),
135
+ } as MicrosoftTeamsMiscData;
136
+ }
137
+
138
+ function buildInstalledTeam(data: {
139
+ id: string;
140
+ name?: string | undefined;
141
+ serviceUrl?: string | undefined;
142
+ }): MicrosoftTeamsInstalledTeam {
143
+ const installedTeam: MicrosoftTeamsInstalledTeam = {
144
+ id: data.id,
145
+ name: data.name || "Engineering",
146
+ addedAt: "2026-07-27T00:00:00.000Z",
147
+ };
148
+ if ("serviceUrl" in data) {
149
+ installedTeam.serviceUrl = data.serviceUrl;
150
+ }
151
+ return installedTeam;
152
+ }
153
+
154
+ function buildChannel(data?: {
155
+ id?: string | undefined;
156
+ name?: string | undefined;
157
+ membershipType?: string | undefined;
158
+ teamId?: string | undefined;
159
+ }): WorkspaceChannel {
160
+ const channel: WorkspaceChannel = {
161
+ id: data?.id || CHANNEL_ID,
162
+ name: data?.name || "General",
163
+ workspaceType: WorkspaceType.MicrosoftTeams,
164
+ teamId: data?.teamId || TEAM_ID,
165
+ };
166
+ if (data && "membershipType" in data) {
167
+ channel.membershipType = data.membershipType;
168
+ }
169
+ return channel;
170
+ }
171
+
172
+ /*
173
+ * Mirrors installFakeBotAdapter from MicrosoftTeamsChats.test.ts, plus a way to
174
+ * make the adapter (or the proactive callback) reject so the roster-error
175
+ * translation can be exercised, and the raw jest.fn/spy so tests can assert the
176
+ * Bot Framework was never reached at all.
177
+ */
178
+ function installFakeBotAdapter(options?: {
179
+ sendActivityResponse?: JSONObject | undefined;
180
+ adapterError?: unknown;
181
+ sendActivityError?: unknown;
182
+ }): FakeBotAdapter {
183
+ const capturedRefs: Array<ConversationReference> = [];
184
+ const continueConversationAsync: jest.Mock = jest.fn(
185
+ async (
186
+ _appId: string,
187
+ ref: ConversationReference,
188
+ cb: ProactiveCallback,
189
+ ): Promise<void> => {
190
+ capturedRefs.push(ref);
191
+
192
+ if (options && "adapterError" in options) {
193
+ throw options.adapterError;
194
+ }
195
+
196
+ const fakeContext: TurnContext = {
197
+ sendActivity: async (): Promise<JSONObject | undefined> => {
198
+ if (options && "sendActivityError" in options) {
199
+ throw options.sendActivityError;
200
+ }
201
+ if (options && "sendActivityResponse" in options) {
202
+ return options.sendActivityResponse;
203
+ }
204
+ return { id: "msg-123" };
205
+ },
206
+ } as unknown as TurnContext;
207
+
208
+ await cb(fakeContext);
209
+ },
210
+ );
211
+
212
+ const fakeAdapter: unknown = {
213
+ continueConversationAsync: continueConversationAsync,
214
+ };
215
+
216
+ const getBotAdapterSpy: jest.SpyInstance = jest
217
+ .spyOn(MicrosoftTeamsUtil as any, "getBotAdapter")
218
+ .mockReturnValue(fakeAdapter);
219
+
220
+ return {
221
+ capturedRefs: capturedRefs,
222
+ continueConversationAsync: continueConversationAsync,
223
+ getBotAdapterSpy: getBotAdapterSpy,
224
+ };
225
+ }
226
+
227
+ function mockProjectAuth(data?: {
228
+ installedTeams?: Record<string, MicrosoftTeamsInstalledTeam> | undefined;
229
+ miscData?: MicrosoftTeamsMiscData | null | undefined;
230
+ workspaceProjectId?: string | undefined;
231
+ }): jest.SpyInstance {
232
+ let miscData: MicrosoftTeamsMiscData | undefined = undefined;
233
+
234
+ if (data && "miscData" in data) {
235
+ miscData = data.miscData === null ? undefined : data.miscData;
236
+ } else if (data && data.installedTeams) {
237
+ miscData = baseMiscData({ installedTeams: data.installedTeams });
238
+ } else {
239
+ miscData = baseMiscData();
240
+ }
241
+
242
+ return jest
243
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
244
+ .mockResolvedValue(
245
+ buildProjectAuthRow({
246
+ workspaceProjectId:
247
+ data && "workspaceProjectId" in data
248
+ ? data.workspaceProjectId
249
+ : TENANT_ID,
250
+ miscData: miscData,
251
+ }),
252
+ );
253
+ }
254
+
255
+ function sendCardToChannel(data?: {
256
+ workspaceChannel?: WorkspaceChannel | undefined;
257
+ teamId?: string | undefined;
258
+ projectId?: ObjectID | undefined;
259
+ }): Promise<WorkspaceThread> {
260
+ return MicrosoftTeamsUtil.sendAdaptiveCardToChannel({
261
+ authToken: "auth-token",
262
+ teamId: data?.teamId || TEAM_ID,
263
+ workspaceChannel: data?.workspaceChannel || buildChannel(),
264
+ adaptiveCard: ADAPTIVE_CARD,
265
+ projectId: data?.projectId || ObjectID.generate(),
266
+ });
267
+ }
268
+
269
+ afterEach(() => {
270
+ jest.restoreAllMocks();
271
+ });
272
+
273
+ /*
274
+ * The botbuilder module mock's jest.fn()s are shared across tests — jest.spyOn
275
+ * on an existing mock returns the same function, so call history and
276
+ * mockResolvedValueOnce queues leak between tests unless reset here.
277
+ */
278
+ beforeEach(() => {
279
+ (MessageFactory.text as jest.Mock).mockReset();
280
+ (MessageFactory.attachment as jest.Mock).mockReset();
281
+ (MessageFactory.attachment as jest.Mock).mockImplementation(
282
+ (attachment: unknown) => {
283
+ return { type: "message", attachments: [attachment] };
284
+ },
285
+ );
286
+ (CardFactory.heroCard as jest.Mock).mockReset();
287
+ (TeamsInfo.getMembers as jest.Mock).mockReset();
288
+ (TeamsInfo.getPagedMembers as jest.Mock).mockReset();
289
+ });
290
+
291
+ describe("MicrosoftTeamsUtil.isBotNotInConversationRosterError", () => {
292
+ test("matches Microsoft's exact roster rejection on an Error object", () => {
293
+ expect(
294
+ MicrosoftTeamsUtil.isBotNotInConversationRosterError(
295
+ new Error(ROSTER_ERROR_TEXT),
296
+ ),
297
+ ).toBe(true);
298
+ });
299
+
300
+ test("matches the shorter 'bot is not part of the conversation' phrasing", () => {
301
+ expect(
302
+ MicrosoftTeamsUtil.isBotNotInConversationRosterError(
303
+ new Error("The bot is not part of the conversation."),
304
+ ),
305
+ ).toBe(true);
306
+ });
307
+
308
+ test("matches a plain string error", () => {
309
+ expect(
310
+ MicrosoftTeamsUtil.isBotNotInConversationRosterError(
311
+ "The bot is not part of the conversation roster",
312
+ ),
313
+ ).toBe(true);
314
+ });
315
+
316
+ test("is case-insensitive", () => {
317
+ expect(
318
+ MicrosoftTeamsUtil.isBotNotInConversationRosterError(
319
+ new Error("THE BOT IS NOT PART OF THE CONVERSATION ROSTER"),
320
+ ),
321
+ ).toBe(true);
322
+ expect(
323
+ MicrosoftTeamsUtil.isBotNotInConversationRosterError(
324
+ new Error("Not Part Of The Conversation Roster"),
325
+ ),
326
+ ).toBe(true);
327
+ });
328
+
329
+ test("matches when the fragment is buried inside a larger Bot Framework payload", () => {
330
+ const wrapped: Error = new Error(
331
+ 'BotFrameworkAdapter.sendActivity(): 403 ERROR {"error":{"code":"BotNotInConversationRoster","message":"The bot is not part of the conversation roster."}}',
332
+ );
333
+ expect(MicrosoftTeamsUtil.isBotNotInConversationRosterError(wrapped)).toBe(
334
+ true,
335
+ );
336
+ });
337
+
338
+ test("returns false for an unrelated error", () => {
339
+ expect(
340
+ MicrosoftTeamsUtil.isBotNotInConversationRosterError(
341
+ new Error("Request timed out"),
342
+ ),
343
+ ).toBe(false);
344
+ });
345
+
346
+ test("returns false for a partially-similar message that is not the roster error", () => {
347
+ expect(
348
+ MicrosoftTeamsUtil.isBotNotInConversationRosterError(
349
+ new Error("The user is not part of the team"),
350
+ ),
351
+ ).toBe(false);
352
+ });
353
+
354
+ test("returns false for null and undefined", () => {
355
+ expect(MicrosoftTeamsUtil.isBotNotInConversationRosterError(null)).toBe(
356
+ false,
357
+ );
358
+ expect(
359
+ MicrosoftTeamsUtil.isBotNotInConversationRosterError(undefined),
360
+ ).toBe(false);
361
+ });
362
+
363
+ test("returns false for an empty message", () => {
364
+ expect(MicrosoftTeamsUtil.isBotNotInConversationRosterError("")).toBe(
365
+ false,
366
+ );
367
+ expect(
368
+ MicrosoftTeamsUtil.isBotNotInConversationRosterError(new Error("")),
369
+ ).toBe(false);
370
+ });
371
+
372
+ test("returns false for an object with no usable message", () => {
373
+ expect(
374
+ MicrosoftTeamsUtil.isBotNotInConversationRosterError({ code: 403 }),
375
+ ).toBe(false);
376
+ });
377
+ });
378
+
379
+ describe("MicrosoftTeamsUtil.getBotNotInTeamMessage", () => {
380
+ test("private channels are told to install into the channel itself", () => {
381
+ const message: string = MicrosoftTeamsUtil.getBotNotInTeamMessage({
382
+ channelName: "Ops War Room",
383
+ membershipType: "private",
384
+ });
385
+
386
+ expect(message).toContain('private channel "Ops War Room"');
387
+ expect(message).toContain("Manage channel > Apps");
388
+ expect(message).toContain(
389
+ "Installing OneUptime in the parent team does not cover private channels.",
390
+ );
391
+ expect(message).not.toContain("Manage team");
392
+ });
393
+
394
+ test("standard channels are told to install into the parent team", () => {
395
+ const message: string = MicrosoftTeamsUtil.getBotNotInTeamMessage({
396
+ channelName: "General",
397
+ membershipType: "standard",
398
+ });
399
+
400
+ expect(message).toContain('team that owns "General"');
401
+ expect(message).toContain("Manage team > Apps > More apps");
402
+ expect(message).not.toContain("Manage channel");
403
+ });
404
+
405
+ test("an undefined membershipType falls back to the parent-team instructions", () => {
406
+ const message: string = MicrosoftTeamsUtil.getBotNotInTeamMessage({
407
+ channelName: "General",
408
+ });
409
+
410
+ expect(message).toContain("Manage team > Apps > More apps");
411
+ expect(message).not.toContain("Manage channel");
412
+ });
413
+
414
+ test("only the exact string 'private' selects the private-channel instructions", () => {
415
+ /*
416
+ * Graph returns membershipType lowercase, so the comparison is exact —
417
+ * pin it so nobody "helpfully" loosens it without thinking about Graph.
418
+ */
419
+ const message: string = MicrosoftTeamsUtil.getBotNotInTeamMessage({
420
+ channelName: "General",
421
+ membershipType: "Private",
422
+ });
423
+
424
+ expect(message).toContain("Manage team > Apps > More apps");
425
+ });
426
+
427
+ test("the two branches produce genuinely different instructions", () => {
428
+ const privateMessage: string = MicrosoftTeamsUtil.getBotNotInTeamMessage({
429
+ channelName: "Same Name",
430
+ membershipType: "private",
431
+ });
432
+ const teamMessage: string = MicrosoftTeamsUtil.getBotNotInTeamMessage({
433
+ channelName: "Same Name",
434
+ membershipType: "standard",
435
+ });
436
+
437
+ expect(privateMessage).not.toBe(teamMessage);
438
+ expect(privateMessage).toContain("Same Name");
439
+ expect(teamMessage).toContain("Same Name");
440
+ });
441
+ });
442
+
443
+ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - shared channel refusal", () => {
444
+ test("a shared channel is refused with a reason, before any Bot Framework call", async () => {
445
+ mockProjectAuth();
446
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
447
+
448
+ await expect(
449
+ sendCardToChannel({
450
+ workspaceChannel: buildChannel({
451
+ name: "Partner Sync",
452
+ membershipType: "shared",
453
+ }),
454
+ }),
455
+ ).rejects.toThrow(
456
+ new BadDataException(
457
+ '"Partner Sync" is a shared channel, and Microsoft Teams does not allow bots to post in shared channels. Please pick a standard or private channel instead.',
458
+ ),
459
+ );
460
+
461
+ expect(adapter.getBotAdapterSpy).not.toHaveBeenCalled();
462
+ expect(adapter.continueConversationAsync).not.toHaveBeenCalled();
463
+ });
464
+
465
+ test("the shared refusal is a BadDataException", async () => {
466
+ mockProjectAuth();
467
+ installFakeBotAdapter();
468
+
469
+ await expect(
470
+ sendCardToChannel({
471
+ workspaceChannel: buildChannel({ membershipType: "shared" }),
472
+ }),
473
+ ).rejects.toBeInstanceOf(BadDataException);
474
+ });
475
+
476
+ test("the shared refusal wins over the install preflight so the real reason is reported", async () => {
477
+ mockProjectAuth({
478
+ installedTeams: {
479
+ "some-other-team": buildInstalledTeam({ id: "some-other-team" }),
480
+ },
481
+ });
482
+ installFakeBotAdapter();
483
+
484
+ await expect(
485
+ sendCardToChannel({
486
+ workspaceChannel: buildChannel({
487
+ name: "Partner Sync",
488
+ membershipType: "shared",
489
+ }),
490
+ }),
491
+ ).rejects.toThrow(/shared channel/);
492
+ });
493
+
494
+ test("standard channels are NOT refused", async () => {
495
+ mockProjectAuth();
496
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
497
+
498
+ await sendCardToChannel({
499
+ workspaceChannel: buildChannel({ membershipType: "standard" }),
500
+ });
501
+
502
+ expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
503
+ });
504
+
505
+ test("private channels are NOT refused (they only need a channel-level install)", async () => {
506
+ mockProjectAuth();
507
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
508
+
509
+ await sendCardToChannel({
510
+ workspaceChannel: buildChannel({ membershipType: "private" }),
511
+ });
512
+
513
+ expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
514
+ });
515
+
516
+ test("an undefined membershipType is NOT refused", async () => {
517
+ mockProjectAuth();
518
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
519
+
520
+ await sendCardToChannel({
521
+ workspaceChannel: buildChannel({ membershipType: undefined }),
522
+ });
523
+
524
+ expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
525
+ });
526
+
527
+ test("the shared comparison is exact: 'Shared' is not treated as shared", async () => {
528
+ /*
529
+ * Graph only ever returns lowercase "shared". This pins the current
530
+ * behavior so a future casing change in Graph is noticed here rather than
531
+ * in a customer's incident channel.
532
+ */
533
+ mockProjectAuth();
534
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
535
+
536
+ await sendCardToChannel({
537
+ workspaceChannel: buildChannel({ membershipType: "Shared" }),
538
+ });
539
+
540
+ expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
541
+ });
542
+ });
543
+
544
+ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - install preflight", () => {
545
+ test("installedTeams undefined (legacy workspace) does NOT block the send", async () => {
546
+ mockProjectAuth({ miscData: baseMiscData() });
547
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
548
+
549
+ const thread: WorkspaceThread = await sendCardToChannel();
550
+
551
+ expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
552
+ expect(thread.threadId).toBe("msg-123");
553
+ });
554
+
555
+ test("an EMPTY installedTeams map does NOT block the send", async () => {
556
+ mockProjectAuth({ installedTeams: {} });
557
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
558
+
559
+ await sendCardToChannel();
560
+
561
+ expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
562
+ });
563
+
564
+ test("installedTeams containing ONLY a different team refuses before the Bot Framework call", async () => {
565
+ mockProjectAuth({
566
+ installedTeams: {
567
+ "team-2": buildInstalledTeam({ id: "team-2", name: "Marketing" }),
568
+ },
569
+ });
570
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
571
+
572
+ await expect(
573
+ sendCardToChannel({
574
+ teamId: TEAM_ID,
575
+ workspaceChannel: buildChannel({ name: "General" }),
576
+ }),
577
+ ).rejects.toThrow(
578
+ new BadDataException(
579
+ MicrosoftTeamsUtil.getBotNotInTeamMessage({ channelName: "General" }),
580
+ ),
581
+ );
582
+
583
+ expect(adapter.getBotAdapterSpy).not.toHaveBeenCalled();
584
+ expect(adapter.continueConversationAsync).not.toHaveBeenCalled();
585
+ });
586
+
587
+ test("installedTeams containing this team proceeds to the send", async () => {
588
+ mockProjectAuth({
589
+ installedTeams: {
590
+ [TEAM_ID]: buildInstalledTeam({ id: TEAM_ID }),
591
+ },
592
+ });
593
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
594
+
595
+ await sendCardToChannel();
596
+
597
+ expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
598
+ });
599
+
600
+ test("several installed teams, one of which is the target, proceeds", async () => {
601
+ mockProjectAuth({
602
+ installedTeams: {
603
+ "team-0": buildInstalledTeam({ id: "team-0" }),
604
+ [TEAM_ID]: buildInstalledTeam({ id: TEAM_ID }),
605
+ "team-2": buildInstalledTeam({ id: "team-2" }),
606
+ },
607
+ });
608
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
609
+
610
+ await sendCardToChannel();
611
+
612
+ expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
613
+ });
614
+
615
+ test("several installed teams, none of which is the target, refuses", async () => {
616
+ mockProjectAuth({
617
+ installedTeams: {
618
+ "team-0": buildInstalledTeam({ id: "team-0" }),
619
+ "team-2": buildInstalledTeam({ id: "team-2" }),
620
+ },
621
+ });
622
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
623
+
624
+ await expect(sendCardToChannel({ teamId: TEAM_ID })).rejects.toThrow(
625
+ /is not installed in the Microsoft Teams team/,
626
+ );
627
+ expect(adapter.continueConversationAsync).not.toHaveBeenCalled();
628
+ });
629
+
630
+ test("the team id lookup is exact — a differently-cased key does not count as installed", async () => {
631
+ mockProjectAuth({
632
+ installedTeams: {
633
+ "TEAM-1": buildInstalledTeam({ id: "TEAM-1" }),
634
+ },
635
+ });
636
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
637
+
638
+ await expect(sendCardToChannel({ teamId: "team-1" })).rejects.toThrow(
639
+ /is not installed in the Microsoft Teams team/,
640
+ );
641
+ expect(adapter.continueConversationAsync).not.toHaveBeenCalled();
642
+ });
643
+
644
+ test("the preflight refusal for a PRIVATE channel gives the channel-install instructions", async () => {
645
+ mockProjectAuth({
646
+ installedTeams: {
647
+ "team-2": buildInstalledTeam({ id: "team-2" }),
648
+ },
649
+ });
650
+ installFakeBotAdapter();
651
+
652
+ await expect(
653
+ sendCardToChannel({
654
+ workspaceChannel: buildChannel({
655
+ name: "Ops War Room",
656
+ membershipType: "private",
657
+ }),
658
+ }),
659
+ ).rejects.toThrow(
660
+ new BadDataException(
661
+ MicrosoftTeamsUtil.getBotNotInTeamMessage({
662
+ channelName: "Ops War Room",
663
+ membershipType: "private",
664
+ }),
665
+ ),
666
+ );
667
+ });
668
+
669
+ test("the preflight refusal is a BadDataException", async () => {
670
+ mockProjectAuth({
671
+ installedTeams: {
672
+ "team-2": buildInstalledTeam({ id: "team-2" }),
673
+ },
674
+ });
675
+ installFakeBotAdapter();
676
+
677
+ await expect(sendCardToChannel()).rejects.toBeInstanceOf(BadDataException);
678
+ });
679
+
680
+ test("the preflight never runs when the integration is missing entirely", async () => {
681
+ jest
682
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
683
+ .mockResolvedValue(null);
684
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
685
+
686
+ await expect(sendCardToChannel()).rejects.toThrow(
687
+ new BadDataException(
688
+ "Microsoft Teams integration not found for this project",
689
+ ),
690
+ );
691
+ expect(adapter.continueConversationAsync).not.toHaveBeenCalled();
692
+ });
693
+ });
694
+
695
+ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - conversation reference", () => {
696
+ test("happy path builds the channel conversation reference the Bot Framework expects", async () => {
697
+ mockProjectAuth({
698
+ installedTeams: {
699
+ [TEAM_ID]: buildInstalledTeam({ id: TEAM_ID }),
700
+ },
701
+ });
702
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
703
+ const channel: WorkspaceChannel = buildChannel({
704
+ id: "19:ops@thread.tacv2",
705
+ name: "Ops",
706
+ membershipType: "standard",
707
+ });
708
+
709
+ const thread: WorkspaceThread = await sendCardToChannel({
710
+ workspaceChannel: channel,
711
+ });
712
+
713
+ expect(adapter.capturedRefs).toHaveLength(1);
714
+ const ref: ConversationReference = adapter.capturedRefs[0]!;
715
+ expect(ref.conversation.id).toBe("19:ops@thread.tacv2");
716
+ expect(ref.conversation.name).toBe("Ops");
717
+ expect(ref.conversation.isGroup).toBe(true);
718
+ expect(ref.conversation.conversationType).toBe("channel");
719
+ expect(ref.conversation.tenantId).toBe(TENANT_ID);
720
+ expect(ref.channelId).toBe("msteams");
721
+ expect(ref.bot.id).toBe(MOCK_APP_CLIENT_ID);
722
+ expect(thread).toEqual({
723
+ channel: channel,
724
+ threadId: "msg-123",
725
+ });
726
+ });
727
+
728
+ test("the adaptive card is sent as an adaptive card attachment", async () => {
729
+ mockProjectAuth();
730
+ installFakeBotAdapter();
731
+
732
+ await sendCardToChannel();
733
+
734
+ expect(MessageFactory.attachment as jest.Mock).toHaveBeenCalledWith({
735
+ contentType: "application/vnd.microsoft.card.adaptive",
736
+ content: ADAPTIVE_CARD,
737
+ });
738
+ });
739
+
740
+ test("serviceUrl comes from the installed team record when it was captured", async () => {
741
+ mockProjectAuth({
742
+ installedTeams: {
743
+ [TEAM_ID]: buildInstalledTeam({
744
+ id: TEAM_ID,
745
+ serviceUrl: GCC_SERVICE_URL,
746
+ }),
747
+ },
748
+ });
749
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
750
+
751
+ await sendCardToChannel({ teamId: TEAM_ID });
752
+
753
+ expect(adapter.capturedRefs[0]!.serviceUrl).toBe(GCC_SERVICE_URL);
754
+ });
755
+
756
+ test("serviceUrl falls back to the commercial cloud endpoint when installedTeams is undefined", async () => {
757
+ mockProjectAuth({ miscData: baseMiscData() });
758
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
759
+
760
+ await sendCardToChannel();
761
+
762
+ expect(adapter.capturedRefs[0]!.serviceUrl).toBe(DEFAULT_SERVICE_URL);
763
+ });
764
+
765
+ test("serviceUrl falls back when the installed team record has no serviceUrl", async () => {
766
+ mockProjectAuth({
767
+ installedTeams: {
768
+ [TEAM_ID]: buildInstalledTeam({ id: TEAM_ID, serviceUrl: undefined }),
769
+ },
770
+ });
771
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
772
+
773
+ await sendCardToChannel({ teamId: TEAM_ID });
774
+
775
+ expect(adapter.capturedRefs[0]!.serviceUrl).toBe(DEFAULT_SERVICE_URL);
776
+ });
777
+
778
+ test("another team's serviceUrl is never borrowed for this team", async () => {
779
+ mockProjectAuth({
780
+ installedTeams: {
781
+ [TEAM_ID]: buildInstalledTeam({ id: TEAM_ID, serviceUrl: undefined }),
782
+ "team-2": buildInstalledTeam({
783
+ id: "team-2",
784
+ serviceUrl: GCC_SERVICE_URL,
785
+ }),
786
+ },
787
+ });
788
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
789
+
790
+ await sendCardToChannel({ teamId: TEAM_ID });
791
+
792
+ expect(adapter.capturedRefs[0]!.serviceUrl).toBe(DEFAULT_SERVICE_URL);
793
+ });
794
+
795
+ test("the serviceUrl of the requested team is used even when several teams are installed", async () => {
796
+ mockProjectAuth({
797
+ installedTeams: {
798
+ "team-0": buildInstalledTeam({
799
+ id: "team-0",
800
+ serviceUrl: "https://smba.trafficmanager.net/apac/",
801
+ }),
802
+ "team-2": buildInstalledTeam({
803
+ id: "team-2",
804
+ serviceUrl: GCC_SERVICE_URL,
805
+ }),
806
+ },
807
+ });
808
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
809
+
810
+ await sendCardToChannel({ teamId: "team-2" });
811
+
812
+ expect(adapter.capturedRefs[0]!.serviceUrl).toBe(GCC_SERVICE_URL);
813
+ });
814
+
815
+ test("threadId is an empty string when sendActivity returns nothing", async () => {
816
+ mockProjectAuth();
817
+ installFakeBotAdapter({ sendActivityResponse: undefined });
818
+
819
+ const thread: WorkspaceThread = await sendCardToChannel();
820
+
821
+ expect(thread.threadId).toBe("");
822
+ });
823
+ });
824
+
825
+ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - roster error translation", () => {
826
+ test("Microsoft's roster rejection is replaced with the parent-team install instructions", async () => {
827
+ mockProjectAuth();
828
+ installFakeBotAdapter({ adapterError: new Error(ROSTER_ERROR_TEXT) });
829
+
830
+ await expect(
831
+ sendCardToChannel({
832
+ workspaceChannel: buildChannel({
833
+ name: "General",
834
+ membershipType: "standard",
835
+ }),
836
+ }),
837
+ ).rejects.toThrow(
838
+ new BadDataException(
839
+ MicrosoftTeamsUtil.getBotNotInTeamMessage({
840
+ channelName: "General",
841
+ membershipType: "standard",
842
+ }),
843
+ ),
844
+ );
845
+ });
846
+
847
+ test("the raw Microsoft text never reaches the caller", async () => {
848
+ mockProjectAuth();
849
+ installFakeBotAdapter({ adapterError: new Error(ROSTER_ERROR_TEXT) });
850
+
851
+ let thrown: unknown = undefined;
852
+ try {
853
+ await sendCardToChannel();
854
+ } catch (err) {
855
+ thrown = err;
856
+ }
857
+
858
+ expect(thrown).toBeInstanceOf(BadDataException);
859
+ expect((thrown as Error).message).not.toContain("conversation roster");
860
+ expect((thrown as Error).message).toContain("Manage team > Apps");
861
+ });
862
+
863
+ test("a roster rejection on a private channel gets the channel-install instructions", async () => {
864
+ mockProjectAuth();
865
+ installFakeBotAdapter({ adapterError: new Error(ROSTER_ERROR_TEXT) });
866
+
867
+ await expect(
868
+ sendCardToChannel({
869
+ workspaceChannel: buildChannel({
870
+ name: "Ops War Room",
871
+ membershipType: "private",
872
+ }),
873
+ }),
874
+ ).rejects.toThrow(
875
+ new BadDataException(
876
+ MicrosoftTeamsUtil.getBotNotInTeamMessage({
877
+ channelName: "Ops War Room",
878
+ membershipType: "private",
879
+ }),
880
+ ),
881
+ );
882
+ });
883
+
884
+ test("a roster rejection raised inside the proactive callback is translated too", async () => {
885
+ mockProjectAuth();
886
+ installFakeBotAdapter({
887
+ sendActivityError: new Error(ROSTER_ERROR_TEXT),
888
+ });
889
+
890
+ await expect(sendCardToChannel()).rejects.toThrow(
891
+ /is not installed in the Microsoft Teams team/,
892
+ );
893
+ });
894
+
895
+ test("a roster rejection thrown as a plain string is translated too", async () => {
896
+ mockProjectAuth();
897
+ installFakeBotAdapter({
898
+ adapterError: "The bot is not part of the conversation roster.",
899
+ });
900
+
901
+ await expect(sendCardToChannel()).rejects.toThrow(
902
+ /is not installed in the Microsoft Teams team/,
903
+ );
904
+ });
905
+
906
+ test("unrelated adapter errors propagate untouched", async () => {
907
+ mockProjectAuth();
908
+ const networkError: Error = new Error("socket hang up");
909
+ installFakeBotAdapter({ adapterError: networkError });
910
+
911
+ await expect(sendCardToChannel()).rejects.toBe(networkError);
912
+ });
913
+
914
+ test("a 429 throttling error is not disguised as an install problem", async () => {
915
+ mockProjectAuth();
916
+ const throttled: Error = new Error("429 Too Many Requests");
917
+ installFakeBotAdapter({ adapterError: throttled });
918
+
919
+ let thrown: unknown = undefined;
920
+ try {
921
+ await sendCardToChannel();
922
+ } catch (err) {
923
+ thrown = err;
924
+ }
925
+
926
+ expect(thrown).toBe(throttled);
927
+ expect((thrown as Error).message).not.toContain("Manage team");
928
+ });
929
+ });
930
+
931
+ describe("MicrosoftTeamsUtil.sendMessage - unresolvable channel destinations are reported", () => {
932
+ function buildChannelPayload(data?: {
933
+ channelNames?: Array<string> | undefined;
934
+ channelIds?: Array<string> | undefined;
935
+ teamId?: string | undefined;
936
+ }): WorkspaceMessagePayload {
937
+ const block: WorkspacePayloadMarkdown = {
938
+ _type: "WorkspacePayloadMarkdown",
939
+ text: "Incident created",
940
+ };
941
+ return {
942
+ _type: "WorkspaceMessagePayload",
943
+ channelNames: data?.channelNames || [],
944
+ channelIds: data?.channelIds || [],
945
+ messageBlocks: [block],
946
+ workspaceType: WorkspaceType.MicrosoftTeams,
947
+ teamId: data?.teamId || TEAM_ID,
948
+ };
949
+ }
950
+
951
+ function callSendMessage(
952
+ payload: WorkspaceMessagePayload,
953
+ ): Promise<WorkspaceSendMessageResponse> {
954
+ return MicrosoftTeamsUtil.sendMessage({
955
+ workspaceMessagePayload: payload,
956
+ authToken: "auth-token",
957
+ userId: "user-1",
958
+ projectId: ObjectID.generate(),
959
+ });
960
+ }
961
+
962
+ function mockSendCardToChannel(): jest.SpyInstance {
963
+ return jest
964
+ .spyOn(MicrosoftTeamsUtil, "sendAdaptiveCardToChannel")
965
+ .mockImplementation(
966
+ async (args: {
967
+ workspaceChannel: WorkspaceChannel;
968
+ }): Promise<WorkspaceThread> => {
969
+ return {
970
+ channel: args.workspaceChannel,
971
+ threadId: `thread-${args.workspaceChannel.id}`,
972
+ };
973
+ },
974
+ );
975
+ }
976
+
977
+ test("a channelName that does not resolve is reported as an error, not a silent success", async () => {
978
+ jest
979
+ .spyOn(MicrosoftTeamsUtil, "getWorkspaceChannelByName")
980
+ .mockResolvedValue(null);
981
+ const sendSpy: jest.SpyInstance = mockSendCardToChannel();
982
+
983
+ const response: WorkspaceSendMessageResponse = await callSendMessage(
984
+ buildChannelPayload({ channelNames: ["deleted-channel"] }),
985
+ );
986
+
987
+ expect(response.threads).toEqual([]);
988
+ expect(response.errors).toEqual([
989
+ {
990
+ channel: {
991
+ id: "",
992
+ name: "deleted-channel",
993
+ workspaceType: WorkspaceType.MicrosoftTeams,
994
+ teamId: TEAM_ID,
995
+ },
996
+ error:
997
+ 'Channel "deleted-channel" was not found in this Microsoft Teams team. It may have been renamed or deleted.',
998
+ },
999
+ ]);
1000
+ expect(sendSpy).not.toHaveBeenCalled();
1001
+ expect(response.workspaceType).toBe(WorkspaceType.MicrosoftTeams);
1002
+ });
1003
+
1004
+ test("a channelName that resolves still sends and records no error", async () => {
1005
+ const channel: WorkspaceChannel = buildChannel({ name: "general" });
1006
+ jest
1007
+ .spyOn(MicrosoftTeamsUtil, "getWorkspaceChannelByName")
1008
+ .mockResolvedValue(channel);
1009
+ const sendSpy: jest.SpyInstance = mockSendCardToChannel();
1010
+
1011
+ const response: WorkspaceSendMessageResponse = await callSendMessage(
1012
+ buildChannelPayload({ channelNames: ["general"] }),
1013
+ );
1014
+
1015
+ expect(sendSpy).toHaveBeenCalledTimes(1);
1016
+ expect(response.errors).toEqual([]);
1017
+ expect(response.threads).toHaveLength(1);
1018
+ expect(response.threads[0]?.channel.id).toBe(CHANNEL_ID);
1019
+ });
1020
+
1021
+ test("a missing channel name does not stop the resolvable ones from sending", async () => {
1022
+ const channel: WorkspaceChannel = buildChannel({ name: "general" });
1023
+ jest
1024
+ .spyOn(MicrosoftTeamsUtil, "getWorkspaceChannelByName")
1025
+ .mockImplementation(
1026
+ async (args: {
1027
+ channelName: string;
1028
+ }): Promise<WorkspaceChannel | null> => {
1029
+ return args.channelName === "general" ? channel : null;
1030
+ },
1031
+ );
1032
+ mockSendCardToChannel();
1033
+
1034
+ const response: WorkspaceSendMessageResponse = await callSendMessage(
1035
+ buildChannelPayload({ channelNames: ["typoed", "general"] }),
1036
+ );
1037
+
1038
+ expect(response.threads).toHaveLength(1);
1039
+ expect(response.errors).toHaveLength(1);
1040
+ expect(response.errors?.[0]?.channel.name).toBe("typoed");
1041
+ });
1042
+
1043
+ test("every missing channel name gets its own error entry", async () => {
1044
+ jest
1045
+ .spyOn(MicrosoftTeamsUtil, "getWorkspaceChannelByName")
1046
+ .mockResolvedValue(null);
1047
+ mockSendCardToChannel();
1048
+
1049
+ const response: WorkspaceSendMessageResponse = await callSendMessage(
1050
+ buildChannelPayload({ channelNames: ["gone-1", "gone-2"] }),
1051
+ );
1052
+
1053
+ expect(response.threads).toEqual([]);
1054
+ expect(response.errors).toHaveLength(2);
1055
+ expect(response.errors?.[0]?.channel.name).toBe("gone-1");
1056
+ expect(response.errors?.[1]?.channel.name).toBe("gone-2");
1057
+ });
1058
+
1059
+ test("a channelId whose lookup throws is reported as an error instead of being swallowed", async () => {
1060
+ jest
1061
+ .spyOn(MicrosoftTeamsUtil, "getWorkspaceChannelFromChannelId")
1062
+ .mockRejectedValue(new Error("Channel not found in team") as never);
1063
+ const sendSpy: jest.SpyInstance = mockSendCardToChannel();
1064
+
1065
+ const response: WorkspaceSendMessageResponse = await callSendMessage(
1066
+ buildChannelPayload({ channelIds: ["19:missing@thread.tacv2"] }),
1067
+ );
1068
+
1069
+ expect(response.threads).toEqual([]);
1070
+ expect(response.errors).toEqual([
1071
+ {
1072
+ channel: {
1073
+ id: "19:missing@thread.tacv2",
1074
+ name: "19:missing@thread.tacv2",
1075
+ workspaceType: WorkspaceType.MicrosoftTeams,
1076
+ teamId: TEAM_ID,
1077
+ },
1078
+ error: "Channel not found in team",
1079
+ },
1080
+ ]);
1081
+ expect(sendSpy).not.toHaveBeenCalled();
1082
+ });
1083
+
1084
+ test("a non-Error thrown by the channelId lookup is stringified into the error entry", async () => {
1085
+ jest
1086
+ .spyOn(MicrosoftTeamsUtil, "getWorkspaceChannelFromChannelId")
1087
+ .mockRejectedValue("graph exploded" as never);
1088
+ mockSendCardToChannel();
1089
+
1090
+ const response: WorkspaceSendMessageResponse = await callSendMessage(
1091
+ buildChannelPayload({ channelIds: ["19:missing@thread.tacv2"] }),
1092
+ );
1093
+
1094
+ expect(response.errors).toHaveLength(1);
1095
+ expect(response.errors?.[0]?.error).toBe("graph exploded");
1096
+ });
1097
+
1098
+ test("one failing channelId does not stop the other channelIds from sending", async () => {
1099
+ const goodChannel: WorkspaceChannel = buildChannel({
1100
+ id: "19:good@thread.tacv2",
1101
+ name: "Good",
1102
+ });
1103
+ jest
1104
+ .spyOn(MicrosoftTeamsUtil, "getWorkspaceChannelFromChannelId")
1105
+ .mockImplementation(
1106
+ async (args: { channelId: string }): Promise<WorkspaceChannel> => {
1107
+ if (args.channelId === "19:bad@thread.tacv2") {
1108
+ throw new Error("lookup failed");
1109
+ }
1110
+ return goodChannel;
1111
+ },
1112
+ );
1113
+ mockSendCardToChannel();
1114
+
1115
+ const response: WorkspaceSendMessageResponse = await callSendMessage(
1116
+ buildChannelPayload({
1117
+ channelIds: ["19:bad@thread.tacv2", "19:good@thread.tacv2"],
1118
+ }),
1119
+ );
1120
+
1121
+ expect(response.threads).toHaveLength(1);
1122
+ expect(response.threads[0]?.channel.id).toBe("19:good@thread.tacv2");
1123
+ expect(response.errors).toHaveLength(1);
1124
+ expect(response.errors?.[0]?.channel.id).toBe("19:bad@thread.tacv2");
1125
+ });
1126
+
1127
+ test("no destinations at all produces an empty, error-free response", async () => {
1128
+ const sendSpy: jest.SpyInstance = mockSendCardToChannel();
1129
+
1130
+ const response: WorkspaceSendMessageResponse = await callSendMessage(
1131
+ buildChannelPayload(),
1132
+ );
1133
+
1134
+ expect(sendSpy).not.toHaveBeenCalled();
1135
+ expect(response.threads).toEqual([]);
1136
+ expect(response.errors).toEqual([]);
1137
+ });
1138
+
1139
+ test("the actionable install message from a failed send surfaces in response.errors", async () => {
1140
+ const channel: WorkspaceChannel = buildChannel({
1141
+ name: "General",
1142
+ membershipType: "standard",
1143
+ });
1144
+ jest
1145
+ .spyOn(MicrosoftTeamsUtil, "getWorkspaceChannelFromChannelId")
1146
+ .mockResolvedValue(channel);
1147
+ mockProjectAuth({
1148
+ installedTeams: {
1149
+ "team-2": buildInstalledTeam({ id: "team-2" }),
1150
+ },
1151
+ });
1152
+ const adapter: FakeBotAdapter = installFakeBotAdapter();
1153
+
1154
+ const response: WorkspaceSendMessageResponse = await callSendMessage(
1155
+ buildChannelPayload({ channelIds: [CHANNEL_ID] }),
1156
+ );
1157
+
1158
+ expect(adapter.continueConversationAsync).not.toHaveBeenCalled();
1159
+ expect(response.threads).toEqual([]);
1160
+ expect(response.errors).toHaveLength(1);
1161
+ expect(response.errors?.[0]?.channel).toEqual(channel);
1162
+ expect(response.errors?.[0]?.error).toBe(
1163
+ MicrosoftTeamsUtil.getBotNotInTeamMessage({
1164
+ channelName: "General",
1165
+ membershipType: "standard",
1166
+ }),
1167
+ );
1168
+ });
1169
+ });