@oneuptime/common 14.0.9 → 14.0.10

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 (383) hide show
  1. package/Models/DatabaseModels/AutoRemediationSuggestion.ts +60 -0
  2. package/Models/DatabaseModels/CephCluster.ts +213 -0
  3. package/Models/DatabaseModels/DatabaseServer.ts +213 -0
  4. package/Models/DatabaseModels/DockerHost.ts +213 -0
  5. package/Models/DatabaseModels/DockerSwarmCluster.ts +213 -0
  6. package/Models/DatabaseModels/GlobalConfig.ts +1 -1
  7. package/Models/DatabaseModels/Host.ts +213 -0
  8. package/Models/DatabaseModels/Index.ts +2 -0
  9. package/Models/DatabaseModels/PodmanHost.ts +213 -0
  10. package/Models/DatabaseModels/ProxmoxCluster.ts +213 -0
  11. package/Models/DatabaseModels/ResourceAiAgent.ts +419 -0
  12. package/Models/DatabaseModels/RunnerJob.ts +144 -0
  13. package/Models/DatabaseModels/VMwareVCenter.ts +213 -0
  14. package/Server/API/AutoRemediationAPI.ts +392 -1
  15. package/Server/API/ResourceAiAccessAPI.ts +1669 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.ts +423 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  18. package/Server/Infrastructure/Semaphore.ts +22 -0
  19. package/Server/Middleware/TelemetryIngest.ts +15 -5
  20. package/Server/Services/AnalyticsDatabaseService.ts +45 -1
  21. package/Server/Services/AutoRemediationRuleEngineService.ts +940 -112
  22. package/Server/Services/CephClusterService.ts +109 -1
  23. package/Server/Services/DatabaseServerService.ts +116 -1
  24. package/Server/Services/DockerHostService.ts +109 -1
  25. package/Server/Services/DockerSwarmClusterService.ts +113 -1
  26. package/Server/Services/HostService.ts +126 -4
  27. package/Server/Services/MetricRecordingRuleService.ts +47 -0
  28. package/Server/Services/PodmanHostService.ts +109 -1
  29. package/Server/Services/ProxmoxClusterService.ts +110 -1
  30. package/Server/Services/ResourceAiAccessService.ts +1439 -0
  31. package/Server/Services/ResourceAiAgentJobService.ts +202 -0
  32. package/Server/Services/ResourceAiAgentService.ts +2432 -0
  33. package/Server/Services/RunnerJobService.ts +611 -4
  34. package/Server/Services/TelemetryUsageBillingService.ts +28 -0
  35. package/Server/Services/TraceRecordingRuleService.ts +33 -0
  36. package/Server/Services/VMwareVCenterService.ts +110 -1
  37. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +1434 -32
  38. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +935 -21
  39. package/Server/Utils/AI/Remediation/RemediationPlanRunner.ts +25 -1
  40. package/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.ts +577 -0
  41. package/Server/Utils/AI/ResourceAccess/ResourceAccessContext.ts +271 -0
  42. package/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.ts +45 -0
  43. package/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.ts +964 -0
  44. package/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.ts +333 -0
  45. package/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.ts +873 -0
  46. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +72 -2
  47. package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +72 -5
  48. package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +72 -5
  49. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +396 -13
  50. package/Server/Utils/AutoRemediation/RemediationVerifier.ts +35 -2
  51. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +9 -1
  52. package/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.ts +920 -0
  53. package/Server/Utils/SessionReplay/SessionReplayUsage.ts +84 -6
  54. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +35 -15
  55. package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +33 -15
  56. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +21 -1
  57. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +298 -224
  58. package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +35 -15
  59. package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +389 -175
  60. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +366 -143
  61. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.ts +163 -0
  62. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.ts +396 -0
  63. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.ts +418 -0
  64. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.ts +152 -0
  65. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.ts +251 -0
  66. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.ts +214 -0
  67. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +93 -0
  68. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +18 -6
  69. package/Tests/App/Dashboard/InvestigationInfrastructureTools.test.tsx +506 -0
  70. package/Tests/App/Dashboard/RemediationSuggestionCardDescription.test.tsx +209 -0
  71. package/Tests/App/Dashboard/RemediationSuggestionCardResource.test.tsx +317 -0
  72. package/Tests/App/Dashboard/ResourceAiAccessSettingsUtil.test.ts +921 -0
  73. package/Tests/App/Dashboard/ResourceAiAgentInstall.test.ts +860 -0
  74. package/Tests/App/Dashboard/ResourceAiAgentPage.test.tsx +1721 -0
  75. package/Tests/App/Dashboard/ResourceAiAgentStatus.test.ts +1002 -0
  76. package/Tests/App/Dashboard/ResourceAiInsightsPage.test.tsx +931 -0
  77. package/Tests/App/Dashboard/ResourceAiNavigation.test.tsx +575 -0
  78. package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +38 -7
  79. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +33 -1
  80. package/Tests/Models/DatabaseModels/ResourceAiAccessColumns.test.ts +738 -0
  81. package/Tests/Models/DatabaseModels/ResourceAiAgentModel.test.ts +842 -0
  82. package/Tests/Server/API/AutoRemediationApproveResourceRound.test.ts +835 -0
  83. package/Tests/Server/API/ResourceAiAccessAPI.test.ts +2730 -0
  84. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +60 -0
  85. package/Tests/Server/Infrastructure/Postgres/AddResourceAiAgentsMigration.test.ts +659 -0
  86. package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +31 -0
  87. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +63 -5
  88. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +103 -5
  89. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +19 -1
  90. package/Tests/Server/Services/AutoRemediationResourceRuleEngine.test.ts +1279 -0
  91. package/Tests/Server/Services/DatabaseServerService.test.ts +55 -0
  92. package/Tests/Server/Services/GroupTelemetryUsageExcludeNames.test.ts +299 -0
  93. package/Tests/Server/Services/HostServiceFindOrCreateMemo.test.ts +44 -0
  94. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +45 -6
  95. package/Tests/Server/Services/RecordingRuleReservedMetricName.test.ts +171 -0
  96. package/Tests/Server/Services/ResourceAiAccessChangeAuthorization.test.ts +256 -0
  97. package/Tests/Server/Services/ResourceAiAccessService.test.ts +1373 -0
  98. package/Tests/Server/Services/ResourceAiAgentJobService.test.ts +375 -0
  99. package/Tests/Server/Services/ResourceAiAgentServiceHelpers.test.ts +1114 -0
  100. package/Tests/Server/Services/ResourceAiAgentServiceLifecycle.test.ts +1050 -0
  101. package/Tests/Server/Services/ResourceAiAgentServiceRegister.test.ts +2251 -0
  102. package/Tests/Server/Services/ResourceAiSettingsCreate.test.ts +373 -0
  103. package/Tests/Server/Services/ResourceAiSettingsPermission.test.ts +1042 -0
  104. package/Tests/Server/Services/ResourceServiceDeleteCleansUpAi.test.ts +319 -0
  105. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +105 -0
  106. package/Tests/Server/Services/RunnerJobEnqueueResourceCommand.test.ts +986 -0
  107. package/Tests/Server/Services/RunnerJobResourceCommandLane.test.ts +211 -0
  108. package/Tests/Server/Services/RunnerJobResourceCommandTimeoutAndRedaction.test.ts +352 -0
  109. package/Tests/Server/Services/TelemetryUsageBillingSloExclusion.test.ts +218 -0
  110. package/Tests/Server/TestingUtils/Services/FakeRunnerJobCount.ts +145 -0
  111. package/Tests/Server/Utils/AI/InvestigationInfrastructureAccessWiring.test.ts +453 -0
  112. package/Tests/Server/Utils/AI/InvestigationInfrastructureReport.test.ts +721 -0
  113. package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +94 -1
  114. package/Tests/Server/Utils/AI/RemediationCommandToolsResource.test.ts +1530 -0
  115. package/Tests/Server/Utils/AI/RemediationExecutionRunnerResourceMode.test.ts +1249 -0
  116. package/Tests/Server/Utils/AI/RemediationPlanRunner.test.ts +117 -0
  117. package/Tests/Server/Utils/AI/RemediationResourceCopyParity.test.ts +463 -0
  118. package/Tests/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.test.ts +722 -0
  119. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAccessContext.test.ts +266 -0
  120. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.test.ts +1220 -0
  121. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.test.ts +569 -0
  122. package/Tests/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.test.ts +830 -0
  123. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorResource.test.ts +717 -0
  124. package/Tests/Server/Utils/AutoRemediation/RemediationVerifierResourceFollowUp.test.ts +338 -0
  125. package/Tests/Server/Utils/Monitor/Criteria/SessionReplayBudgetTemplateCriteria.test.ts +422 -0
  126. package/Tests/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.test.ts +1647 -0
  127. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +143 -1
  128. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +33 -3
  129. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsAccountNotLinked.test.ts +1093 -0
  130. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.test.ts +1049 -0
  131. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.test.ts +1676 -0
  132. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCards.test.ts +2884 -0
  133. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.test.ts +2490 -0
  134. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmit.test.ts +2293 -0
  135. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmitServerTimezone.test.ts +386 -0
  136. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.test.ts +1477 -0
  137. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.test.ts +1967 -0
  138. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsStripHtmlTags.test.ts +92 -0
  139. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsSubmittedFormRemoval.test.ts +1819 -0
  140. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.test.ts +1033 -0
  141. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezoneServerZone.test.ts +610 -0
  142. package/Tests/Server/Utils/Workspace/MicrosoftTeamsBotMessageHandling.test.ts +2610 -0
  143. package/Tests/Server/Utils/Workspace/MicrosoftTeamsCreateCommandsEndToEnd.test.ts +4672 -0
  144. package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +190 -65
  145. package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +10 -4
  146. package/Tests/Types/AutoRemediation/AiRemediationCommandPlanResourceCommand.test.ts +305 -0
  147. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +277 -13
  148. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +113 -0
  149. package/Tests/Types/Monitor/RumAlertTemplates.test.ts +773 -2
  150. package/Tests/Types/ResourceAiAgent/AiResourceType.test.ts +419 -0
  151. package/Tests/Types/ResourceAiAgent/ResourceAiAccess.test.ts +517 -0
  152. package/Tests/Types/Runbook/RunbookStepType.test.ts +147 -8
  153. package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +769 -1
  154. package/Tests/Utils/AiRemediation/Resource/CephCommandPolicy.test.ts +1653 -0
  155. package/Tests/Utils/AiRemediation/Resource/CephOutputRedaction.test.ts +204 -0
  156. package/Tests/Utils/AiRemediation/Resource/DatabaseCommandPolicy.test.ts +1077 -0
  157. package/Tests/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.test.ts +558 -0
  158. package/Tests/Utils/AiRemediation/Resource/DatabaseQueryRedactor.test.ts +834 -0
  159. package/Tests/Utils/AiRemediation/Resource/DockerCliGrammar.test.ts +747 -0
  160. package/Tests/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.test.ts +1259 -0
  161. package/Tests/Utils/AiRemediation/Resource/DockerOutputRedaction.test.ts +386 -0
  162. package/Tests/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.test.ts +782 -0
  163. package/Tests/Utils/AiRemediation/Resource/GovcCommandPolicy.test.ts +2259 -0
  164. package/Tests/Utils/AiRemediation/Resource/HostCommandPolicy.test.ts +2019 -0
  165. package/Tests/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.test.ts +2497 -0
  166. package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicy.test.ts +1305 -0
  167. package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.test.ts +585 -0
  168. package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactor.test.ts +334 -0
  169. package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactorCopyParity.test.ts +98 -0
  170. package/Tests/Utils/AiRemediation/Resource/ResourcePolicyImportClosure.test.ts +391 -0
  171. package/Tests/Utils/AiRemediation/ResourceAiAgentPolicyCopyParity.test.ts +497 -0
  172. package/Tests/Utils/SessionReplay/SessionReplayBudgetMetricType.test.ts +383 -0
  173. package/Types/AI/ResourceAiAccessApi.ts +140 -0
  174. package/Types/AI/ResourceAiAccessPermissions.ts +126 -0
  175. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +183 -2
  176. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +55 -22
  177. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +35 -3
  178. package/Types/Monitor/RumAlertTemplates.ts +351 -4
  179. package/Types/ResourceAiAgent/AiResourceType.ts +310 -0
  180. package/Types/ResourceAiAgent/ResourceAiAccess.ts +574 -0
  181. package/Types/Rum/SessionReplayBudgetMetricType.ts +47 -0
  182. package/Types/Runbook/RunbookStepType.ts +29 -0
  183. package/Types/Telemetry/TelemetryIngestSurface.ts +14 -2
  184. package/Utils/AI/InvestigationReport.ts +121 -15
  185. package/Utils/AiRemediation/Resource/CephCommandPolicy.ts +1936 -0
  186. package/Utils/AiRemediation/Resource/DatabaseCommandPolicy.ts +166 -0
  187. package/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.ts +1532 -0
  188. package/Utils/AiRemediation/Resource/DatabaseQueryRedactor.ts +1394 -0
  189. package/Utils/AiRemediation/Resource/DockerCliGrammar.ts +1839 -0
  190. package/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.ts +490 -0
  191. package/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.ts +687 -0
  192. package/Utils/AiRemediation/Resource/GovcCommandPolicy.ts +2096 -0
  193. package/Utils/AiRemediation/Resource/HostCommandPolicy.ts +2982 -0
  194. package/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.ts +1679 -0
  195. package/Utils/AiRemediation/Resource/ResourceCommandPolicy.ts +851 -0
  196. package/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.ts +593 -0
  197. package/Utils/AiRemediation/Resource/ResourceOutputRedactor.ts +2812 -0
  198. package/Utils/SessionReplay/SessionReplayBudgetMetricType.ts +184 -0
  199. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
  200. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
  201. package/build/dist/Models/DatabaseModels/CephCluster.js +219 -0
  202. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  203. package/build/dist/Models/DatabaseModels/DatabaseServer.js +219 -0
  204. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -1
  205. package/build/dist/Models/DatabaseModels/DockerHost.js +219 -0
  206. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  207. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +219 -0
  208. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
  209. package/build/dist/Models/DatabaseModels/GlobalConfig.js +1 -1
  210. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  211. package/build/dist/Models/DatabaseModels/Host.js +219 -0
  212. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  213. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  214. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  215. package/build/dist/Models/DatabaseModels/PodmanHost.js +219 -0
  216. package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
  217. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +219 -0
  218. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  219. package/build/dist/Models/DatabaseModels/ResourceAiAgent.js +439 -0
  220. package/build/dist/Models/DatabaseModels/ResourceAiAgent.js.map +1 -0
  221. package/build/dist/Models/DatabaseModels/RunnerJob.js +145 -0
  222. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  223. package/build/dist/Models/DatabaseModels/VMwareVCenter.js +219 -0
  224. package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -1
  225. package/build/dist/Server/API/AutoRemediationAPI.js +231 -1
  226. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  227. package/build/dist/Server/API/ResourceAiAccessAPI.js +1119 -0
  228. package/build/dist/Server/API/ResourceAiAccessAPI.js.map +1 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js +168 -0
  230. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js.map +1 -0
  231. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  232. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  233. package/build/dist/Server/Infrastructure/Semaphore.js +6 -0
  234. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  235. package/build/dist/Server/Middleware/TelemetryIngest.js +12 -5
  236. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  237. package/build/dist/Server/Services/AnalyticsDatabaseService.js +26 -1
  238. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  239. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +601 -3
  240. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
  241. package/build/dist/Server/Services/CephClusterService.js +104 -0
  242. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  243. package/build/dist/Server/Services/DatabaseServerService.js +99 -0
  244. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -1
  245. package/build/dist/Server/Services/DockerHostService.js +104 -0
  246. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  247. package/build/dist/Server/Services/DockerSwarmClusterService.js +104 -0
  248. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  249. package/build/dist/Server/Services/HostService.js +114 -2
  250. package/build/dist/Server/Services/HostService.js.map +1 -1
  251. package/build/dist/Server/Services/MetricRecordingRuleService.js +46 -0
  252. package/build/dist/Server/Services/MetricRecordingRuleService.js.map +1 -1
  253. package/build/dist/Server/Services/PodmanHostService.js +104 -0
  254. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  255. package/build/dist/Server/Services/ProxmoxClusterService.js +104 -0
  256. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  257. package/build/dist/Server/Services/ResourceAiAccessService.js +1028 -0
  258. package/build/dist/Server/Services/ResourceAiAccessService.js.map +1 -0
  259. package/build/dist/Server/Services/ResourceAiAgentJobService.js +173 -0
  260. package/build/dist/Server/Services/ResourceAiAgentJobService.js.map +1 -0
  261. package/build/dist/Server/Services/ResourceAiAgentService.js +1687 -0
  262. package/build/dist/Server/Services/ResourceAiAgentService.js.map +1 -0
  263. package/build/dist/Server/Services/RunnerJobService.js +344 -5
  264. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  265. package/build/dist/Server/Services/TelemetryUsageBillingService.js +26 -0
  266. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  267. package/build/dist/Server/Services/TraceRecordingRuleService.js +37 -0
  268. package/build/dist/Server/Services/TraceRecordingRuleService.js.map +1 -1
  269. package/build/dist/Server/Services/VMwareVCenterService.js +104 -0
  270. package/build/dist/Server/Services/VMwareVCenterService.js.map +1 -1
  271. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +1029 -30
  272. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  273. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +633 -28
  274. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  275. package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js +21 -1
  276. package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js.map +1 -1
  277. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js +330 -0
  278. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js.map +1 -0
  279. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js +160 -0
  280. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js.map +1 -0
  281. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js +33 -0
  282. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js.map +1 -0
  283. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js +640 -0
  284. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js.map +1 -0
  285. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js +226 -0
  286. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js.map +1 -0
  287. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js +600 -0
  288. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js.map +1 -0
  289. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +50 -4
  290. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  291. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +55 -4
  292. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
  293. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +55 -4
  294. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
  295. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +290 -13
  296. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  297. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +29 -1
  298. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
  299. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +9 -1
  300. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  301. package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js +627 -0
  302. package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js.map +1 -0
  303. package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js +60 -5
  304. package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js.map +1 -1
  305. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +19 -15
  306. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  307. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +19 -15
  308. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
  309. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +17 -1
  310. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  311. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +183 -209
  312. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  313. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +19 -15
  314. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
  315. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +233 -167
  316. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
  317. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +263 -110
  318. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  319. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js +129 -0
  320. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js.map +1 -0
  321. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js +266 -0
  322. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js.map +1 -0
  323. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js +258 -0
  324. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js.map +1 -0
  325. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js +104 -0
  326. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js.map +1 -0
  327. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js +183 -0
  328. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js.map +1 -0
  329. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js +123 -0
  330. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js.map +1 -0
  331. package/build/dist/Types/AI/ResourceAiAccessApi.js +30 -0
  332. package/build/dist/Types/AI/ResourceAiAccessApi.js.map +1 -0
  333. package/build/dist/Types/AI/ResourceAiAccessPermissions.js +98 -0
  334. package/build/dist/Types/AI/ResourceAiAccessPermissions.js.map +1 -0
  335. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +116 -29
  336. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  337. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +47 -21
  338. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  339. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +5 -0
  340. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  341. package/build/dist/Types/Monitor/RumAlertTemplates.js +199 -3
  342. package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
  343. package/build/dist/Types/ResourceAiAgent/AiResourceType.js +242 -0
  344. package/build/dist/Types/ResourceAiAgent/AiResourceType.js.map +1 -0
  345. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js +275 -0
  346. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js.map +1 -0
  347. package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js +45 -0
  348. package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js.map +1 -0
  349. package/build/dist/Types/Runbook/RunbookStepType.js +27 -0
  350. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  351. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +13 -2
  352. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  353. package/build/dist/Utils/AI/InvestigationReport.js +71 -11
  354. package/build/dist/Utils/AI/InvestigationReport.js.map +1 -1
  355. package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js +1462 -0
  356. package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js.map +1 -0
  357. package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js +122 -0
  358. package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js.map +1 -0
  359. package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js +1139 -0
  360. package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js.map +1 -0
  361. package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js +1060 -0
  362. package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js.map +1 -0
  363. package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js +1301 -0
  364. package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js.map +1 -0
  365. package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js +370 -0
  366. package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js.map +1 -0
  367. package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js +498 -0
  368. package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js.map +1 -0
  369. package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js +1565 -0
  370. package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js.map +1 -0
  371. package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js +2206 -0
  372. package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js.map +1 -0
  373. package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js +1129 -0
  374. package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js.map +1 -0
  375. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js +565 -0
  376. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js.map +1 -0
  377. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js +408 -0
  378. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js.map +1 -0
  379. package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js +1910 -0
  380. package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js.map +1 -0
  381. package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js +160 -0
  382. package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js.map +1 -0
  383. package/package.json +1 -1
