@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,1605 @@
1
+ import { describe, expect, test, afterEach, beforeEach } from "@jest/globals";
2
+
3
+ /*
4
+ * Extensive tests for Microsoft Teams TEAM INSTALL capture in
5
+ * MicrosoftTeamsUtil.
6
+ *
7
+ * The bug being pinned: Microsoft tells us the OneUptime app was installed
8
+ * into a TEAM via conversationUpdate (bot in membersAdded) and
9
+ * installationUpdate (action add / add-upgrade). Both signals used to be
10
+ * routed only into captureChatFromBotActivity, which drops everything that is
11
+ * not a personal or groupChat conversation — so the team install was thrown
12
+ * away. Two things broke because of that:
13
+ *
14
+ * - we could not tell whether a proactive channel post would be accepted
15
+ * (Graph can LIST every team in the tenant, but the Bot Framework only
16
+ * accepts a post into a team the app is actually a roster member of), and
17
+ * - we lost the per-install serviceUrl, which GCC/DoD tenants need because
18
+ * their Bot Framework endpoint is not the global one.
19
+ *
20
+ * Covered here:
21
+ *
22
+ * - captureTeamFromBotActivity / removeTeamFromBotActivity: the activity ->
23
+ * MicrosoftTeamsInstalledTeam translation, including which fields are
24
+ * required, the serviceUrl precedence, and the swallow-don't-throw contract.
25
+ * - saveTeamToProjectAuthTokens / removeTeamFromProjectAuthTokens: installs
26
+ * are stored in miscData.installedTeams on EVERY WorkspaceProjectAuthToken
27
+ * row whose workspaceProjectId equals the Teams tenant id.
28
+ * - getInstalledTeamsForProject: the read side of the install store.
29
+ * - handleConversationUpdateActivity / handleInstallationUpdateActivity /
30
+ * handleBotMessageActivity: the wiring that must call BOTH the chat capture
31
+ * and the team capture, which is exactly what was missing before.
32
+ */
33
+
34
+ jest.mock("../../../../Server/EnvironmentConfig", () => {
35
+ return {
36
+ ...(jest.requireActual("../../../../Server/EnvironmentConfig") as Record<
37
+ string,
38
+ unknown
39
+ >),
40
+ MicrosoftTeamsAppClientId: "11111111-2222-3333-4444-555555555555",
41
+ MicrosoftTeamsAppClientSecret: "test-secret",
42
+ MicrosoftTeamsAppTenantId: "test-tenant",
43
+ };
44
+ });
45
+
46
+ /*
47
+ * Same botbuilder module factory as MicrosoftTeamsChats.test.ts — the
48
+ * repo-wide manual mock (Tests/__mocks__/botbuilder.js) does not expose
49
+ * TeamsInfo or MessageFactory.attachment, both of which MicrosoftTeams.ts
50
+ * touches at import time.
51
+ */
52
+ jest.mock("botbuilder", () => {
53
+ return {
54
+ CloudAdapter: class CloudAdapter {},
55
+ ConfigurationBotFrameworkAuthentication: class ConfigurationBotFrameworkAuthentication {},
56
+ TeamsActivityHandler: class TeamsActivityHandler {},
57
+ TurnContext: class TurnContext {},
58
+ ActivityHandler: class ActivityHandler {},
59
+ MessageFactory: {
60
+ text: jest.fn(),
61
+ attachment: jest.fn((attachment: unknown) => {
62
+ return { type: "message", attachments: [attachment] };
63
+ }),
64
+ },
65
+ CardFactory: { heroCard: jest.fn() },
66
+ TeamsInfo: {
67
+ getMembers: jest.fn(),
68
+ getPagedMembers: jest.fn(),
69
+ },
70
+ };
71
+ });
72
+
73
+ import MicrosoftTeamsUtil, {
74
+ MicrosoftTeamsTenantResolution,
75
+ } from "../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
76
+ import WorkspaceProjectAuthTokenService from "../../../../Server/Services/WorkspaceProjectAuthTokenService";
77
+ import WorkspaceProjectAuthToken, {
78
+ MicrosoftTeamsChat,
79
+ MicrosoftTeamsInstalledTeam,
80
+ MicrosoftTeamsMiscData,
81
+ MicrosoftTeamsTeam,
82
+ } from "../../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
83
+ import WorkspaceType from "../../../../Types/Workspace/WorkspaceType";
84
+ import ObjectID from "../../../../Types/ObjectID";
85
+ import LIMIT_MAX from "../../../../Types/Database/LimitMax";
86
+ import { JSONObject } from "../../../../Types/JSON";
87
+ import { TeamsInfo, type TurnContext } from "botbuilder";
88
+
89
+ const BOT_RECIPIENT_ID: string = "bot-recipient-id";
90
+ const TURN_CONTEXT_SERVICE_URL: string =
91
+ "https://smba.trafficmanager.net/emea/";
92
+ const ACTIVITY_SERVICE_URL: string = "https://smba.trafficmanager.net/amer/";
93
+ const GCC_SERVICE_URL: string =
94
+ "https://smba.infra.gov.teams.microsoft.us/gov/";
95
+ const TEAM_ID: string = "19:team-aaa@thread.tacv2";
96
+ const TENANT_ID: string = "tenant-1";
97
+ const ISO_TIMESTAMP: RegExp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
98
+
99
+ interface UpdateOneByIdArgs {
100
+ id: ObjectID;
101
+ data: { miscData: MicrosoftTeamsMiscData };
102
+ }
103
+
104
+ function buildTurnContext(data?: {
105
+ serviceUrl?: string | undefined;
106
+ recipientId?: string | undefined;
107
+ }): TurnContext {
108
+ return {
109
+ activity: {
110
+ recipient: { id: data?.recipientId || BOT_RECIPIENT_ID },
111
+ serviceUrl:
112
+ data && "serviceUrl" in data
113
+ ? data.serviceUrl
114
+ : TURN_CONTEXT_SERVICE_URL,
115
+ conversation: { id: "ctx-conversation-id" },
116
+ },
117
+ sendActivity: async (): Promise<undefined> => {
118
+ return undefined;
119
+ },
120
+ } as unknown as TurnContext;
121
+ }
122
+
123
+ function buildProjectAuthRow(data: {
124
+ id?: ObjectID | undefined;
125
+ workspaceProjectId?: string | undefined;
126
+ miscData?: MicrosoftTeamsMiscData | undefined;
127
+ }): WorkspaceProjectAuthToken {
128
+ return {
129
+ id: data.id || ObjectID.generate(),
130
+ workspaceProjectId: data.workspaceProjectId,
131
+ miscData: data.miscData,
132
+ } as unknown as WorkspaceProjectAuthToken;
133
+ }
134
+
135
+ function baseMiscData(
136
+ overrides?: Partial<MicrosoftTeamsMiscData>,
137
+ ): MicrosoftTeamsMiscData {
138
+ return {
139
+ tenantId: "tenant-xyz",
140
+ teamId: "team-1",
141
+ teamName: "Engineering",
142
+ botId: "bot-1",
143
+ ...(overrides || {}),
144
+ } as MicrosoftTeamsMiscData;
145
+ }
146
+
147
+ function buildInstalledTeam(data?: {
148
+ id?: string | undefined;
149
+ name?: string | undefined;
150
+ serviceUrl?: string | undefined;
151
+ }): MicrosoftTeamsInstalledTeam {
152
+ return {
153
+ id: data?.id || TEAM_ID,
154
+ name: data?.name || "Engineering",
155
+ serviceUrl:
156
+ data && "serviceUrl" in data ? data.serviceUrl : TURN_CONTEXT_SERVICE_URL,
157
+ addedAt: "2026-07-27T00:00:00.000Z",
158
+ };
159
+ }
160
+
161
+ function buildChat(data?: {
162
+ id?: string | undefined;
163
+ name?: string | undefined;
164
+ }): MicrosoftTeamsChat {
165
+ return {
166
+ id: data?.id || "19:groupchat@thread.v2",
167
+ name: data?.name || "Alice, Bob",
168
+ chatType: "groupChat",
169
+ serviceUrl: TURN_CONTEXT_SERVICE_URL,
170
+ addedAt: "2026-07-27T00:00:00.000Z",
171
+ };
172
+ }
173
+
174
+ /*
175
+ * A team-scoped bot activity as Teams delivers it: the team id and tenant id
176
+ * live under channelData, never on the conversation object.
177
+ */
178
+ function teamActivity(overrides?: JSONObject): JSONObject {
179
+ return {
180
+ channelData: {
181
+ team: { id: TEAM_ID, name: "Engineering" },
182
+ tenant: { id: TENANT_ID },
183
+ channel: { id: "19:general@thread.tacv2" },
184
+ },
185
+ conversation: {
186
+ conversationType: "channel",
187
+ id: "19:general@thread.tacv2",
188
+ },
189
+ serviceUrl: ACTIVITY_SERVICE_URL,
190
+ ...(overrides || {}),
191
+ };
192
+ }
193
+
194
+ function getUpdateArgs(
195
+ updateSpy: jest.SpyInstance,
196
+ callIndex: number,
197
+ ): UpdateOneByIdArgs {
198
+ return updateSpy.mock.calls[callIndex]?.[0] as unknown as UpdateOneByIdArgs;
199
+ }
200
+
201
+ /*
202
+ * Fan-out assertions must be anchored to the row they are about. Looking the
203
+ * call up by id (rather than by position) is what makes "row two got row one's
204
+ * miscData" a failure instead of a coincidence.
205
+ */
206
+ function getUpdateArgsForId(
207
+ updateSpy: jest.SpyInstance,
208
+ id: ObjectID,
209
+ ): UpdateOneByIdArgs {
210
+ const calls: Array<Array<unknown>> = updateSpy.mock.calls as Array<
211
+ Array<unknown>
212
+ >;
213
+
214
+ for (const call of calls) {
215
+ const args: UpdateOneByIdArgs = call[0] as UpdateOneByIdArgs;
216
+
217
+ if (String(args.id) === id.toString()) {
218
+ return args;
219
+ }
220
+ }
221
+
222
+ throw new Error(`No updateOneById call was made for id ${id.toString()}`);
223
+ }
224
+
225
+ interface DistinctRowFixture {
226
+ id: ObjectID;
227
+ tenantId: string;
228
+ teamName: string;
229
+ botId: string;
230
+ existingTeam: MicrosoftTeamsInstalledTeam;
231
+ chat: MicrosoftTeamsChat;
232
+ }
233
+
234
+ /*
235
+ * Every fan-out row must be DISTINGUISHABLE from its siblings. If all rows are
236
+ * built from the same baseMiscData(), a bug that merges once and writes the
237
+ * same object to every row is invisible: each row's assertions pass against
238
+ * every other row's data. Each fixture therefore owns its scalars, its
239
+ * pre-existing installedTeams entry and its availableChats entry.
240
+ */
241
+ function buildDistinctRowFixture(label: string): DistinctRowFixture {
242
+ return {
243
+ id: ObjectID.generate(),
244
+ tenantId: `tenant-${label}`,
245
+ teamName: `${label} team name`,
246
+ botId: `bot-${label}`,
247
+ existingTeam: buildInstalledTeam({
248
+ id: `19:${label}-existing@thread.tacv2`,
249
+ name: `${label} existing team`,
250
+ serviceUrl: `https://smba.trafficmanager.net/${label}/`,
251
+ }),
252
+ chat: buildChat({
253
+ id: `19:${label}-chat@thread.v2`,
254
+ name: `${label} chat`,
255
+ }),
256
+ };
257
+ }
258
+
259
+ function buildRowFromFixture(data: {
260
+ fixture: DistinctRowFixture;
261
+ installedTeams?: Record<string, MicrosoftTeamsInstalledTeam> | undefined;
262
+ }): WorkspaceProjectAuthToken {
263
+ const overrides: Partial<MicrosoftTeamsMiscData> = {
264
+ tenantId: data.fixture.tenantId,
265
+ teamName: data.fixture.teamName,
266
+ botId: data.fixture.botId,
267
+ availableChats: { [data.fixture.chat.id]: data.fixture.chat },
268
+ };
269
+
270
+ if (data.installedTeams) {
271
+ overrides.installedTeams = data.installedTeams;
272
+ }
273
+
274
+ return buildProjectAuthRow({
275
+ id: data.fixture.id,
276
+ miscData: baseMiscData(overrides),
277
+ });
278
+ }
279
+
280
+ function getUpdatedIds(updateSpy: jest.SpyInstance): Array<string> {
281
+ const calls: Array<Array<unknown>> = updateSpy.mock.calls as Array<
282
+ Array<unknown>
283
+ >;
284
+ const ids: Array<string> = [];
285
+
286
+ for (const call of calls) {
287
+ ids.push(String((call[0] as UpdateOneByIdArgs).id));
288
+ }
289
+
290
+ return ids;
291
+ }
292
+
293
+ function buildRowsFromFixtures(data: {
294
+ fixtures: Array<DistinctRowFixture>;
295
+ sharedTeam?: MicrosoftTeamsInstalledTeam | undefined;
296
+ }): Array<WorkspaceProjectAuthToken> {
297
+ const rows: Array<WorkspaceProjectAuthToken> = [];
298
+
299
+ for (const fixture of data.fixtures) {
300
+ const installedTeams: Record<string, MicrosoftTeamsInstalledTeam> = {
301
+ [fixture.existingTeam.id]: fixture.existingTeam,
302
+ };
303
+
304
+ if (data.sharedTeam) {
305
+ installedTeams[data.sharedTeam.id] = data.sharedTeam;
306
+ }
307
+
308
+ rows.push(
309
+ buildRowFromFixture({
310
+ fixture: fixture,
311
+ installedTeams: installedTeams,
312
+ }),
313
+ );
314
+ }
315
+
316
+ return rows;
317
+ }
318
+
319
+ function fixtureIds(fixtures: Array<DistinctRowFixture>): Array<string> {
320
+ return fixtures.map((fixture: DistinctRowFixture): string => {
321
+ return fixture.id.toString();
322
+ });
323
+ }
324
+
325
+ function mockFindBy(rows: Array<WorkspaceProjectAuthToken>): jest.SpyInstance {
326
+ return jest
327
+ .spyOn(WorkspaceProjectAuthTokenService, "findBy")
328
+ .mockResolvedValue(rows);
329
+ }
330
+
331
+ function mockUpdateOneById(): jest.SpyInstance {
332
+ return jest
333
+ .spyOn(WorkspaceProjectAuthTokenService, "updateOneById")
334
+ .mockResolvedValue(undefined as never);
335
+ }
336
+
337
+ afterEach(() => {
338
+ jest.restoreAllMocks();
339
+ });
340
+
341
+ /*
342
+ * The botbuilder module mock's jest.fn()s are shared by every test in this
343
+ * file — jest.spyOn on an existing mock returns the same function, so call
344
+ * history and mockResolvedValueOnce queues leak between tests unless reset.
345
+ */
346
+ beforeEach(() => {
347
+ (TeamsInfo.getPagedMembers as jest.Mock).mockReset();
348
+ (TeamsInfo.getMembers as jest.Mock).mockReset();
349
+ });
350
+
351
+ describe("MicrosoftTeamsUtil.captureTeamFromBotActivity", () => {
352
+ test("builds an installed team record from channelData and saves it under the tenant", async () => {
353
+ const saveSpy: jest.SpyInstance = jest
354
+ .spyOn(MicrosoftTeamsUtil, "saveTeamToProjectAuthTokens")
355
+ .mockResolvedValue(undefined as never);
356
+
357
+ await MicrosoftTeamsUtil.captureTeamFromBotActivity({
358
+ activity: teamActivity(),
359
+ turnContext: buildTurnContext(),
360
+ });
361
+
362
+ expect(saveSpy).toHaveBeenCalledTimes(1);
363
+ const saveArgs: { tenantId: string; team: MicrosoftTeamsInstalledTeam } =
364
+ saveSpy.mock.calls[0]?.[0] as {
365
+ tenantId: string;
366
+ team: MicrosoftTeamsInstalledTeam;
367
+ };
368
+ expect(saveArgs.tenantId).toBe(TENANT_ID);
369
+ expect(saveArgs.team.id).toBe(TEAM_ID);
370
+ expect(saveArgs.team.name).toBe("Engineering");
371
+ expect(saveArgs.team.serviceUrl).toBe(ACTIVITY_SERVICE_URL);
372
+ });
373
+
374
+ test("addedAt is an ISO-8601 string", async () => {
375
+ const saveSpy: jest.SpyInstance = jest
376
+ .spyOn(MicrosoftTeamsUtil, "saveTeamToProjectAuthTokens")
377
+ .mockResolvedValue(undefined as never);
378
+
379
+ await MicrosoftTeamsUtil.captureTeamFromBotActivity({
380
+ activity: teamActivity(),
381
+ turnContext: buildTurnContext(),
382
+ });
383
+
384
+ const team: MicrosoftTeamsInstalledTeam = (
385
+ saveSpy.mock.calls[0]?.[0] as { team: MicrosoftTeamsInstalledTeam }
386
+ ).team;
387
+ expect(typeof team.addedAt).toBe("string");
388
+ expect(team.addedAt).toMatch(ISO_TIMESTAMP);
389
+ expect(Number.isNaN(new Date(team.addedAt as string).getTime())).toBe(
390
+ false,
391
+ );
392
+ });
393
+
394
+ test("serviceUrl prefers the activity over the turn context", async () => {
395
+ const saveSpy: jest.SpyInstance = jest
396
+ .spyOn(MicrosoftTeamsUtil, "saveTeamToProjectAuthTokens")
397
+ .mockResolvedValue(undefined as never);
398
+
399
+ await MicrosoftTeamsUtil.captureTeamFromBotActivity({
400
+ activity: teamActivity({ serviceUrl: GCC_SERVICE_URL }),
401
+ turnContext: buildTurnContext({ serviceUrl: TURN_CONTEXT_SERVICE_URL }),
402
+ });
403
+
404
+ const team: MicrosoftTeamsInstalledTeam = (
405
+ saveSpy.mock.calls[0]?.[0] as { team: MicrosoftTeamsInstalledTeam }
406
+ ).team;
407
+ expect(team.serviceUrl).toBe(GCC_SERVICE_URL);
408
+ });
409
+
410
+ test("serviceUrl falls back to the turn context when the activity has none", async () => {
411
+ const saveSpy: jest.SpyInstance = jest
412
+ .spyOn(MicrosoftTeamsUtil, "saveTeamToProjectAuthTokens")
413
+ .mockResolvedValue(undefined as never);
414
+ const activity: JSONObject = teamActivity();
415
+ delete activity["serviceUrl"];
416
+
417
+ await MicrosoftTeamsUtil.captureTeamFromBotActivity({
418
+ activity: activity,
419
+ turnContext: buildTurnContext({ serviceUrl: GCC_SERVICE_URL }),
420
+ });
421
+
422
+ const team: MicrosoftTeamsInstalledTeam = (
423
+ saveSpy.mock.calls[0]?.[0] as { team: MicrosoftTeamsInstalledTeam }
424
+ ).team;
425
+ expect(team.serviceUrl).toBe(GCC_SERVICE_URL);
426
+ });
427
+
428
+ test("serviceUrl is undefined when neither the activity nor the turn context has one", async () => {
429
+ const saveSpy: jest.SpyInstance = jest
430
+ .spyOn(MicrosoftTeamsUtil, "saveTeamToProjectAuthTokens")
431
+ .mockResolvedValue(undefined as never);
432
+ const activity: JSONObject = teamActivity();
433
+ delete activity["serviceUrl"];
434
+
435
+ await MicrosoftTeamsUtil.captureTeamFromBotActivity({
436
+ activity: activity,
437
+ turnContext: buildTurnContext({ serviceUrl: undefined }),
438
+ });
439
+
440
+ expect(saveSpy).toHaveBeenCalledTimes(1);
441
+ const team: MicrosoftTeamsInstalledTeam = (
442
+ saveSpy.mock.calls[0]?.[0] as { team: MicrosoftTeamsInstalledTeam }
443
+ ).team;
444
+ expect(team.serviceUrl).toBeUndefined();
445
+ });
446
+
447
+ test("team name absent leaves name undefined but still records the install", async () => {
448
+ const saveSpy: jest.SpyInstance = jest
449
+ .spyOn(MicrosoftTeamsUtil, "saveTeamToProjectAuthTokens")
450
+ .mockResolvedValue(undefined as never);
451
+
452
+ await MicrosoftTeamsUtil.captureTeamFromBotActivity({
453
+ activity: teamActivity({
454
+ channelData: { team: { id: TEAM_ID }, tenant: { id: TENANT_ID } },
455
+ }),
456
+ turnContext: buildTurnContext(),
457
+ });
458
+
459
+ expect(saveSpy).toHaveBeenCalledTimes(1);
460
+ const team: MicrosoftTeamsInstalledTeam = (
461
+ saveSpy.mock.calls[0]?.[0] as { team: MicrosoftTeamsInstalledTeam }
462
+ ).team;
463
+ expect(team.id).toBe(TEAM_ID);
464
+ expect(team.name).toBeUndefined();
465
+ });
466
+
467
+ test("an empty-string team name is normalized to undefined", async () => {
468
+ const saveSpy: jest.SpyInstance = jest
469
+ .spyOn(MicrosoftTeamsUtil, "saveTeamToProjectAuthTokens")
470
+ .mockResolvedValue(undefined as never);
471
+
472
+ await MicrosoftTeamsUtil.captureTeamFromBotActivity({
473
+ activity: teamActivity({
474
+ channelData: {
475
+ team: { id: TEAM_ID, name: "" },
476
+ tenant: { id: TENANT_ID },
477
+ },
478
+ }),
479
+ turnContext: buildTurnContext(),
480
+ });
481
+
482
+ const team: MicrosoftTeamsInstalledTeam = (
483
+ saveSpy.mock.calls[0]?.[0] as { team: MicrosoftTeamsInstalledTeam }
484
+ ).team;
485
+ expect(team.name).toBeUndefined();
486
+ });
487
+
488
+ test("missing team id short-circuits before ANY database read", async () => {
489
+ const findBySpy: jest.SpyInstance = mockFindBy([]);
490
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
491
+
492
+ await MicrosoftTeamsUtil.captureTeamFromBotActivity({
493
+ activity: teamActivity({
494
+ channelData: { tenant: { id: TENANT_ID } },
495
+ }),
496
+ turnContext: buildTurnContext(),
497
+ });
498
+
499
+ expect(findBySpy).not.toHaveBeenCalled();
500
+ expect(updateSpy).not.toHaveBeenCalled();
501
+ });
502
+
503
+ test("a personal chat activity (no channelData at all) is ignored without throwing", async () => {
504
+ const findBySpy: jest.SpyInstance = mockFindBy([]);
505
+
506
+ await expect(
507
+ MicrosoftTeamsUtil.captureTeamFromBotActivity({
508
+ activity: {
509
+ conversation: { conversationType: "personal", id: "a:1personal" },
510
+ serviceUrl: ACTIVITY_SERVICE_URL,
511
+ },
512
+ turnContext: buildTurnContext(),
513
+ }),
514
+ ).resolves.toBeUndefined();
515
+
516
+ expect(findBySpy).not.toHaveBeenCalled();
517
+ });
518
+
519
+ test("an empty-string team id is treated as missing", async () => {
520
+ const findBySpy: jest.SpyInstance = mockFindBy([]);
521
+
522
+ await MicrosoftTeamsUtil.captureTeamFromBotActivity({
523
+ activity: teamActivity({
524
+ channelData: { team: { id: "" }, tenant: { id: TENANT_ID } },
525
+ }),
526
+ turnContext: buildTurnContext(),
527
+ });
528
+
529
+ expect(findBySpy).not.toHaveBeenCalled();
530
+ });
531
+
532
+ test("missing tenant id records nothing (a team install we cannot attribute is useless)", async () => {
533
+ const saveSpy: jest.SpyInstance = jest
534
+ .spyOn(MicrosoftTeamsUtil, "saveTeamToProjectAuthTokens")
535
+ .mockResolvedValue(undefined as never);
536
+
537
+ await MicrosoftTeamsUtil.captureTeamFromBotActivity({
538
+ activity: teamActivity({
539
+ channelData: { team: { id: TEAM_ID, name: "Engineering" } },
540
+ }),
541
+ turnContext: buildTurnContext(),
542
+ });
543
+
544
+ expect(saveSpy).not.toHaveBeenCalled();
545
+ });
546
+
547
+ test("an empty-string tenant id records nothing", async () => {
548
+ const saveSpy: jest.SpyInstance = jest
549
+ .spyOn(MicrosoftTeamsUtil, "saveTeamToProjectAuthTokens")
550
+ .mockResolvedValue(undefined as never);
551
+
552
+ await MicrosoftTeamsUtil.captureTeamFromBotActivity({
553
+ activity: teamActivity({
554
+ channelData: { team: { id: TEAM_ID }, tenant: { id: "" } },
555
+ }),
556
+ turnContext: buildTurnContext(),
557
+ });
558
+
559
+ expect(saveSpy).not.toHaveBeenCalled();
560
+ });
561
+
562
+ test("a throwing save is swallowed, not propagated (a failed capture must never 500 the bot endpoint)", async () => {
563
+ jest
564
+ .spyOn(MicrosoftTeamsUtil, "saveTeamToProjectAuthTokens")
565
+ .mockRejectedValue(new Error("database is down") as never);
566
+
567
+ await expect(
568
+ MicrosoftTeamsUtil.captureTeamFromBotActivity({
569
+ activity: teamActivity(),
570
+ turnContext: buildTurnContext(),
571
+ }),
572
+ ).resolves.toBeUndefined();
573
+ });
574
+ });
575
+
576
+ describe("MicrosoftTeamsUtil.removeTeamFromBotActivity", () => {
577
+ test("removes the team install for the tenant on the activity", async () => {
578
+ const removeSpy: jest.SpyInstance = jest
579
+ .spyOn(MicrosoftTeamsUtil, "removeTeamFromProjectAuthTokens")
580
+ .mockResolvedValue(undefined as never);
581
+
582
+ await MicrosoftTeamsUtil.removeTeamFromBotActivity({
583
+ activity: teamActivity(),
584
+ });
585
+
586
+ expect(removeSpy).toHaveBeenCalledTimes(1);
587
+ expect(removeSpy).toHaveBeenCalledWith({
588
+ tenantId: TENANT_ID,
589
+ teamId: TEAM_ID,
590
+ });
591
+ });
592
+
593
+ test("missing team id removes nothing", async () => {
594
+ const removeSpy: jest.SpyInstance = jest
595
+ .spyOn(MicrosoftTeamsUtil, "removeTeamFromProjectAuthTokens")
596
+ .mockResolvedValue(undefined as never);
597
+
598
+ await MicrosoftTeamsUtil.removeTeamFromBotActivity({
599
+ activity: teamActivity({ channelData: { tenant: { id: TENANT_ID } } }),
600
+ });
601
+
602
+ expect(removeSpy).not.toHaveBeenCalled();
603
+ });
604
+
605
+ test("missing tenant id removes nothing", async () => {
606
+ const removeSpy: jest.SpyInstance = jest
607
+ .spyOn(MicrosoftTeamsUtil, "removeTeamFromProjectAuthTokens")
608
+ .mockResolvedValue(undefined as never);
609
+
610
+ await MicrosoftTeamsUtil.removeTeamFromBotActivity({
611
+ activity: teamActivity({ channelData: { team: { id: TEAM_ID } } }),
612
+ });
613
+
614
+ expect(removeSpy).not.toHaveBeenCalled();
615
+ });
616
+
617
+ test("an activity with no channelData removes nothing and does not throw", async () => {
618
+ const removeSpy: jest.SpyInstance = jest
619
+ .spyOn(MicrosoftTeamsUtil, "removeTeamFromProjectAuthTokens")
620
+ .mockResolvedValue(undefined as never);
621
+
622
+ await expect(
623
+ MicrosoftTeamsUtil.removeTeamFromBotActivity({ activity: {} }),
624
+ ).resolves.toBeUndefined();
625
+
626
+ expect(removeSpy).not.toHaveBeenCalled();
627
+ });
628
+
629
+ test("a throwing removal is swallowed, not propagated", async () => {
630
+ jest
631
+ .spyOn(MicrosoftTeamsUtil, "removeTeamFromProjectAuthTokens")
632
+ .mockRejectedValue(new Error("database is down") as never);
633
+
634
+ await expect(
635
+ MicrosoftTeamsUtil.removeTeamFromBotActivity({
636
+ activity: teamActivity(),
637
+ }),
638
+ ).resolves.toBeUndefined();
639
+ });
640
+ });
641
+
642
+ describe("MicrosoftTeamsUtil.saveTeamToProjectAuthTokens", () => {
643
+ test("queries project auth rows by MicrosoftTeams workspace type and tenant id", async () => {
644
+ const findBySpy: jest.SpyInstance = mockFindBy([]);
645
+
646
+ await MicrosoftTeamsUtil.saveTeamToProjectAuthTokens({
647
+ tenantId: "tenant-abc",
648
+ team: buildInstalledTeam(),
649
+ });
650
+
651
+ expect(findBySpy).toHaveBeenCalledTimes(1);
652
+ expect(findBySpy).toHaveBeenCalledWith(
653
+ expect.objectContaining({
654
+ query: {
655
+ workspaceType: WorkspaceType.MicrosoftTeams,
656
+ workspaceProjectId: "tenant-abc",
657
+ },
658
+ select: {
659
+ _id: true,
660
+ miscData: true,
661
+ },
662
+ limit: LIMIT_MAX,
663
+ skip: 0,
664
+ props: {
665
+ isRoot: true,
666
+ },
667
+ }),
668
+ );
669
+ });
670
+
671
+ test("fans out to EVERY project connected to the tenant, merging into each row's OWN miscData (3 rows -> 3 updates)", async () => {
672
+ /*
673
+ * Three rows that share nothing: different scalars, different pre-existing
674
+ * installedTeams and different availableChats. Merging once and writing the
675
+ * result to all three rows must fail here, not pass by symmetry.
676
+ */
677
+ const fixtures: Array<DistinctRowFixture> = [
678
+ buildDistinctRowFixture("row-one"),
679
+ buildDistinctRowFixture("row-two"),
680
+ buildDistinctRowFixture("row-three"),
681
+ ];
682
+ mockFindBy(buildRowsFromFixtures({ fixtures: fixtures }));
683
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
684
+
685
+ const team: MicrosoftTeamsInstalledTeam = buildInstalledTeam();
686
+ await MicrosoftTeamsUtil.saveTeamToProjectAuthTokens({
687
+ tenantId: "tenant-abc",
688
+ team: team,
689
+ });
690
+
691
+ expect(updateSpy).toHaveBeenCalledTimes(3);
692
+ expect(getUpdatedIds(updateSpy)).toEqual(fixtureIds(fixtures));
693
+
694
+ const savedObjects: Set<MicrosoftTeamsMiscData> =
695
+ new Set<MicrosoftTeamsMiscData>();
696
+
697
+ for (const fixture of fixtures) {
698
+ // Anchored on this row's id: a sibling's payload cannot stand in for it.
699
+ const savedMiscData: MicrosoftTeamsMiscData = getUpdateArgsForId(
700
+ updateSpy,
701
+ fixture.id,
702
+ ).data.miscData;
703
+ savedObjects.add(savedMiscData);
704
+
705
+ /*
706
+ * The new install is added on top of THIS row's install history. The
707
+ * exact-map comparison also proves no sibling's team leaked in.
708
+ */
709
+ expect(savedMiscData.installedTeams).toEqual({
710
+ [fixture.existingTeam.id]: fixture.existingTeam,
711
+ [team.id]: team,
712
+ });
713
+ // This row's chat store is untouched, and is not a sibling's chat store.
714
+ expect(savedMiscData.availableChats).toEqual({
715
+ [fixture.chat.id]: fixture.chat,
716
+ });
717
+ expect(savedMiscData.tenantId).toBe(fixture.tenantId);
718
+ expect(savedMiscData.teamName).toBe(fixture.teamName);
719
+ expect(savedMiscData.botId).toBe(fixture.botId);
720
+ }
721
+
722
+ /*
723
+ * Each row must be written a miscData object of its own — one shared object
724
+ * handed to all three updates is the exact defect this test guards.
725
+ */
726
+ expect(savedObjects.size).toBe(3);
727
+ });
728
+
729
+ test("updates are made with root props", async () => {
730
+ mockFindBy([buildProjectAuthRow({ miscData: baseMiscData() })]);
731
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
732
+
733
+ await MicrosoftTeamsUtil.saveTeamToProjectAuthTokens({
734
+ tenantId: "tenant-abc",
735
+ team: buildInstalledTeam(),
736
+ });
737
+
738
+ expect(updateSpy).toHaveBeenCalledWith(
739
+ expect.objectContaining({ props: { isRoot: true } }),
740
+ );
741
+ });
742
+
743
+ test("merging an install preserves other miscData fields, availableChats and other installed teams", async () => {
744
+ const existingTeam: MicrosoftTeamsInstalledTeam = buildInstalledTeam({
745
+ id: "19:other-team@thread.tacv2",
746
+ name: "Support",
747
+ });
748
+ const existingChat: MicrosoftTeamsChat = buildChat();
749
+ mockFindBy([
750
+ buildProjectAuthRow({
751
+ miscData: baseMiscData({
752
+ appAccessToken: "token-abc",
753
+ adminConsentGranted: true,
754
+ availableChats: { [existingChat.id]: existingChat },
755
+ installedTeams: { [existingTeam.id]: existingTeam },
756
+ }),
757
+ }),
758
+ ]);
759
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
760
+
761
+ const newTeam: MicrosoftTeamsInstalledTeam = buildInstalledTeam();
762
+ await MicrosoftTeamsUtil.saveTeamToProjectAuthTokens({
763
+ tenantId: "tenant-abc",
764
+ team: newTeam,
765
+ });
766
+
767
+ expect(updateSpy).toHaveBeenCalledTimes(1);
768
+ const savedMiscData: MicrosoftTeamsMiscData = getUpdateArgs(updateSpy, 0)
769
+ .data.miscData;
770
+ expect(savedMiscData.tenantId).toBe("tenant-xyz");
771
+ expect(savedMiscData.teamId).toBe("team-1");
772
+ expect(savedMiscData.teamName).toBe("Engineering");
773
+ expect(savedMiscData.botId).toBe("bot-1");
774
+ expect(savedMiscData.appAccessToken).toBe("token-abc");
775
+ expect(savedMiscData.adminConsentGranted).toBe(true);
776
+ // The chat store must survive a team install untouched.
777
+ expect(savedMiscData.availableChats).toEqual({
778
+ [existingChat.id]: existingChat,
779
+ });
780
+ expect(savedMiscData.installedTeams).toEqual({
781
+ [existingTeam.id]: existingTeam,
782
+ [newTeam.id]: newTeam,
783
+ });
784
+ });
785
+
786
+ test("re-installing the same team id overwrites the stored record (upsert, keeps serviceUrl fresh)", async () => {
787
+ const staleTeam: MicrosoftTeamsInstalledTeam = buildInstalledTeam({
788
+ name: "Old name",
789
+ serviceUrl: "https://smba.trafficmanager.net/OLD/",
790
+ });
791
+ mockFindBy([
792
+ buildProjectAuthRow({
793
+ miscData: baseMiscData({
794
+ installedTeams: { [staleTeam.id]: staleTeam },
795
+ }),
796
+ }),
797
+ ]);
798
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
799
+
800
+ const freshTeam: MicrosoftTeamsInstalledTeam = buildInstalledTeam({
801
+ name: "New name",
802
+ serviceUrl: GCC_SERVICE_URL,
803
+ });
804
+ await MicrosoftTeamsUtil.saveTeamToProjectAuthTokens({
805
+ tenantId: "tenant-abc",
806
+ team: freshTeam,
807
+ });
808
+
809
+ const savedMiscData: MicrosoftTeamsMiscData = getUpdateArgs(updateSpy, 0)
810
+ .data.miscData;
811
+ expect(Object.keys(savedMiscData.installedTeams || {})).toHaveLength(1);
812
+ expect(savedMiscData.installedTeams?.[freshTeam.id]?.name).toBe("New name");
813
+ expect(savedMiscData.installedTeams?.[freshTeam.id]?.serviceUrl).toBe(
814
+ GCC_SERVICE_URL,
815
+ );
816
+ });
817
+
818
+ test("creates installedTeams when miscData is undefined", async () => {
819
+ mockFindBy([buildProjectAuthRow({ miscData: undefined })]);
820
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
821
+
822
+ const team: MicrosoftTeamsInstalledTeam = buildInstalledTeam();
823
+ await MicrosoftTeamsUtil.saveTeamToProjectAuthTokens({
824
+ tenantId: "tenant-abc",
825
+ team: team,
826
+ });
827
+
828
+ expect(updateSpy).toHaveBeenCalledTimes(1);
829
+ expect(getUpdateArgs(updateSpy, 0).data.miscData.installedTeams).toEqual({
830
+ [team.id]: team,
831
+ });
832
+ });
833
+
834
+ test("creates installedTeams when the map is present but empty", async () => {
835
+ mockFindBy([
836
+ buildProjectAuthRow({
837
+ miscData: baseMiscData({ installedTeams: {} }),
838
+ }),
839
+ ]);
840
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
841
+
842
+ const team: MicrosoftTeamsInstalledTeam = buildInstalledTeam();
843
+ await MicrosoftTeamsUtil.saveTeamToProjectAuthTokens({
844
+ tenantId: "tenant-abc",
845
+ team: team,
846
+ });
847
+
848
+ expect(getUpdateArgs(updateSpy, 0).data.miscData.installedTeams).toEqual({
849
+ [team.id]: team,
850
+ });
851
+ });
852
+
853
+ test("no matching project auth rows -> no updates", async () => {
854
+ mockFindBy([]);
855
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
856
+
857
+ await MicrosoftTeamsUtil.saveTeamToProjectAuthTokens({
858
+ tenantId: "tenant-abc",
859
+ team: buildInstalledTeam(),
860
+ });
861
+
862
+ expect(updateSpy).not.toHaveBeenCalled();
863
+ });
864
+
865
+ test("does not mutate the row's own miscData object", async () => {
866
+ const originalMiscData: MicrosoftTeamsMiscData = baseMiscData({
867
+ installedTeams: {},
868
+ });
869
+ mockFindBy([buildProjectAuthRow({ miscData: originalMiscData })]);
870
+ mockUpdateOneById();
871
+
872
+ await MicrosoftTeamsUtil.saveTeamToProjectAuthTokens({
873
+ tenantId: "tenant-abc",
874
+ team: buildInstalledTeam(),
875
+ });
876
+
877
+ expect(originalMiscData.installedTeams).toEqual({});
878
+ });
879
+ });
880
+
881
+ describe("MicrosoftTeamsUtil.removeTeamFromProjectAuthTokens", () => {
882
+ test("queries project auth rows by MicrosoftTeams workspace type and tenant id", async () => {
883
+ const findBySpy: jest.SpyInstance = mockFindBy([]);
884
+
885
+ await MicrosoftTeamsUtil.removeTeamFromProjectAuthTokens({
886
+ tenantId: "tenant-abc",
887
+ teamId: TEAM_ID,
888
+ });
889
+
890
+ expect(findBySpy).toHaveBeenCalledWith(
891
+ expect.objectContaining({
892
+ query: {
893
+ workspaceType: WorkspaceType.MicrosoftTeams,
894
+ workspaceProjectId: "tenant-abc",
895
+ },
896
+ limit: LIMIT_MAX,
897
+ skip: 0,
898
+ props: {
899
+ isRoot: true,
900
+ },
901
+ }),
902
+ );
903
+ });
904
+
905
+ test("removes only the target team and keeps the others", async () => {
906
+ const teamToRemove: MicrosoftTeamsInstalledTeam = buildInstalledTeam({
907
+ id: "19:remove@thread.tacv2",
908
+ name: "Removed",
909
+ });
910
+ const teamToKeep: MicrosoftTeamsInstalledTeam = buildInstalledTeam({
911
+ id: "19:keep@thread.tacv2",
912
+ name: "Kept",
913
+ });
914
+ mockFindBy([
915
+ buildProjectAuthRow({
916
+ miscData: baseMiscData({
917
+ installedTeams: {
918
+ [teamToRemove.id]: teamToRemove,
919
+ [teamToKeep.id]: teamToKeep,
920
+ },
921
+ }),
922
+ }),
923
+ ]);
924
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
925
+
926
+ await MicrosoftTeamsUtil.removeTeamFromProjectAuthTokens({
927
+ tenantId: "tenant-abc",
928
+ teamId: teamToRemove.id,
929
+ });
930
+
931
+ expect(updateSpy).toHaveBeenCalledTimes(1);
932
+ expect(getUpdateArgs(updateSpy, 0).data.miscData.installedTeams).toEqual({
933
+ [teamToKeep.id]: teamToKeep,
934
+ });
935
+ });
936
+
937
+ test("removal preserves the rest of miscData, including availableChats", async () => {
938
+ const team: MicrosoftTeamsInstalledTeam = buildInstalledTeam();
939
+ const chat: MicrosoftTeamsChat = buildChat();
940
+ mockFindBy([
941
+ buildProjectAuthRow({
942
+ miscData: baseMiscData({
943
+ appAccessToken: "token-abc",
944
+ availableChats: { [chat.id]: chat },
945
+ installedTeams: { [team.id]: team },
946
+ }),
947
+ }),
948
+ ]);
949
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
950
+
951
+ await MicrosoftTeamsUtil.removeTeamFromProjectAuthTokens({
952
+ tenantId: "tenant-abc",
953
+ teamId: team.id,
954
+ });
955
+
956
+ const savedMiscData: MicrosoftTeamsMiscData = getUpdateArgs(updateSpy, 0)
957
+ .data.miscData;
958
+ expect(savedMiscData.installedTeams).toEqual({});
959
+ expect(savedMiscData.availableChats).toEqual({ [chat.id]: chat });
960
+ expect(savedMiscData.appAccessToken).toBe("token-abc");
961
+ expect(savedMiscData.botId).toBe("bot-1");
962
+ });
963
+
964
+ test("does not call updateOneById when the row does not have that team", async () => {
965
+ const otherTeam: MicrosoftTeamsInstalledTeam = buildInstalledTeam({
966
+ id: "19:other@thread.tacv2",
967
+ });
968
+ mockFindBy([
969
+ buildProjectAuthRow({
970
+ miscData: baseMiscData({
971
+ installedTeams: { [otherTeam.id]: otherTeam },
972
+ }),
973
+ }),
974
+ ]);
975
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
976
+
977
+ await MicrosoftTeamsUtil.removeTeamFromProjectAuthTokens({
978
+ tenantId: "tenant-abc",
979
+ teamId: TEAM_ID,
980
+ });
981
+
982
+ expect(updateSpy).not.toHaveBeenCalled();
983
+ });
984
+
985
+ test("rows with no installedTeams at all are skipped without an update", async () => {
986
+ mockFindBy([
987
+ buildProjectAuthRow({ miscData: baseMiscData() }),
988
+ buildProjectAuthRow({ miscData: undefined }),
989
+ ]);
990
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
991
+
992
+ await MicrosoftTeamsUtil.removeTeamFromProjectAuthTokens({
993
+ tenantId: "tenant-abc",
994
+ teamId: TEAM_ID,
995
+ });
996
+
997
+ expect(updateSpy).not.toHaveBeenCalled();
998
+ });
999
+
1000
+ test("removes the team from every matching row, keeping each row's OWN remaining data (multi-row removal)", async () => {
1001
+ const team: MicrosoftTeamsInstalledTeam = buildInstalledTeam();
1002
+ /*
1003
+ * Each row keeps a DIFFERENT second install alongside the shared target, so
1004
+ * a removal that computes one miscData and writes it to every row cannot
1005
+ * survive: row two would be handed row one's surviving team.
1006
+ */
1007
+ const fixtures: Array<DistinctRowFixture> = [
1008
+ buildDistinctRowFixture("remove-row-one"),
1009
+ buildDistinctRowFixture("remove-row-two"),
1010
+ buildDistinctRowFixture("remove-row-three"),
1011
+ ];
1012
+ mockFindBy(buildRowsFromFixtures({ fixtures: fixtures, sharedTeam: team }));
1013
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
1014
+
1015
+ await MicrosoftTeamsUtil.removeTeamFromProjectAuthTokens({
1016
+ tenantId: "tenant-abc",
1017
+ teamId: team.id,
1018
+ });
1019
+
1020
+ expect(updateSpy).toHaveBeenCalledTimes(3);
1021
+ expect(getUpdatedIds(updateSpy)).toEqual(fixtureIds(fixtures));
1022
+
1023
+ const savedObjects: Set<MicrosoftTeamsMiscData> =
1024
+ new Set<MicrosoftTeamsMiscData>();
1025
+
1026
+ for (const fixture of fixtures) {
1027
+ const savedMiscData: MicrosoftTeamsMiscData = getUpdateArgsForId(
1028
+ updateSpy,
1029
+ fixture.id,
1030
+ ).data.miscData;
1031
+ savedObjects.add(savedMiscData);
1032
+
1033
+ // Only the target goes; this row's other install stays, siblings' do not.
1034
+ expect(savedMiscData.installedTeams).toEqual({
1035
+ [fixture.existingTeam.id]: fixture.existingTeam,
1036
+ });
1037
+ expect(savedMiscData.availableChats).toEqual({
1038
+ [fixture.chat.id]: fixture.chat,
1039
+ });
1040
+ expect(savedMiscData.tenantId).toBe(fixture.tenantId);
1041
+ expect(savedMiscData.teamName).toBe(fixture.teamName);
1042
+ expect(savedMiscData.botId).toBe(fixture.botId);
1043
+ }
1044
+
1045
+ expect(savedObjects.size).toBe(3);
1046
+ });
1047
+
1048
+ test("only the rows that contain the team are updated, and that update carries that row's own miscData", async () => {
1049
+ const team: MicrosoftTeamsInstalledTeam = buildInstalledTeam();
1050
+ const noInstalledTeamsRow: DistinctRowFixture =
1051
+ buildDistinctRowFixture("no-map-row");
1052
+ const rowWithTeam: DistinctRowFixture =
1053
+ buildDistinctRowFixture("has-team-row");
1054
+ const emptyMapRow: DistinctRowFixture =
1055
+ buildDistinctRowFixture("empty-map-row");
1056
+ mockFindBy([
1057
+ buildRowFromFixture({ fixture: noInstalledTeamsRow }),
1058
+ buildRowFromFixture({
1059
+ fixture: rowWithTeam,
1060
+ installedTeams: {
1061
+ [rowWithTeam.existingTeam.id]: rowWithTeam.existingTeam,
1062
+ [team.id]: team,
1063
+ },
1064
+ }),
1065
+ buildRowFromFixture({ fixture: emptyMapRow, installedTeams: {} }),
1066
+ ]);
1067
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
1068
+
1069
+ await MicrosoftTeamsUtil.removeTeamFromProjectAuthTokens({
1070
+ tenantId: "tenant-abc",
1071
+ teamId: team.id,
1072
+ });
1073
+
1074
+ expect(updateSpy).toHaveBeenCalledTimes(1);
1075
+ expect(String(getUpdateArgs(updateSpy, 0).id)).toBe(
1076
+ rowWithTeam.id.toString(),
1077
+ );
1078
+ /*
1079
+ * The one update must carry the matching row's data — not data assembled
1080
+ * from a skipped neighbour that happened to be iterated first.
1081
+ */
1082
+ const savedMiscData: MicrosoftTeamsMiscData = getUpdateArgs(updateSpy, 0)
1083
+ .data.miscData;
1084
+ expect(savedMiscData.installedTeams).toEqual({
1085
+ [rowWithTeam.existingTeam.id]: rowWithTeam.existingTeam,
1086
+ });
1087
+ expect(savedMiscData.availableChats).toEqual({
1088
+ [rowWithTeam.chat.id]: rowWithTeam.chat,
1089
+ });
1090
+ expect(savedMiscData.tenantId).toBe(rowWithTeam.tenantId);
1091
+ expect(savedMiscData.botId).toBe(rowWithTeam.botId);
1092
+ });
1093
+
1094
+ test("team ids are matched case-sensitively (a different casing is a different team)", async () => {
1095
+ const team: MicrosoftTeamsInstalledTeam = buildInstalledTeam({
1096
+ id: "19:Team-AAA@thread.tacv2",
1097
+ });
1098
+ mockFindBy([
1099
+ buildProjectAuthRow({
1100
+ miscData: baseMiscData({ installedTeams: { [team.id]: team } }),
1101
+ }),
1102
+ ]);
1103
+ const updateSpy: jest.SpyInstance = mockUpdateOneById();
1104
+
1105
+ await MicrosoftTeamsUtil.removeTeamFromProjectAuthTokens({
1106
+ tenantId: "tenant-abc",
1107
+ teamId: "19:team-aaa@thread.tacv2",
1108
+ });
1109
+
1110
+ expect(updateSpy).not.toHaveBeenCalled();
1111
+ });
1112
+
1113
+ test("does not mutate the row's own installedTeams object", async () => {
1114
+ const team: MicrosoftTeamsInstalledTeam = buildInstalledTeam();
1115
+ const originalMiscData: MicrosoftTeamsMiscData = baseMiscData({
1116
+ installedTeams: { [team.id]: team },
1117
+ });
1118
+ mockFindBy([buildProjectAuthRow({ miscData: originalMiscData })]);
1119
+ mockUpdateOneById();
1120
+
1121
+ await MicrosoftTeamsUtil.removeTeamFromProjectAuthTokens({
1122
+ tenantId: "tenant-abc",
1123
+ teamId: team.id,
1124
+ });
1125
+
1126
+ expect(originalMiscData.installedTeams).toEqual({ [team.id]: team });
1127
+ });
1128
+ });
1129
+
1130
+ describe("MicrosoftTeamsUtil.getInstalledTeamsForProject", () => {
1131
+ test("returns empty object when there is no project auth", async () => {
1132
+ jest
1133
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
1134
+ .mockResolvedValue(null);
1135
+
1136
+ const teams: Record<string, MicrosoftTeamsInstalledTeam> =
1137
+ await MicrosoftTeamsUtil.getInstalledTeamsForProject({
1138
+ projectId: ObjectID.generate(),
1139
+ });
1140
+
1141
+ expect(teams).toEqual({});
1142
+ });
1143
+
1144
+ test("returns empty object when project auth has no miscData", async () => {
1145
+ jest
1146
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
1147
+ .mockResolvedValue(buildProjectAuthRow({ miscData: undefined }));
1148
+
1149
+ const teams: Record<string, MicrosoftTeamsInstalledTeam> =
1150
+ await MicrosoftTeamsUtil.getInstalledTeamsForProject({
1151
+ projectId: ObjectID.generate(),
1152
+ });
1153
+
1154
+ expect(teams).toEqual({});
1155
+ });
1156
+
1157
+ test("returns empty object when miscData has no installedTeams", async () => {
1158
+ jest
1159
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
1160
+ .mockResolvedValue(buildProjectAuthRow({ miscData: baseMiscData() }));
1161
+
1162
+ const teams: Record<string, MicrosoftTeamsInstalledTeam> =
1163
+ await MicrosoftTeamsUtil.getInstalledTeamsForProject({
1164
+ projectId: ObjectID.generate(),
1165
+ });
1166
+
1167
+ expect(teams).toEqual({});
1168
+ });
1169
+
1170
+ test("returns installedTeams when present, keyed by team id", async () => {
1171
+ const teamOne: MicrosoftTeamsInstalledTeam = buildInstalledTeam();
1172
+ const teamTwo: MicrosoftTeamsInstalledTeam = buildInstalledTeam({
1173
+ id: "19:second@thread.tacv2",
1174
+ name: "Support",
1175
+ serviceUrl: GCC_SERVICE_URL,
1176
+ });
1177
+ jest
1178
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
1179
+ .mockResolvedValue(
1180
+ buildProjectAuthRow({
1181
+ miscData: baseMiscData({
1182
+ installedTeams: { [teamOne.id]: teamOne, [teamTwo.id]: teamTwo },
1183
+ }),
1184
+ }),
1185
+ );
1186
+
1187
+ const teams: Record<string, MicrosoftTeamsInstalledTeam> =
1188
+ await MicrosoftTeamsUtil.getInstalledTeamsForProject({
1189
+ projectId: ObjectID.generate(),
1190
+ });
1191
+
1192
+ expect(teams).toEqual({ [teamOne.id]: teamOne, [teamTwo.id]: teamTwo });
1193
+ expect(teams[teamTwo.id]?.serviceUrl).toBe(GCC_SERVICE_URL);
1194
+ });
1195
+
1196
+ test("does not confuse availableTeams (what Graph can see) with installedTeams (what we can post to)", async () => {
1197
+ jest
1198
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
1199
+ .mockResolvedValue(
1200
+ buildProjectAuthRow({
1201
+ miscData: baseMiscData({
1202
+ availableTeams: {
1203
+ "19:listed-only@thread.tacv2": {
1204
+ id: "19:listed-only@thread.tacv2",
1205
+ name: "Listed but not installed",
1206
+ },
1207
+ } as Record<string, MicrosoftTeamsTeam>,
1208
+ }),
1209
+ }),
1210
+ );
1211
+
1212
+ const teams: Record<string, MicrosoftTeamsInstalledTeam> =
1213
+ await MicrosoftTeamsUtil.getInstalledTeamsForProject({
1214
+ projectId: ObjectID.generate(),
1215
+ });
1216
+
1217
+ expect(teams).toEqual({});
1218
+ });
1219
+
1220
+ test("looks up project auth by projectId and MicrosoftTeams workspace type", async () => {
1221
+ const getProjectAuthSpy: jest.SpyInstance = jest
1222
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
1223
+ .mockResolvedValue(null);
1224
+ const projectId: ObjectID = ObjectID.generate();
1225
+
1226
+ await MicrosoftTeamsUtil.getInstalledTeamsForProject({
1227
+ projectId: projectId,
1228
+ });
1229
+
1230
+ expect(getProjectAuthSpy).toHaveBeenCalledWith({
1231
+ projectId: projectId,
1232
+ workspaceType: WorkspaceType.MicrosoftTeams,
1233
+ });
1234
+ });
1235
+ });
1236
+
1237
+ interface WiringSpies {
1238
+ captureChatSpy: jest.SpyInstance;
1239
+ captureTeamSpy: jest.SpyInstance;
1240
+ removeChatSpy: jest.SpyInstance;
1241
+ removeTeamSpy: jest.SpyInstance;
1242
+ }
1243
+
1244
+ /*
1245
+ * Route-level spies: the wiring tests assert WHICH capture ran, not what it
1246
+ * did internally, so both the chat and the team capture are stubbed out.
1247
+ */
1248
+ function installWiringSpies(): WiringSpies {
1249
+ return {
1250
+ captureChatSpy: jest
1251
+ .spyOn(MicrosoftTeamsUtil as any, "captureChatFromBotActivity")
1252
+ .mockResolvedValue(undefined as never),
1253
+ captureTeamSpy: jest
1254
+ .spyOn(MicrosoftTeamsUtil, "captureTeamFromBotActivity")
1255
+ .mockResolvedValue(undefined as never),
1256
+ removeChatSpy: jest
1257
+ .spyOn(MicrosoftTeamsUtil as any, "removeChatFromBotActivity")
1258
+ .mockResolvedValue(undefined as never),
1259
+ removeTeamSpy: jest
1260
+ .spyOn(MicrosoftTeamsUtil, "removeTeamFromBotActivity")
1261
+ .mockResolvedValue(undefined as never),
1262
+ };
1263
+ }
1264
+
1265
+ describe("MicrosoftTeamsUtil.handleConversationUpdateActivity - team install wiring", () => {
1266
+ function installWelcomeSpy(): jest.SpyInstance {
1267
+ return jest
1268
+ .spyOn(MicrosoftTeamsUtil as any, "sendWelcomeAdaptiveCard")
1269
+ .mockResolvedValue(undefined as never);
1270
+ }
1271
+
1272
+ test("bot added to a team calls BOTH the chat capture and the team capture", async () => {
1273
+ installWelcomeSpy();
1274
+ const spies: WiringSpies = installWiringSpies();
1275
+ const activity: JSONObject = teamActivity({
1276
+ membersAdded: [{ id: BOT_RECIPIENT_ID }],
1277
+ });
1278
+ const turnContext: TurnContext = buildTurnContext();
1279
+
1280
+ await MicrosoftTeamsUtil.handleConversationUpdateActivity({
1281
+ activity: activity,
1282
+ turnContext: turnContext,
1283
+ });
1284
+
1285
+ expect(spies.captureChatSpy).toHaveBeenCalledTimes(1);
1286
+ expect(spies.captureTeamSpy).toHaveBeenCalledTimes(1);
1287
+ expect(spies.captureTeamSpy).toHaveBeenCalledWith({
1288
+ activity: activity,
1289
+ turnContext: turnContext,
1290
+ });
1291
+ expect(spies.removeTeamSpy).not.toHaveBeenCalled();
1292
+ });
1293
+
1294
+ test("bot removed from a team calls BOTH the chat removal and the team removal", async () => {
1295
+ installWelcomeSpy();
1296
+ const spies: WiringSpies = installWiringSpies();
1297
+ const activity: JSONObject = teamActivity({
1298
+ membersRemoved: [{ id: BOT_RECIPIENT_ID }],
1299
+ });
1300
+
1301
+ await MicrosoftTeamsUtil.handleConversationUpdateActivity({
1302
+ activity: activity,
1303
+ turnContext: buildTurnContext(),
1304
+ });
1305
+
1306
+ expect(spies.removeChatSpy).toHaveBeenCalledTimes(1);
1307
+ expect(spies.removeTeamSpy).toHaveBeenCalledTimes(1);
1308
+ expect(spies.removeTeamSpy).toHaveBeenCalledWith({ activity: activity });
1309
+ expect(spies.captureTeamSpy).not.toHaveBeenCalled();
1310
+ });
1311
+
1312
+ test("a non-bot member added or removed touches neither capture", async () => {
1313
+ installWelcomeSpy();
1314
+ const spies: WiringSpies = installWiringSpies();
1315
+
1316
+ await MicrosoftTeamsUtil.handleConversationUpdateActivity({
1317
+ activity: teamActivity({
1318
+ membersAdded: [{ id: "some-human-user" }],
1319
+ membersRemoved: [{ id: "another-human-user" }],
1320
+ }),
1321
+ turnContext: buildTurnContext(),
1322
+ });
1323
+
1324
+ expect(spies.captureChatSpy).not.toHaveBeenCalled();
1325
+ expect(spies.captureTeamSpy).not.toHaveBeenCalled();
1326
+ expect(spies.removeChatSpy).not.toHaveBeenCalled();
1327
+ expect(spies.removeTeamSpy).not.toHaveBeenCalled();
1328
+ });
1329
+
1330
+ test("a conversationUpdate with no member lists at all is a no-op", async () => {
1331
+ installWelcomeSpy();
1332
+ const spies: WiringSpies = installWiringSpies();
1333
+
1334
+ await MicrosoftTeamsUtil.handleConversationUpdateActivity({
1335
+ activity: teamActivity(),
1336
+ turnContext: buildTurnContext(),
1337
+ });
1338
+
1339
+ expect(spies.captureTeamSpy).not.toHaveBeenCalled();
1340
+ expect(spies.removeTeamSpy).not.toHaveBeenCalled();
1341
+ });
1342
+
1343
+ test("bot added to a personal chat still runs the team capture (it self-filters on channelData.team)", async () => {
1344
+ installWelcomeSpy();
1345
+ const spies: WiringSpies = installWiringSpies();
1346
+
1347
+ await MicrosoftTeamsUtil.handleConversationUpdateActivity({
1348
+ activity: {
1349
+ membersAdded: [{ id: BOT_RECIPIENT_ID }],
1350
+ conversation: { conversationType: "personal", id: "a:1personal" },
1351
+ channelData: { tenant: { id: TENANT_ID } },
1352
+ serviceUrl: ACTIVITY_SERVICE_URL,
1353
+ },
1354
+ turnContext: buildTurnContext(),
1355
+ });
1356
+
1357
+ expect(spies.captureChatSpy).toHaveBeenCalledTimes(1);
1358
+ expect(spies.captureTeamSpy).toHaveBeenCalledTimes(1);
1359
+ });
1360
+ });
1361
+
1362
+ describe("MicrosoftTeamsUtil.handleInstallationUpdateActivity - team install wiring", () => {
1363
+ test("action 'add' captures BOTH the chat and the team", async () => {
1364
+ const spies: WiringSpies = installWiringSpies();
1365
+ const activity: JSONObject = teamActivity({ action: "add" });
1366
+ const turnContext: TurnContext = buildTurnContext();
1367
+
1368
+ await MicrosoftTeamsUtil.handleInstallationUpdateActivity({
1369
+ activity: activity,
1370
+ turnContext: turnContext,
1371
+ });
1372
+
1373
+ expect(spies.captureChatSpy).toHaveBeenCalledTimes(1);
1374
+ expect(spies.captureTeamSpy).toHaveBeenCalledTimes(1);
1375
+ expect(spies.captureTeamSpy).toHaveBeenCalledWith({
1376
+ activity: activity,
1377
+ turnContext: turnContext,
1378
+ });
1379
+ });
1380
+
1381
+ test("action 'add-upgrade' captures BOTH (manifest upgrade that re-adds the bot)", async () => {
1382
+ const spies: WiringSpies = installWiringSpies();
1383
+
1384
+ await MicrosoftTeamsUtil.handleInstallationUpdateActivity({
1385
+ activity: teamActivity({ action: "add-upgrade" }),
1386
+ turnContext: buildTurnContext(),
1387
+ });
1388
+
1389
+ expect(spies.captureChatSpy).toHaveBeenCalledTimes(1);
1390
+ expect(spies.captureTeamSpy).toHaveBeenCalledTimes(1);
1391
+ expect(spies.removeTeamSpy).not.toHaveBeenCalled();
1392
+ });
1393
+
1394
+ test("action 'remove' removes BOTH the chat and the team", async () => {
1395
+ const spies: WiringSpies = installWiringSpies();
1396
+ const activity: JSONObject = teamActivity({ action: "remove" });
1397
+
1398
+ await MicrosoftTeamsUtil.handleInstallationUpdateActivity({
1399
+ activity: activity,
1400
+ turnContext: buildTurnContext(),
1401
+ });
1402
+
1403
+ expect(spies.removeChatSpy).toHaveBeenCalledTimes(1);
1404
+ expect(spies.removeTeamSpy).toHaveBeenCalledTimes(1);
1405
+ expect(spies.removeTeamSpy).toHaveBeenCalledWith({ activity: activity });
1406
+ expect(spies.captureTeamSpy).not.toHaveBeenCalled();
1407
+ });
1408
+
1409
+ test("action 'remove-upgrade' removes BOTH", async () => {
1410
+ const spies: WiringSpies = installWiringSpies();
1411
+
1412
+ await MicrosoftTeamsUtil.handleInstallationUpdateActivity({
1413
+ activity: teamActivity({ action: "remove-upgrade" }),
1414
+ turnContext: buildTurnContext(),
1415
+ });
1416
+
1417
+ expect(spies.removeChatSpy).toHaveBeenCalledTimes(1);
1418
+ expect(spies.removeTeamSpy).toHaveBeenCalledTimes(1);
1419
+ });
1420
+
1421
+ test("an unknown action does neither", async () => {
1422
+ const spies: WiringSpies = installWiringSpies();
1423
+
1424
+ await MicrosoftTeamsUtil.handleInstallationUpdateActivity({
1425
+ activity: teamActivity({ action: "something-else" }),
1426
+ turnContext: buildTurnContext(),
1427
+ });
1428
+
1429
+ expect(spies.captureTeamSpy).not.toHaveBeenCalled();
1430
+ expect(spies.removeTeamSpy).not.toHaveBeenCalled();
1431
+ expect(spies.captureChatSpy).not.toHaveBeenCalled();
1432
+ expect(spies.removeChatSpy).not.toHaveBeenCalled();
1433
+ });
1434
+
1435
+ test("a missing action does neither", async () => {
1436
+ const spies: WiringSpies = installWiringSpies();
1437
+
1438
+ await MicrosoftTeamsUtil.handleInstallationUpdateActivity({
1439
+ activity: teamActivity(),
1440
+ turnContext: buildTurnContext(),
1441
+ });
1442
+
1443
+ expect(spies.captureTeamSpy).not.toHaveBeenCalled();
1444
+ expect(spies.removeTeamSpy).not.toHaveBeenCalled();
1445
+ });
1446
+
1447
+ test("the action match is case-sensitive ('Add' is not 'add')", async () => {
1448
+ const spies: WiringSpies = installWiringSpies();
1449
+
1450
+ await MicrosoftTeamsUtil.handleInstallationUpdateActivity({
1451
+ activity: teamActivity({ action: "Add" }),
1452
+ turnContext: buildTurnContext(),
1453
+ });
1454
+
1455
+ expect(spies.captureTeamSpy).not.toHaveBeenCalled();
1456
+ expect(spies.captureChatSpy).not.toHaveBeenCalled();
1457
+ });
1458
+ });
1459
+
1460
+ describe("MicrosoftTeamsUtil.handleBotMessageActivity - team install backfill wiring", () => {
1461
+ /*
1462
+ * Teams the app was added to before install capture shipped fire no new
1463
+ * install event (a manifest version bump alone sends no installationUpdate
1464
+ * per Microsoft docs), so an inbound channel message is the only recovery
1465
+ * path. These activities carry no @mention, so the handler exits right
1466
+ * after the backfill step.
1467
+ */
1468
+ function messageActivity(conversation: JSONObject): JSONObject {
1469
+ return {
1470
+ type: "message",
1471
+ text: "hello there",
1472
+ from: { id: "user-1", name: "Alice" },
1473
+ recipient: { id: BOT_RECIPIENT_ID },
1474
+ conversation: conversation,
1475
+ channelData: {
1476
+ team: { id: TEAM_ID, name: "Engineering" },
1477
+ tenant: { id: TENANT_ID },
1478
+ },
1479
+ serviceUrl: ACTIVITY_SERVICE_URL,
1480
+ entities: [],
1481
+ };
1482
+ }
1483
+
1484
+ test("a CHANNEL message backfills the team install and does not run the chat capture", async () => {
1485
+ const spies: WiringSpies = installWiringSpies();
1486
+ const activity: JSONObject = messageActivity({
1487
+ conversationType: "channel",
1488
+ id: "19:general@thread.tacv2",
1489
+ });
1490
+ const turnContext: TurnContext = buildTurnContext();
1491
+
1492
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
1493
+ activity: activity,
1494
+ turnContext: turnContext,
1495
+ });
1496
+
1497
+ expect(spies.captureTeamSpy).toHaveBeenCalledTimes(1);
1498
+ expect(spies.captureTeamSpy).toHaveBeenCalledWith({
1499
+ activity: activity,
1500
+ turnContext: turnContext,
1501
+ });
1502
+ expect(spies.captureChatSpy).not.toHaveBeenCalled();
1503
+ });
1504
+
1505
+ test("a groupChat message runs the chat capture and NOT the team capture", async () => {
1506
+ const spies: WiringSpies = installWiringSpies();
1507
+
1508
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
1509
+ activity: messageActivity({
1510
+ conversationType: "groupChat",
1511
+ id: "19:groupchat@thread.v2",
1512
+ }),
1513
+ turnContext: buildTurnContext(),
1514
+ });
1515
+
1516
+ expect(spies.captureChatSpy).toHaveBeenCalledTimes(1);
1517
+ expect(spies.captureTeamSpy).not.toHaveBeenCalled();
1518
+ });
1519
+
1520
+ test("a personal message runs the chat capture and NOT the team capture", async () => {
1521
+ const spies: WiringSpies = installWiringSpies();
1522
+ /*
1523
+ * A personal message is a direct message, so the handler runs on past the
1524
+ * backfill into tenant resolution. Stop it there — this test is only about
1525
+ * which capture ran.
1526
+ */
1527
+ jest
1528
+ .spyOn(MicrosoftTeamsUtil, "resolveProjectByTenantId")
1529
+ .mockResolvedValue({
1530
+ projectAuth: null,
1531
+ isAmbiguous: false,
1532
+ candidateProjectIds: [],
1533
+ } as MicrosoftTeamsTenantResolution);
1534
+
1535
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
1536
+ activity: messageActivity({
1537
+ conversationType: "personal",
1538
+ id: "a:1personal",
1539
+ }),
1540
+ turnContext: buildTurnContext(),
1541
+ });
1542
+
1543
+ expect(spies.captureChatSpy).toHaveBeenCalledTimes(1);
1544
+ expect(spies.captureTeamSpy).not.toHaveBeenCalled();
1545
+ });
1546
+
1547
+ test("a conversation with no conversationType triggers neither capture", async () => {
1548
+ const spies: WiringSpies = installWiringSpies();
1549
+
1550
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
1551
+ activity: messageActivity({ id: "19:unknown@thread.tacv2" }),
1552
+ turnContext: buildTurnContext(),
1553
+ });
1554
+
1555
+ expect(spies.captureChatSpy).not.toHaveBeenCalled();
1556
+ expect(spies.captureTeamSpy).not.toHaveBeenCalled();
1557
+ });
1558
+
1559
+ test("conversationType matching is case-sensitive ('Channel' is not 'channel')", async () => {
1560
+ const spies: WiringSpies = installWiringSpies();
1561
+
1562
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
1563
+ activity: messageActivity({
1564
+ conversationType: "Channel",
1565
+ id: "19:general@thread.tacv2",
1566
+ }),
1567
+ turnContext: buildTurnContext(),
1568
+ });
1569
+
1570
+ expect(spies.captureTeamSpy).not.toHaveBeenCalled();
1571
+ });
1572
+
1573
+ test("the bot's own channel message triggers no backfill (loop guard runs first)", async () => {
1574
+ const spies: WiringSpies = installWiringSpies();
1575
+ const activity: JSONObject = messageActivity({
1576
+ conversationType: "channel",
1577
+ id: "19:general@thread.tacv2",
1578
+ });
1579
+ (activity["from"] as JSONObject)["id"] = BOT_RECIPIENT_ID;
1580
+
1581
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
1582
+ activity: activity,
1583
+ turnContext: buildTurnContext(),
1584
+ });
1585
+
1586
+ expect(spies.captureTeamSpy).not.toHaveBeenCalled();
1587
+ expect(spies.captureChatSpy).not.toHaveBeenCalled();
1588
+ });
1589
+
1590
+ test("a message from a member flagged with the bot role triggers no backfill", async () => {
1591
+ const spies: WiringSpies = installWiringSpies();
1592
+ const activity: JSONObject = messageActivity({
1593
+ conversationType: "channel",
1594
+ id: "19:general@thread.tacv2",
1595
+ });
1596
+ (activity["from"] as JSONObject)["role"] = "bot";
1597
+
1598
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
1599
+ activity: activity,
1600
+ turnContext: buildTurnContext(),
1601
+ });
1602
+
1603
+ expect(spies.captureTeamSpy).not.toHaveBeenCalled();
1604
+ });
1605
+ });