@@ -0,0 +1,1093 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ jest,
7
+ test,
8
+ } from "@jest/globals";
9
+
10
+ /*
11
+ * Issue #4111: "create incident" and "create maintenance" in Microsoft Teams
12
+ * answered "Sorry, I encountered an error processing your request" - twice.
13
+ * Part of the fix is that the bot checks the sender before it sends either
14
+ * form, and the commonest reason a sender cannot use a form is that they
15
+ * never connected their Microsoft Teams account to OneUptime.
16
+ *
17
+ * MicrosoftTeamsAuthAction.getOneUptimeUserIdFromTeamsUserId used to throw a
18
+ * plain BadDataException for that and log it with logger.error. So a card
19
+ * button pressed by such a user was answered "Sorry, that action failed.
20
+ * Please try again later.", and every press filled the server log with an
21
+ * error that was not one.
22
+ *
23
+ * It now throws MicrosoftTeamsAccountNotLinkedException, logged at debug.
24
+ * That is still a BadDataException with the same message, so whatever
25
+ * handled the old exception handles this one. A failure to read the link
26
+ * table is not "not linked": it is rethrown as it was and logged as an error,
27
+ * so the bot need not tell a linked user to connect an account they have
28
+ * connected.
29
+ *
30
+ * The first half pins the lookup, with WorkspaceUserAuthTokenService stubbed.
31
+ * The second runs the real lookup under the places that turn the exception
32
+ * into guidance - a card button (handleBotInvokeActivity), the create
33
+ * commands (MicrosoftTeamsCreateCommands) and the AI assistant ("ask ...",
34
+ * through handleBotMessageActivity) - with only the link table, the tenant
35
+ * and the dashboard URL stubbed. Each says where to connect the account,
36
+ * with the link to User Settings, and takes nothing else for "not linked".
37
+ *
38
+ * The AI assistant used to answer every lookup failure - a database error
39
+ * included - with "Please connect your Microsoft Teams account in OneUptime
40
+ * User Settings", so it told a linked user to connect the account they had
41
+ * connected. It now answers only the not-linked exception, and rethrows
42
+ * anything else, which the message handler answers with its one generic
43
+ * reply and logs by the command, never by the question.
44
+ *
45
+ * The create commands check the sender only in a personal chat, where the
46
+ * sender is the one who will submit the form. In a team channel or a group
47
+ * chat anyone there may submit it, so the form is posted without looking the
48
+ * sender up, and the submit checks whoever submits it.
49
+ */
50
+
51
+ jest.mock("../../../../../Server/EnvironmentConfig", () => {
52
+ return {
53
+ ...(jest.requireActual("../../../../../Server/EnvironmentConfig") as Record<
54
+ string,
55
+ unknown
56
+ >),
57
+ MicrosoftTeamsAppClientId: "11111111-2222-3333-4444-555555555555",
58
+ MicrosoftTeamsAppClientSecret: "test-secret",
59
+ MicrosoftTeamsAppTenantId: "test-tenant",
60
+ };
61
+ });
62
+
63
+ /*
64
+ * Same botbuilder module factory as MicrosoftTeamsTeamInstalls.test.ts: the
65
+ * repo-wide manual mock (Tests/__mocks__/botbuilder.js) does not expose
66
+ * TeamsInfo or MessageFactory.attachment, both of which MicrosoftTeams.ts
67
+ * touches at import time.
68
+ */
69
+ jest.mock("botbuilder", () => {
70
+ return {
71
+ CloudAdapter: class CloudAdapter {},
72
+ ConfigurationBotFrameworkAuthentication: class ConfigurationBotFrameworkAuthentication {},
73
+ TeamsActivityHandler: class TeamsActivityHandler {},
74
+ TurnContext: class TurnContext {},
75
+ ActivityHandler: class ActivityHandler {},
76
+ MessageFactory: {
77
+ text: jest.fn(),
78
+ attachment: jest.fn((attachment: unknown) => {
79
+ return { type: "message", attachments: [attachment] };
80
+ }),
81
+ },
82
+ CardFactory: { heroCard: jest.fn() },
83
+ TeamsInfo: {
84
+ getMembers: jest.fn(),
85
+ getPagedMembers: jest.fn(),
86
+ },
87
+ };
88
+ });
89
+
90
+ import type { TurnContext } from "botbuilder";
91
+ import type { SpyInstance } from "jest-mock";
92
+ import WorkspaceProjectAuthToken from "../../../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
93
+ import WorkspaceUserAuthToken from "../../../../../Models/DatabaseModels/WorkspaceUserAuthToken";
94
+ import DatabaseConfig from "../../../../../Server/DatabaseConfig";
95
+ import AIService from "../../../../../Server/Services/AIService";
96
+ import IncidentService from "../../../../../Server/Services/IncidentService";
97
+ import ScheduledMaintenanceService from "../../../../../Server/Services/ScheduledMaintenanceService";
98
+ import WorkspaceUserAuthTokenService from "../../../../../Server/Services/WorkspaceUserAuthTokenService";
99
+ import logger, { LogAttributes } from "../../../../../Server/Utils/Logger";
100
+ import {
101
+ MicrosoftTeamsIncidentActionType,
102
+ MicrosoftTeamsScheduledMaintenanceActionType,
103
+ } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/ActionTypes";
104
+ import MicrosoftTeamsAuthAction, {
105
+ MicrosoftTeamsAccountNotLinkedException,
106
+ } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/Auth";
107
+ import MicrosoftTeamsIncidentActions, {
108
+ MicrosoftTeamsNewIncidentFormChoices,
109
+ } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/Incident";
110
+ import MicrosoftTeamsScheduledMaintenanceActions, {
111
+ MicrosoftTeamsNewScheduledMaintenanceFormChoices,
112
+ } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance";
113
+ import MicrosoftTeamsUtil from "../../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
114
+ import { MicrosoftTeamsCardChoiceList } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices";
115
+ import MicrosoftTeamsCreateCommands from "../../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands";
116
+ import MicrosoftTeamsReplies, {
117
+ MICROSOFT_TEAMS_ADAPTIVE_CARD_CONTENT_TYPE,
118
+ } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies";
119
+ import WorkspaceActionAuthorization from "../../../../../Server/Utils/Workspace/WorkspaceActionAuthorization";
120
+ import URL from "../../../../../Types/API/URL";
121
+ import BadDataException from "../../../../../Types/Exception/BadDataException";
122
+ import Exception from "../../../../../Types/Exception/Exception";
123
+ import ExceptionCode from "../../../../../Types/Exception/ExceptionCode";
124
+ import { JSONObject } from "../../../../../Types/JSON";
125
+ import ObjectID from "../../../../../Types/ObjectID";
126
+ import WorkspaceType from "../../../../../Types/Workspace/WorkspaceType";
127
+
128
+ const PROJECT_ID: ObjectID = new ObjectID(
129
+ "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
130
+ );
131
+ const LINKED_USER_ID: ObjectID = new ObjectID(
132
+ "d2e3f4a5-b6c7-4d8e-9f0a-1b2c3d4e5f6a",
133
+ );
134
+
135
+ // The sender as Teams names them (from.aadObjectId), and their tenant.
136
+ const TEAMS_USER_ID: string = "e3f4a5b6-c7d8-4e9f-8a1b-2c3d4e5f6a7b";
137
+ const TENANT_ID: string = "f4a5b6c7-d8e9-4f0a-9b2c-3d4e5f6a7b8c";
138
+
139
+ const INCIDENT_ID: string = "a5b6c7d8-e9f0-4a1b-8c3d-4e5f6a7b8c9d";
140
+ const SEVERITY_ID: string = "b6c7d8e9-f0a1-4b2c-9d3e-5f6a7b8c9d0e";
141
+
142
+ // The message the exception carried before the fix, and still carries.
143
+ const NOT_LINKED_MESSAGE: string =
144
+ "No OneUptime user linked to this Microsoft Teams user. Please authenticate with Microsoft Teams.";
145
+
146
+ const DASHBOARD_URL: string = "https://oneuptime.test/dashboard";
147
+ const SETTINGS_LINK: string = `${DASHBOARD_URL}/${PROJECT_ID.toString()}/user-settings/microsoft-teams-integration`;
148
+
149
+ // Where the log lines say it happened.
150
+ const LOG_ATTRIBUTES: JSONObject = {
151
+ projectId: PROJECT_ID.toString(),
152
+ workspaceUserId: TEAMS_USER_ID,
153
+ };
154
+ const NOT_LINKED_LOG_LINE: string = `No OneUptime user linked to Teams user: ${TEAMS_USER_ID}`;
155
+ const LOOKUP_FAILED_LOG_LINE: string = `Error finding OneUptime user for Teams user: ${TEAMS_USER_ID}`;
156
+
157
+ function lookUpTeamsUser(): Promise<ObjectID> {
158
+ return MicrosoftTeamsAuthAction.getOneUptimeUserIdFromTeamsUserId({
159
+ teamsUserId: TEAMS_USER_ID,
160
+ projectId: PROJECT_ID,
161
+ });
162
+ }
163
+
164
+ // A WorkspaceUserAuthToken row: the link between a Teams and a OneUptime user.
165
+ function linkRow(userId?: ObjectID): WorkspaceUserAuthToken {
166
+ const row: WorkspaceUserAuthToken = new WorkspaceUserAuthToken();
167
+ row.workspaceUserId = TEAMS_USER_ID;
168
+ row.projectId = PROJECT_ID;
169
+ row.workspaceType = WorkspaceType.MicrosoftTeams;
170
+
171
+ if (userId) {
172
+ row.userId = userId;
173
+ }
174
+
175
+ return row;
176
+ }
177
+
178
+ // What a promise that must fail was rejected with.
179
+ async function rejectionOf(promise: Promise<unknown>): Promise<unknown> {
180
+ try {
181
+ await promise;
182
+ } catch (error) {
183
+ return error;
184
+ }
185
+
186
+ throw new Error("Expected the promise to be rejected, but it resolved.");
187
+ }
188
+
189
+ type LinkTableSpy = SpyInstance<typeof WorkspaceUserAuthTokenService.findOneBy>;
190
+
191
+ function stubLinkTable(row: WorkspaceUserAuthToken | null): LinkTableSpy {
192
+ return jest
193
+ .spyOn(WorkspaceUserAuthTokenService, "findOneBy")
194
+ .mockResolvedValue(row);
195
+ }
196
+
197
+ function stubUnreadableLinkTable(error: unknown): LinkTableSpy {
198
+ return jest
199
+ .spyOn(WorkspaceUserAuthTokenService, "findOneBy")
200
+ .mockRejectedValue(error);
201
+ }
202
+
203
+ interface UnlinkedCase {
204
+ name: string;
205
+ row: () => WorkspaceUserAuthToken | null;
206
+ }
207
+
208
+ const UNLINKED: ReadonlyArray<UnlinkedCase> = [
209
+ {
210
+ name: "no link for this Teams user in this project",
211
+ row: (): WorkspaceUserAuthToken | null => {
212
+ return null;
213
+ },
214
+ },
215
+ {
216
+ name: "a link that names no OneUptime user",
217
+ row: (): WorkspaceUserAuthToken | null => {
218
+ return linkRow();
219
+ },
220
+ },
221
+ ];
222
+
223
+ interface LookupFailureCase {
224
+ name: string;
225
+ error: () => unknown;
226
+ // How a log line names it (MicrosoftTeamsReplies.describeError).
227
+ described: string;
228
+ }
229
+
230
+ const LOOKUP_FAILURES: ReadonlyArray<LookupFailureCase> = [
231
+ {
232
+ name: "the database is unreachable",
233
+ error: (): Error => {
234
+ return new Error("Connection terminated unexpectedly");
235
+ },
236
+ described: "Error: Connection terminated unexpectedly",
237
+ },
238
+ {
239
+ name: "the database rejects the query",
240
+ error: (): Error => {
241
+ return Object.assign(
242
+ new Error('relation "WorkspaceUserAuthToken" does not exist'),
243
+ { name: "QueryFailedError", code: "42P01" },
244
+ );
245
+ },
246
+ described:
247
+ 'QueryFailedError 42P01: relation "WorkspaceUserAuthToken" does not exist',
248
+ },
249
+ {
250
+ // The not-linked exception is a BadDataException; this one is not it.
251
+ name: "the data layer throws a BadDataException of its own",
252
+ error: (): BadDataException => {
253
+ return new BadDataException("Invalid select: userId");
254
+ },
255
+ described: "BadDataException: Invalid select: userId",
256
+ },
257
+ ];
258
+
259
+ let debugLog: SpyInstance<typeof logger.debug>;
260
+ let errorLog: SpyInstance<typeof logger.error>;
261
+
262
+ /*
263
+ * Every logger.error call, with exactly the arguments it was given. Copied
264
+ * into arrays made here: jest records calls in arrays of its own realm,
265
+ * which toStrictEqual would tell apart from the literals they are compared
266
+ * with.
267
+ */
268
+ function errorLogCalls(): Array<Array<unknown>> {
269
+ return Array.from(
270
+ errorLog.mock.calls,
271
+ (call: Parameters<typeof logger.error>): Array<unknown> => {
272
+ return [...call];
273
+ },
274
+ );
275
+ }
276
+
277
+ beforeEach((): void => {
278
+ debugLog = jest.spyOn(logger, "debug").mockImplementation((): void => {});
279
+ errorLog = jest.spyOn(logger, "error").mockImplementation((): void => {});
280
+ });
281
+
282
+ afterEach((): void => {
283
+ jest.restoreAllMocks();
284
+ });
285
+
286
+ describe("MicrosoftTeamsAuthAction.getOneUptimeUserIdFromTeamsUserId", (): void => {
287
+ test("a linked Teams user resolves to the OneUptime user the link names, looked up by that Teams user in this project's Microsoft Teams links", async (): Promise<void> => {
288
+ const row: WorkspaceUserAuthToken = linkRow(LINKED_USER_ID);
289
+ const findOneBy: LinkTableSpy = stubLinkTable(row);
290
+
291
+ const userId: ObjectID = await lookUpTeamsUser();
292
+
293
+ expect(userId).toBe(row.userId);
294
+ expect(userId.toString()).toBe(LINKED_USER_ID.toString());
295
+
296
+ expect(findOneBy).toHaveBeenCalledTimes(1);
297
+ expect(findOneBy).toHaveBeenCalledWith({
298
+ query: {
299
+ workspaceUserId: TEAMS_USER_ID,
300
+ projectId: PROJECT_ID,
301
+ workspaceType: WorkspaceType.MicrosoftTeams,
302
+ },
303
+ select: {
304
+ userId: true,
305
+ },
306
+ props: {
307
+ isRoot: true,
308
+ },
309
+ });
310
+
311
+ expect(errorLog).not.toHaveBeenCalled();
312
+ expect(debugLog).not.toHaveBeenCalledWith(
313
+ NOT_LINKED_LOG_LINE,
314
+ expect.anything(),
315
+ );
316
+ });
317
+
318
+ test.each(UNLINKED)(
319
+ "$name: throws MicrosoftTeamsAccountNotLinkedException, still a BadDataException with the same message",
320
+ async (unlinked: UnlinkedCase): Promise<void> => {
321
+ stubLinkTable(unlinked.row());
322
+
323
+ const error: unknown = await rejectionOf(lookUpTeamsUser());
324
+
325
+ expect(error).toBeInstanceOf(MicrosoftTeamsAccountNotLinkedException);
326
+ expect(error).toBeInstanceOf(BadDataException);
327
+ expect((error as BadDataException).message).toBe(NOT_LINKED_MESSAGE);
328
+ expect((error as BadDataException).code).toBe(
329
+ ExceptionCode.BadDataException,
330
+ );
331
+ },
332
+ );
333
+
334
+ test.each(UNLINKED)(
335
+ "$name: logged at debug, as expected - not as a server error",
336
+ async (unlinked: UnlinkedCase): Promise<void> => {
337
+ stubLinkTable(unlinked.row());
338
+
339
+ await rejectionOf(lookUpTeamsUser());
340
+
341
+ // master: logger.error("Error finding OneUptime user ...") and the error.
342
+ expect(errorLog).not.toHaveBeenCalled();
343
+ expect(debugLog).toHaveBeenCalledWith(
344
+ NOT_LINKED_LOG_LINE,
345
+ LOG_ATTRIBUTES,
346
+ );
347
+ },
348
+ );
349
+
350
+ test.each(LOOKUP_FAILURES)(
351
+ "$name: that error is rethrown as it is - never as 'not linked' - and logged as an error",
352
+ async (failure: LookupFailureCase): Promise<void> => {
353
+ const lookupError: unknown = failure.error();
354
+ stubUnreadableLinkTable(lookupError);
355
+
356
+ const error: unknown = await rejectionOf(lookUpTeamsUser());
357
+
358
+ expect(error).toBe(lookupError);
359
+ expect(error).not.toBeInstanceOf(MicrosoftTeamsAccountNotLinkedException);
360
+
361
+ expect(errorLog).toHaveBeenCalledWith(
362
+ LOOKUP_FAILED_LOG_LINE,
363
+ LOG_ATTRIBUTES,
364
+ );
365
+ expect(errorLog).toHaveBeenCalledWith(lookupError);
366
+ expect(debugLog).not.toHaveBeenCalledWith(
367
+ NOT_LINKED_LOG_LINE,
368
+ expect.anything(),
369
+ );
370
+ },
371
+ );
372
+ });
373
+
374
+ describe("MicrosoftTeamsAccountNotLinkedException", (): void => {
375
+ test("is a BadDataException, so whatever handles bad data still handles it", (): void => {
376
+ const error: MicrosoftTeamsAccountNotLinkedException =
377
+ new MicrosoftTeamsAccountNotLinkedException(NOT_LINKED_MESSAGE);
378
+
379
+ expect(error).toBeInstanceOf(BadDataException);
380
+ expect(error).toBeInstanceOf(Exception);
381
+ expect(error).toBeInstanceOf(Error);
382
+ expect(error.code).toBe(ExceptionCode.BadDataException);
383
+ expect(error.message).toBe(NOT_LINKED_MESSAGE);
384
+
385
+ // The bot shows a BadDataException's message to the user.
386
+ expect(MicrosoftTeamsReplies.getUserFacingErrorMessage(error)).toBe(
387
+ NOT_LINKED_MESSAGE,
388
+ );
389
+ });
390
+
391
+ test("an ordinary BadDataException is not taken for it", (): void => {
392
+ expect(new BadDataException(NOT_LINKED_MESSAGE)).not.toBeInstanceOf(
393
+ MicrosoftTeamsAccountNotLinkedException,
394
+ );
395
+ });
396
+
397
+ test("a log line names it by its class, not as a plain Error: OneUptime's exceptions keep Error's name", (): void => {
398
+ const error: MicrosoftTeamsAccountNotLinkedException =
399
+ new MicrosoftTeamsAccountNotLinkedException(NOT_LINKED_MESSAGE);
400
+
401
+ expect(error.name).toBe("Error");
402
+ expect(MicrosoftTeamsReplies.describeError(error)).toBe(
403
+ `MicrosoftTeamsAccountNotLinkedException: ${NOT_LINKED_MESSAGE}`,
404
+ );
405
+ // So the log tells it apart from the BadDataException it extends.
406
+ expect(
407
+ MicrosoftTeamsReplies.describeError(
408
+ new BadDataException(NOT_LINKED_MESSAGE),
409
+ ),
410
+ ).toBe(`BadDataException: ${NOT_LINKED_MESSAGE}`);
411
+ });
412
+ });
413
+
414
+ interface FakeTurn {
415
+ activity: JSONObject;
416
+ turnContext: TurnContext;
417
+ replies: Array<string>;
418
+ }
419
+
420
+ function createFakeTurn(activity: JSONObject): FakeTurn {
421
+ const replies: Array<string> = [];
422
+
423
+ const turnContext: TurnContext = {
424
+ activity: activity,
425
+ sendActivity: async (reply: unknown): Promise<{ id: string }> => {
426
+ replies.push(typeof reply === "string" ? reply : JSON.stringify(reply));
427
+ return { id: `reply-${replies.length}` };
428
+ },
429
+ } as unknown as TurnContext;
430
+
431
+ return { activity: activity, turnContext: turnContext, replies: replies };
432
+ }
433
+
434
+ function fromSender(): JSONObject {
435
+ return { id: "29:teams-user", aadObjectId: TEAMS_USER_ID, name: "Alex" };
436
+ }
437
+
438
+ // A card button press: Acknowledge on an incident notification.
439
+ function acknowledgeButtonActivity(): JSONObject {
440
+ return {
441
+ type: "invoke",
442
+ name: "adaptiveCard/action",
443
+ id: "1727611260456",
444
+ value: {
445
+ action: MicrosoftTeamsIncidentActionType.AckIncident,
446
+ actionValue: INCIDENT_ID,
447
+ },
448
+ from: fromSender(),
449
+ recipient: { id: "28:oneuptime-bot" },
450
+ conversation: { conversationType: "personal", id: "a:personal-chat" },
451
+ channelData: { tenant: { id: TENANT_ID } },
452
+ };
453
+ }
454
+
455
+ function commandActivity(text: string): JSONObject {
456
+ return {
457
+ type: "message",
458
+ id: "1727611290789",
459
+ text: text,
460
+ from: fromSender(),
461
+ recipient: { id: "28:oneuptime-bot" },
462
+ conversation: { conversationType: "personal", id: "a:personal-chat" },
463
+ channelData: { tenant: { id: TENANT_ID } },
464
+ };
465
+ }
466
+
467
+ // Conversations where anyone there can submit a form the bot posts.
468
+ type SharedConversation = "channel" | "groupChat";
469
+
470
+ const SHARED_CONVERSATIONS: Record<SharedConversation, JSONObject> = {
471
+ channel: {
472
+ conversation: {
473
+ conversationType: "channel",
474
+ id: "19:general@thread.tacv2;messageid=1727611290789",
475
+ isGroup: true,
476
+ },
477
+ channelData: {
478
+ tenant: { id: TENANT_ID },
479
+ team: { id: "19:engineering@thread.tacv2" },
480
+ channel: { id: "19:general@thread.tacv2" },
481
+ },
482
+ },
483
+ groupChat: {
484
+ conversation: {
485
+ conversationType: "groupChat",
486
+ id: "19:on-call-chat@thread.v2",
487
+ isGroup: true,
488
+ },
489
+ },
490
+ };
491
+
492
+ // The same activity, in a team channel or a group chat.
493
+ function inSharedConversation(
494
+ activity: JSONObject,
495
+ where: SharedConversation,
496
+ ): JSONObject {
497
+ return { ...activity, ...SHARED_CONVERSATIONS[where] };
498
+ }
499
+
500
+ // A question for the AI assistant, typed to the bot in a personal chat.
501
+ function askActivity(activityId: string): JSONObject {
502
+ return {
503
+ type: "message",
504
+ id: activityId,
505
+ text: "ask why is checkout failing?",
506
+ from: fromSender(),
507
+ recipient: { id: "28:oneuptime-bot" },
508
+ conversation: { conversationType: "personal", id: "a:personal-chat" },
509
+ channelData: { tenant: { id: TENANT_ID } },
510
+ };
511
+ }
512
+
513
+ function connectYourAccount(purpose: string): string {
514
+ return `To ${purpose}, first connect your Microsoft Teams account to OneUptime in [OneUptime → User Settings → Microsoft Teams](${SETTINGS_LINK}), then try again.`;
515
+ }
516
+
517
+ // What every "connect your account" reply says, whatever its wording.
518
+ const CONNECT_YOUR_ACCOUNT: string = "connect your Microsoft Teams account";
519
+
520
+ /*
521
+ * A private static of MicrosoftTeamsUtil, reached through a cast the way
522
+ * MicrosoftTeamsBotMessageHandling.test.ts reaches it.
523
+ */
524
+ interface MicrosoftTeamsUtilInternals {
525
+ captureChatFromBotActivity: (data: {
526
+ activity: JSONObject;
527
+ turnContext: TurnContext;
528
+ onlyIfMissingOrStale?: boolean | undefined;
529
+ }) => Promise<void>;
530
+ }
531
+
532
+ describe("what a Teams user without a linked account is told (the real lookup; only the link table is stubbed)", (): void => {
533
+ let memberPropsSpy: SpyInstance<
534
+ typeof WorkspaceActionAuthorization.getProjectMemberProps
535
+ >;
536
+
537
+ beforeEach((): void => {
538
+ const projectAuth: WorkspaceProjectAuthToken =
539
+ new WorkspaceProjectAuthToken();
540
+ projectAuth.projectId = PROJECT_ID;
541
+
542
+ jest
543
+ .spyOn(MicrosoftTeamsUtil, "resolveProjectByTenantId")
544
+ .mockResolvedValue({
545
+ projectAuth: projectAuth,
546
+ isAmbiguous: false,
547
+ candidateProjectIds: [PROJECT_ID],
548
+ });
549
+ jest
550
+ .spyOn(DatabaseConfig, "getDashboardUrl")
551
+ .mockResolvedValue(URL.fromString(DASHBOARD_URL));
552
+ memberPropsSpy = jest
553
+ .spyOn(WorkspaceActionAuthorization, "getProjectMemberProps")
554
+ .mockResolvedValue({ userId: LINKED_USER_ID, tenantId: PROJECT_ID });
555
+ });
556
+
557
+ describe("a card button (handleBotInvokeActivity)", (): void => {
558
+ let acknowledgeSpy: SpyInstance<typeof IncidentService.acknowledgeIncident>;
559
+
560
+ beforeEach((): void => {
561
+ acknowledgeSpy = jest
562
+ .spyOn(IncidentService, "acknowledgeIncident")
563
+ .mockRejectedValue(new Error("The action must not run."));
564
+ });
565
+
566
+ async function pressAcknowledge(): Promise<FakeTurn> {
567
+ const activity: JSONObject = acknowledgeButtonActivity();
568
+ const turn: FakeTurn = createFakeTurn(activity);
569
+
570
+ await MicrosoftTeamsUtil.handleBotInvokeActivity({
571
+ activity: activity,
572
+ turnContext: turn.turnContext,
573
+ });
574
+
575
+ return turn;
576
+ }
577
+
578
+ test.each(UNLINKED)(
579
+ "$name: one reply that says where to connect the account; the action does not run",
580
+ async (unlinked: UnlinkedCase): Promise<void> => {
581
+ stubLinkTable(unlinked.row());
582
+
583
+ const turn: FakeTurn = await pressAcknowledge();
584
+
585
+ // master: ["Sorry, that action failed. Please try again later."]
586
+ expect(turn.replies).toEqual([
587
+ connectYourAccount("use OneUptime actions in Microsoft Teams"),
588
+ ]);
589
+ expect(memberPropsSpy).not.toHaveBeenCalled();
590
+ expect(acknowledgeSpy).not.toHaveBeenCalled();
591
+ expect(errorLog).not.toHaveBeenCalled();
592
+ },
593
+ );
594
+
595
+ test("without a dashboard URL the reply still says where to connect the account", async (): Promise<void> => {
596
+ stubLinkTable(null);
597
+ jest
598
+ .spyOn(DatabaseConfig, "getDashboardUrl")
599
+ .mockRejectedValue(new Error("No host is configured."));
600
+
601
+ const turn: FakeTurn = await pressAcknowledge();
602
+
603
+ expect(turn.replies).toEqual([
604
+ "To use OneUptime actions in Microsoft Teams, first connect your Microsoft Teams account to OneUptime in OneUptime under User Settings → Microsoft Teams, then try again.",
605
+ ]);
606
+ expect(errorLog).not.toHaveBeenCalled();
607
+ });
608
+
609
+ // What handleBotInvokeActivity's log lines carry for this button.
610
+ const ACKNOWLEDGE_LOG_ATTRIBUTES: LogAttributes = {
611
+ actionType: MicrosoftTeamsIncidentActionType.AckIncident,
612
+ projectId: PROJECT_ID.toString(),
613
+ };
614
+
615
+ test("a link table that cannot be read: the generic failure, logged as an error - never 'connect your account'", async (): Promise<void> => {
616
+ const lookupError: Error = new Error(
617
+ "Connection terminated unexpectedly",
618
+ );
619
+ stubUnreadableLinkTable(lookupError);
620
+
621
+ const turn: FakeTurn = await pressAcknowledge();
622
+
623
+ expect(turn.replies).toEqual([
624
+ "Sorry, that action failed. Please try again later.",
625
+ ]);
626
+ expect(errorLog).toHaveBeenCalledWith(
627
+ LOOKUP_FAILED_LOG_LINE,
628
+ LOG_ATTRIBUTES,
629
+ );
630
+ expect(acknowledgeSpy).not.toHaveBeenCalled();
631
+ expect(errorLogCalls()).toStrictEqual([
632
+ [LOOKUP_FAILED_LOG_LINE, LOG_ATTRIBUTES],
633
+ [lookupError],
634
+ [
635
+ "Error handling bot invoke activity: Error: Connection terminated unexpectedly",
636
+ ACKNOWLEDGE_LOG_ATTRIBUTES,
637
+ ],
638
+ [lookupError, ACKNOWLEDGE_LOG_ATTRIBUTES],
639
+ ]);
640
+ });
641
+
642
+ test("a BadDataException of the data layer's own is answered with its reason, not with 'connect your account'", async (): Promise<void> => {
643
+ const lookupError: BadDataException = new BadDataException(
644
+ "Invalid select: userId",
645
+ );
646
+ stubUnreadableLinkTable(lookupError);
647
+
648
+ const turn: FakeTurn = await pressAcknowledge();
649
+
650
+ expect(turn.replies).toEqual([
651
+ "Sorry, that action failed: Invalid select: userId",
652
+ ]);
653
+ expect(errorLog).toHaveBeenCalledWith(
654
+ LOOKUP_FAILED_LOG_LINE,
655
+ LOG_ATTRIBUTES,
656
+ );
657
+ // Named by its class, so the log does not read it as "not linked".
658
+ expect(errorLogCalls()).toStrictEqual([
659
+ [LOOKUP_FAILED_LOG_LINE, LOG_ATTRIBUTES],
660
+ [lookupError],
661
+ [
662
+ "Error handling bot invoke activity: BadDataException: Invalid select: userId",
663
+ ACKNOWLEDGE_LOG_ATTRIBUTES,
664
+ ],
665
+ [lookupError, ACKNOWLEDGE_LOG_ATTRIBUTES],
666
+ ]);
667
+ });
668
+ });
669
+
670
+ // A spy, as far as these tests read it.
671
+ interface CallRecorder {
672
+ mock: { calls: Array<unknown> };
673
+ }
674
+
675
+ interface CreateCommandCase {
676
+ name: string;
677
+ purpose: string;
678
+ what: string;
679
+ run: (turn: FakeTurn, activity: JSONObject) => Promise<void>;
680
+ // Stubs the read of the form's lists; the returned spy must stay unused.
681
+ stubFormLists: () => CallRecorder;
682
+ // Stubs the read of the form's lists with a small project's.
683
+ stubReadableFormLists: () => CallRecorder;
684
+ // The form's heading, and the action its submit button sends.
685
+ formTitle: string;
686
+ submitAction: string;
687
+ // The form filled in, as its submit carries it.
688
+ submittedFields: JSONObject;
689
+ // Stubs the create, which must not run: it rejects if it does.
690
+ stubCreateThatMustNotRun: () => CallRecorder;
691
+ }
692
+
693
+ function noChoices(): MicrosoftTeamsCardChoiceList {
694
+ return { choices: [], totalCount: 0 };
695
+ }
696
+
697
+ const CREATE_COMMANDS: ReadonlyArray<CreateCommandCase> = [
698
+ {
699
+ name: "create incident",
700
+ purpose: "create incidents from Microsoft Teams",
701
+ what: "an incident",
702
+ run: (turn: FakeTurn, activity: JSONObject): Promise<void> => {
703
+ return MicrosoftTeamsCreateCommands.handleCreateIncidentCommand({
704
+ turnContext: turn.turnContext,
705
+ activity: activity,
706
+ projectId: PROJECT_ID,
707
+ initialTitle: undefined,
708
+ });
709
+ },
710
+ stubFormLists: (): CallRecorder => {
711
+ return jest
712
+ .spyOn(MicrosoftTeamsIncidentActions, "getNewIncidentFormChoices")
713
+ .mockRejectedValue(new Error("The form's lists must not be read."));
714
+ },
715
+ stubReadableFormLists: (): CallRecorder => {
716
+ const choices: MicrosoftTeamsNewIncidentFormChoices = {
717
+ severities: {
718
+ choices: [{ title: "Critical", value: SEVERITY_ID }],
719
+ totalCount: 1,
720
+ },
721
+ monitors: noChoices(),
722
+ monitorStatuses: noChoices(),
723
+ labels: noChoices(),
724
+ onCallDutyPolicies: noChoices(),
725
+ };
726
+
727
+ return jest
728
+ .spyOn(MicrosoftTeamsIncidentActions, "getNewIncidentFormChoices")
729
+ .mockResolvedValue(choices);
730
+ },
731
+ formTitle: "Create New Incident",
732
+ submitAction: MicrosoftTeamsIncidentActionType.SubmitNewIncident,
733
+ submittedFields: {
734
+ incidentTitle: "Checkout is failing",
735
+ incidentDescription: "Payments time out after 30 seconds.",
736
+ incidentSeverity: SEVERITY_ID,
737
+ },
738
+ stubCreateThatMustNotRun: (): CallRecorder => {
739
+ return jest
740
+ .spyOn(IncidentService, "create")
741
+ .mockRejectedValue(new Error("The incident must not be created."));
742
+ },
743
+ },
744
+ {
745
+ name: "create maintenance",
746
+ purpose: "schedule maintenance from Microsoft Teams",
747
+ what: "a scheduled maintenance event",
748
+ run: (turn: FakeTurn, activity: JSONObject): Promise<void> => {
749
+ return MicrosoftTeamsCreateCommands.handleCreateScheduledMaintenanceCommand(
750
+ {
751
+ turnContext: turn.turnContext,
752
+ activity: activity,
753
+ projectId: PROJECT_ID,
754
+ initialTitle: undefined,
755
+ },
756
+ );
757
+ },
758
+ stubFormLists: (): CallRecorder => {
759
+ return jest
760
+ .spyOn(
761
+ MicrosoftTeamsScheduledMaintenanceActions,
762
+ "getNewScheduledMaintenanceFormChoices",
763
+ )
764
+ .mockRejectedValue(new Error("The form's lists must not be read."));
765
+ },
766
+ stubReadableFormLists: (): CallRecorder => {
767
+ const choices: MicrosoftTeamsNewScheduledMaintenanceFormChoices = {
768
+ monitors: noChoices(),
769
+ monitorStatuses: noChoices(),
770
+ labels: noChoices(),
771
+ };
772
+
773
+ return jest
774
+ .spyOn(
775
+ MicrosoftTeamsScheduledMaintenanceActions,
776
+ "getNewScheduledMaintenanceFormChoices",
777
+ )
778
+ .mockResolvedValue(choices);
779
+ },
780
+ formTitle: "Create New Scheduled Maintenance",
781
+ submitAction:
782
+ MicrosoftTeamsScheduledMaintenanceActionType.SubmitNewScheduledMaintenance,
783
+ submittedFields: {
784
+ scheduledMaintenanceTitle: "Database upgrade",
785
+ scheduledMaintenanceDescription: "Upgrading the primary to 16.4.",
786
+ startDate: "2099-06-01",
787
+ startTime: "10:00",
788
+ endDate: "2099-06-01",
789
+ endTime: "11:00",
790
+ },
791
+ stubCreateThatMustNotRun: (): CallRecorder => {
792
+ return jest
793
+ .spyOn(ScheduledMaintenanceService, "create")
794
+ .mockRejectedValue(new Error("The event must not be created."));
795
+ },
796
+ },
797
+ ];
798
+
799
+ describe("the create commands (MicrosoftTeamsCreateCommands)", (): void => {
800
+ test.each(CREATE_COMMANDS)(
801
+ "'$name': one reply that says where to connect the account, and no form",
802
+ async (command: CreateCommandCase): Promise<void> => {
803
+ stubLinkTable(null);
804
+ const formLists: { mock: { calls: Array<unknown> } } =
805
+ command.stubFormLists();
806
+ const activity: JSONObject = commandActivity(command.name);
807
+ const turn: FakeTurn = createFakeTurn(activity);
808
+
809
+ await command.run(turn, activity);
810
+
811
+ expect(turn.replies).toEqual([connectYourAccount(command.purpose)]);
812
+ // Told before filling the form in: its lists were never read.
813
+ expect(formLists.mock.calls).toHaveLength(0);
814
+ expect(memberPropsSpy).not.toHaveBeenCalled();
815
+ expect(errorLog).not.toHaveBeenCalled();
816
+ },
817
+ );
818
+
819
+ test.each(CREATE_COMMANDS)(
820
+ "'$name' while the link table cannot be read: 'try again in a minute', logged as an error - never 'connect your account'",
821
+ async (command: CreateCommandCase): Promise<void> => {
822
+ const lookupError: Error = new Error(
823
+ "Connection terminated unexpectedly",
824
+ );
825
+ stubUnreadableLinkTable(lookupError);
826
+ const formLists: { mock: { calls: Array<unknown> } } =
827
+ command.stubFormLists();
828
+ const activity: JSONObject = commandActivity(command.name);
829
+ const turn: FakeTurn = createFakeTurn(activity);
830
+
831
+ await command.run(turn, activity);
832
+
833
+ expect(turn.replies).toEqual([
834
+ `Sorry, I couldn't open the form to create ${command.what} because OneUptime could not check your account just now. Please try again in a minute.`,
835
+ ]);
836
+ expect(formLists.mock.calls).toHaveLength(0);
837
+ expect(errorLog).toHaveBeenCalledWith(
838
+ LOOKUP_FAILED_LOG_LINE,
839
+ LOG_ATTRIBUTES,
840
+ );
841
+ const attributes: LogAttributes = { projectId: PROJECT_ID.toString() };
842
+ expect(errorLogCalls()).toStrictEqual([
843
+ [LOOKUP_FAILED_LOG_LINE, LOG_ATTRIBUTES],
844
+ [lookupError],
845
+ [
846
+ "Could not check the Microsoft Teams sender of a create command: Error: Connection terminated unexpectedly",
847
+ attributes,
848
+ ],
849
+ [lookupError, attributes],
850
+ ]);
851
+ },
852
+ );
853
+ });
854
+
855
+ describe("the create commands in a team channel or a group chat, where anyone there may submit the form", (): void => {
856
+ /*
857
+ * Whoever types the command need not be who fills the form in, so the
858
+ * sender is not looked up: a sender without a linked account still gets
859
+ * the form, for the channel. The personal-chat tests above show the check
860
+ * where the sender is the only one who can submit it.
861
+ */
862
+ interface SharedCreateCase {
863
+ command: CreateCommandCase;
864
+ where: SharedConversation;
865
+ // The conversation, as a test title names it.
866
+ place: string;
867
+ }
868
+
869
+ const SHARED_CREATE_CASES: ReadonlyArray<SharedCreateCase> =
870
+ CREATE_COMMANDS.flatMap(
871
+ (command: CreateCommandCase): Array<SharedCreateCase> => {
872
+ return [
873
+ { command: command, where: "channel", place: "team channel" },
874
+ { command: command, where: "groupChat", place: "group chat" },
875
+ ];
876
+ },
877
+ );
878
+
879
+ test.each(SHARED_CREATE_CASES)(
880
+ "'$command.name' in a $place, from a sender without a linked account: the form, and the sender is not looked up",
881
+ async (sharedCase: SharedCreateCase): Promise<void> => {
882
+ const { command, where } = sharedCase;
883
+ const linkTable: LinkTableSpy = stubLinkTable(null);
884
+ const assertCanCreate: SpyInstance<
885
+ typeof WorkspaceActionAuthorization.assertCanCreate
886
+ > = jest.spyOn(WorkspaceActionAuthorization, "assertCanCreate");
887
+ const formLists: CallRecorder = command.stubReadableFormLists();
888
+ const activity: JSONObject = inSharedConversation(
889
+ commandActivity(command.name),
890
+ where,
891
+ );
892
+ const turn: FakeTurn = createFakeTurn(activity);
893
+
894
+ await command.run(turn, activity);
895
+
896
+ // One reply: the form, with its submit button.
897
+ expect(turn.replies).toHaveLength(1);
898
+ expect(JSON.parse(turn.replies[0] || "{}") as JSONObject).toMatchObject(
899
+ {
900
+ attachments: [
901
+ {
902
+ contentType: MICROSOFT_TEAMS_ADAPTIVE_CARD_CONTENT_TYPE,
903
+ content: {
904
+ body: expect.arrayContaining([
905
+ expect.objectContaining({ text: command.formTitle }),
906
+ ]),
907
+ actions: [
908
+ expect.objectContaining({
909
+ data: expect.objectContaining({
910
+ action: command.submitAction,
911
+ }),
912
+ }),
913
+ ],
914
+ },
915
+ },
916
+ ],
917
+ },
918
+ );
919
+ expect(formLists.mock.calls).toHaveLength(1);
920
+
921
+ // Nobody was looked up: the submit checks whoever submits it.
922
+ expect(linkTable).not.toHaveBeenCalled();
923
+ expect(memberPropsSpy).not.toHaveBeenCalled();
924
+ expect(assertCanCreate).not.toHaveBeenCalled();
925
+ expect(errorLog).not.toHaveBeenCalled();
926
+ },
927
+ );
928
+
929
+ test.each(SHARED_CREATE_CASES)(
930
+ "the '$command.name' form submitted in a $place by someone without a linked account: where to connect it, and nothing is created",
931
+ async (sharedCase: SharedCreateCase): Promise<void> => {
932
+ const { command, where } = sharedCase;
933
+ stubLinkTable(null);
934
+ const create: CallRecorder = command.stubCreateThatMustNotRun();
935
+ const activity: JSONObject = inSharedConversation(
936
+ {
937
+ type: "message",
938
+ id: "1727611320123",
939
+ replyToId: "1727611300999",
940
+ value: { action: command.submitAction, ...command.submittedFields },
941
+ from: fromSender(),
942
+ recipient: { id: "28:oneuptime-bot" },
943
+ channelData: { tenant: { id: TENANT_ID } },
944
+ },
945
+ where,
946
+ );
947
+ const turn: FakeTurn = createFakeTurn(activity);
948
+
949
+ await MicrosoftTeamsUtil.handleBotInvokeActivity({
950
+ activity: activity,
951
+ turnContext: turn.turnContext,
952
+ });
953
+
954
+ expect(turn.replies).toEqual([
955
+ connectYourAccount("use OneUptime actions in Microsoft Teams"),
956
+ ]);
957
+ expect(create.mock.calls).toHaveLength(0);
958
+ expect(memberPropsSpy).not.toHaveBeenCalled();
959
+ expect(errorLog).not.toHaveBeenCalled();
960
+ },
961
+ );
962
+ });
963
+
964
+ describe("the AI assistant ('ask ...', through handleBotMessageActivity)", (): void => {
965
+ let aiEnabledSpy: SpyInstance<typeof AIService.isProjectAIEnabled>;
966
+
967
+ beforeEach((): void => {
968
+ // Recording the chat reads the database; it is not what this is about.
969
+ jest
970
+ .spyOn(
971
+ MicrosoftTeamsUtil as unknown as MicrosoftTeamsUtilInternals,
972
+ "captureChatFromBotActivity",
973
+ )
974
+ .mockResolvedValue(undefined);
975
+ // The first thing the assistant reads once it knows who is asking.
976
+ aiEnabledSpy = jest
977
+ .spyOn(AIService, "isProjectAIEnabled")
978
+ .mockResolvedValue(false);
979
+ });
980
+
981
+ /*
982
+ * Each question gets an activity id of its own: handleBotMessageActivity
983
+ * drops a second delivery of the same id, for ten minutes.
984
+ */
985
+ let lastAskActivityId: number = 1727611300100;
986
+
987
+ async function ask(): Promise<FakeTurn> {
988
+ lastAskActivityId += 1;
989
+ const activity: JSONObject = askActivity(String(lastAskActivityId));
990
+ const turn: FakeTurn = createFakeTurn(activity);
991
+
992
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
993
+ activity: activity,
994
+ turnContext: turn.turnContext,
995
+ });
996
+
997
+ return turn;
998
+ }
999
+
1000
+ test.each(UNLINKED)(
1001
+ "$name: one reply that says to connect the account; the assistant is not asked",
1002
+ async (unlinked: UnlinkedCase): Promise<void> => {
1003
+ stubLinkTable(unlinked.row());
1004
+
1005
+ const turn: FakeTurn = await ask();
1006
+
1007
+ expect(turn.replies).toHaveLength(1);
1008
+ expect(turn.replies[0]).toContain(CONNECT_YOUR_ACCOUNT);
1009
+ // The same guidance as a card button's, with the link to the setting.
1010
+ expect(turn.replies).toEqual([
1011
+ connectYourAccount("ask OneUptime questions from Microsoft Teams"),
1012
+ ]);
1013
+ expect(memberPropsSpy).not.toHaveBeenCalled();
1014
+ expect(aiEnabledSpy).not.toHaveBeenCalled();
1015
+ expect(errorLog).not.toHaveBeenCalled();
1016
+ },
1017
+ );
1018
+
1019
+ test("without a dashboard URL the reply still says where to connect the account", async (): Promise<void> => {
1020
+ stubLinkTable(null);
1021
+ jest
1022
+ .spyOn(DatabaseConfig, "getDashboardUrl")
1023
+ .mockRejectedValue(new Error("No host is configured."));
1024
+
1025
+ const turn: FakeTurn = await ask();
1026
+
1027
+ expect(turn.replies).toEqual([
1028
+ "To ask OneUptime questions from Microsoft Teams, first connect your Microsoft Teams account to OneUptime in OneUptime under User Settings → Microsoft Teams, then try again.",
1029
+ ]);
1030
+ expect(errorLog).not.toHaveBeenCalled();
1031
+ });
1032
+
1033
+ /*
1034
+ * Pinned a reported bug, now fixed: answerObservabilityQuestion
1035
+ * (MicrosoftTeams.ts) caught every error of the lookup and answered
1036
+ * "I couldn't find your OneUptime account. Please connect your Microsoft
1037
+ * Teams account in OneUptime User Settings before asking me questions.",
1038
+ * the very confusion MicrosoftTeamsAccountNotLinkedException exists to
1039
+ * prevent. Any one reply that does not send the user to connect an
1040
+ * account passes here; the tests below pin which reply it is.
1041
+ */
1042
+ test("a link table that cannot be read: one reply, which does not tell a linked user to connect the account they have connected", async (): Promise<void> => {
1043
+ stubUnreadableLinkTable(new Error("Connection terminated unexpectedly"));
1044
+
1045
+ const turn: FakeTurn = await ask();
1046
+
1047
+ expect(turn.replies).toHaveLength(1);
1048
+ expect(turn.replies[0]).not.toContain(CONNECT_YOUR_ACCOUNT);
1049
+ expect(memberPropsSpy).not.toHaveBeenCalled();
1050
+ expect(aiEnabledSpy).not.toHaveBeenCalled();
1051
+ expect(errorLog).toHaveBeenCalledWith(
1052
+ LOOKUP_FAILED_LOG_LINE,
1053
+ LOG_ATTRIBUTES,
1054
+ );
1055
+ });
1056
+
1057
+ /*
1058
+ * The lookup's error is not the assistant's to answer: it is rethrown,
1059
+ * and handleBotMessageActivity answers it as any failed message, once.
1060
+ */
1061
+ test.each(LOOKUP_FAILURES)(
1062
+ "$name: the message handler's one reply, and the failure logged by the command, not by the question",
1063
+ async (failure: LookupFailureCase): Promise<void> => {
1064
+ const lookupError: unknown = failure.error();
1065
+ stubUnreadableLinkTable(lookupError);
1066
+
1067
+ const turn: FakeTurn = await ask();
1068
+
1069
+ expect(turn.replies).toEqual([
1070
+ MicrosoftTeamsUtil.getUnexpectedErrorMessage(turn.activity),
1071
+ ]);
1072
+ expect(memberPropsSpy).not.toHaveBeenCalled();
1073
+ expect(aiEnabledSpy).not.toHaveBeenCalled();
1074
+
1075
+ const attributes: LogAttributes = { projectId: PROJECT_ID.toString() };
1076
+ expect(errorLogCalls()).toStrictEqual([
1077
+ // The lookup's own lines.
1078
+ [LOOKUP_FAILED_LOG_LINE, LOG_ATTRIBUTES],
1079
+ [lookupError],
1080
+ // The message handler's, under the reference the reply gives.
1081
+ [
1082
+ `Microsoft Teams message ${turn.activity["id"] as string} ("ask") failed: ${failure.described}`,
1083
+ attributes,
1084
+ ],
1085
+ [lookupError, attributes],
1086
+ ]);
1087
+ for (const call of errorLogCalls()) {
1088
+ expect(String(call[0])).not.toContain("checkout");
1089
+ }
1090
+ },
1091
+ );
1092
+ });
1093
+ });