@oneuptime/common 14.0.10 → 14.0.11

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 (837) hide show
  1. package/Models/DatabaseModels/AIConversation.ts +127 -1
  2. package/Models/DatabaseModels/CephCluster.ts +12 -0
  3. package/Models/DatabaseModels/CloudResource.ts +12 -0
  4. package/Models/DatabaseModels/DatabaseServer.ts +12 -0
  5. package/Models/DatabaseModels/DockerHost.ts +12 -0
  6. package/Models/DatabaseModels/DockerSwarmCluster.ts +12 -0
  7. package/Models/DatabaseModels/GlobalOidc.ts +0 -4
  8. package/Models/DatabaseModels/GlobalOidcProject.ts +0 -4
  9. package/Models/DatabaseModels/GlobalSso.ts +0 -4
  10. package/Models/DatabaseModels/GlobalSsoProject.ts +0 -4
  11. package/Models/DatabaseModels/Host.ts +12 -0
  12. package/Models/DatabaseModels/IncidentForm.ts +594 -0
  13. package/Models/DatabaseModels/IncidentFormSubmission.ts +283 -0
  14. package/Models/DatabaseModels/IncidentTemplate.ts +59 -0
  15. package/Models/DatabaseModels/Index.ts +14 -0
  16. package/Models/DatabaseModels/IoTFleet.ts +12 -0
  17. package/Models/DatabaseModels/KubernetesCluster.ts +12 -0
  18. package/Models/DatabaseModels/MessageQueue.ts +1102 -0
  19. package/Models/DatabaseModels/MessageQueueLabelRule.ts +553 -0
  20. package/Models/DatabaseModels/MessageQueueOwnerRule.ts +633 -0
  21. package/Models/DatabaseModels/MessageQueueOwnerTeam.ts +491 -0
  22. package/Models/DatabaseModels/MessageQueueOwnerUser.ts +490 -0
  23. package/Models/DatabaseModels/PodmanHost.ts +12 -0
  24. package/Models/DatabaseModels/Project.ts +59 -0
  25. package/Models/DatabaseModels/ProjectOidc.ts +0 -4
  26. package/Models/DatabaseModels/ProjectSso.ts +0 -4
  27. package/Models/DatabaseModels/ProxmoxCluster.ts +12 -0
  28. package/Models/DatabaseModels/RumApplication.ts +12 -0
  29. package/Models/DatabaseModels/ServerlessFunction.ts +12 -0
  30. package/Models/DatabaseModels/Service.ts +12 -0
  31. package/Models/DatabaseModels/StatusPageOidc.ts +0 -4
  32. package/Models/DatabaseModels/StatusPageSso.ts +0 -4
  33. package/Models/DatabaseModels/TeamComplianceSetting.ts +47 -2
  34. package/Models/DatabaseModels/VMwareVCenter.ts +12 -0
  35. package/Server/API/AIChatAPI.ts +13 -114
  36. package/Server/API/AIInvestigationAPI.ts +3 -3
  37. package/Server/API/AIInvestigationConversationAPI.ts +266 -0
  38. package/Server/API/IncidentFormAPI.ts +376 -0
  39. package/Server/API/ProjectOIDC.ts +16 -25
  40. package/Server/API/ProjectSSO.ts +16 -24
  41. package/Server/API/StatusPageAPI.ts +42 -77
  42. package/Server/Enterprise/EnterpriseEdition.ts +93 -28
  43. package/Server/Enterprise/EnterpriseFeature.ts +25 -3
  44. package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +1 -1
  45. package/Server/Enterprise/EnterpriseServerModule.ts +4 -1
  46. package/Server/EnvironmentConfig.ts +1 -1
  47. package/Server/Infrastructure/Postgres/SchemaMigrations/1796400000000-AddIncidentForms.ts +129 -0
  48. package/Server/Infrastructure/Postgres/SchemaMigrations/1796500000000-AddTeamComplianceRuleNotificationChannels.ts +52 -0
  49. package/Server/Infrastructure/Postgres/SchemaMigrations/1796600000000-AddAIInvestigationConversationAndTimeLimits.ts +75 -0
  50. package/Server/Infrastructure/Postgres/SchemaMigrations/1796700000000-AddMessageQueueTables.ts +477 -0
  51. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  52. package/Server/Middleware/IncidentFormRateLimit.ts +847 -0
  53. package/Server/Middleware/UserAuthorization.ts +16 -38
  54. package/Server/Services/AIService.ts +10 -13
  55. package/Server/Services/IncidentCustomFieldService.ts +112 -2
  56. package/Server/Services/IncidentEpisodeMemberService.ts +24 -4
  57. package/Server/Services/IncidentEpisodeService.ts +10 -1
  58. package/Server/Services/IncidentFormService.ts +1566 -0
  59. package/Server/Services/IncidentFormSubmissionService.ts +97 -0
  60. package/Server/Services/IncidentGroupingEngineService.ts +45 -8
  61. package/Server/Services/IncidentService.ts +123 -5
  62. package/Server/Services/IncidentTemplateService.ts +22 -0
  63. package/Server/Services/Index.ts +14 -0
  64. package/Server/Services/MessageQueueLabelRuleEngineService.ts +321 -0
  65. package/Server/Services/MessageQueueLabelRuleService.ts +14 -0
  66. package/Server/Services/MessageQueueOwnerRuleEngineService.ts +412 -0
  67. package/Server/Services/MessageQueueOwnerRuleService.ts +14 -0
  68. package/Server/Services/MessageQueueOwnerTeamService.ts +62 -0
  69. package/Server/Services/MessageQueueOwnerUserService.ts +62 -0
  70. package/Server/Services/MessageQueueService.ts +1463 -0
  71. package/Server/Services/OpenTelemetryIngestService.ts +37 -4
  72. package/Server/Services/ProjectService.ts +0 -59
  73. package/Server/Services/StatusPagePrivateUserService.ts +1 -4
  74. package/Server/Services/StatusPageService.ts +1 -51
  75. package/Server/Services/TeamComplianceSettingService.ts +163 -42
  76. package/Server/Services/UserNotificationRuleService.ts +8 -1
  77. package/Server/Services/WorkspaceNotificationSummaryService.ts +12 -3
  78. package/Server/Types/Database/Permissions/CreatePermission.ts +17 -3
  79. package/Server/Types/Database/Permissions/EditionPermission.ts +140 -38
  80. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +16 -0
  81. package/Server/Types/Database/Permissions/TablePermission.ts +3 -3
  82. package/Server/Types/Database/Permissions/UpdatePermission.ts +20 -6
  83. package/Server/Utils/AI/Chat/AgentContextCompactor.ts +200 -0
  84. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +307 -48
  85. package/Server/Utils/AI/Chat/ChatRunCancellation.ts +156 -0
  86. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +78 -18
  87. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +1 -1
  88. package/Server/Utils/AI/Chat/ToolOutputPager.ts +322 -0
  89. package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +88 -30
  90. package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +13 -1
  91. package/Server/Utils/AI/CodeFix/CodeFixReadiness.ts +0 -32
  92. package/Server/Utils/AI/CodeFix/FixRunBudget.ts +27 -25
  93. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +7 -1
  94. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +34 -14
  95. package/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.ts +69 -24
  96. package/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.ts +9 -0
  97. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +53 -10
  98. package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +27 -10
  99. package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +27 -10
  100. package/Server/Utils/AI/SRE/InvestigationQueue.ts +16 -19
  101. package/Server/Utils/AI/SRE/InvestigationThread.ts +646 -0
  102. package/Server/Utils/AI/SRE/InvestigationThreadService.ts +715 -0
  103. package/Server/Utils/AI/Toolbox/Serializer.ts +10 -3
  104. package/Server/Utils/EditionEnforcement.ts +19 -202
  105. package/Server/Utils/LLM/LLMService.ts +259 -63
  106. package/Server/Utils/RelaxedScimTeamLocksReport.ts +245 -0
  107. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +25 -0
  108. package/Server/Utils/SameOriginRequest.ts +203 -0
  109. package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +2 -0
  110. package/Server/Utils/Telemetry/EntityRegistry.ts +4 -1
  111. package/Server/Utils/Telemetry/MessageQueueDiscovery.ts +1402 -0
  112. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +4 -1
  113. package/Server/Utils/VendorAssets.ts +3 -3
  114. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +3 -1
  115. package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +3 -1
  116. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +10 -1
  117. package/Tests/App/Accounts/IncidentFormPage.test.tsx +2036 -0
  118. package/Tests/App/Accounts/IncidentFormServerMessages.test.ts +119 -0
  119. package/Tests/App/Accounts/IncidentFormUtils.test.ts +1157 -0
  120. package/Tests/App/Accounts/LoginPasskey.test.tsx +6 -4
  121. package/Tests/App/Accounts/LoginSso.test.tsx +724 -0
  122. package/Tests/App/AdminDashboard/AdminAuthenticationRequireSso.test.tsx +338 -0
  123. package/Tests/App/AdminDashboard/AdminAuthenticationSsoCardLocales.test.tsx +331 -0
  124. package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +47 -33
  125. package/Tests/App/AdminDashboard/GlobalSsoOidcPages.test.tsx +1021 -0
  126. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +876 -78
  127. package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +20 -3
  128. package/Tests/App/Dashboard/CephProxmoxVMwareDocumentationCards.test.tsx +359 -0
  129. package/Tests/App/Dashboard/CephSetupGuide.test.ts +961 -0
  130. package/Tests/App/Dashboard/CloudDocumentationCard.test.tsx +381 -95
  131. package/Tests/App/Dashboard/CloudSetupGuide.test.ts +1463 -0
  132. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +174 -12
  133. package/Tests/App/Dashboard/ContainerSetupGuideCards.test.tsx +345 -0
  134. package/Tests/App/Dashboard/CoreListEmptyStates.test.tsx +501 -0
  135. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +0 -1
  136. package/Tests/App/Dashboard/DatabaseDocumentationCard.test.tsx +400 -0
  137. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +5 -0
  138. package/Tests/App/Dashboard/DatabaseSetupGuide.test.ts +1869 -0
  139. package/Tests/App/Dashboard/DatabaseTelemetryScope.test.ts +44 -0
  140. package/Tests/App/Dashboard/DatabaseTelemetryTabs.test.tsx +50 -9
  141. package/Tests/App/Dashboard/DockerSetupGuide.test.ts +899 -0
  142. package/Tests/App/Dashboard/DockerSwarmSetupGuide.test.ts +964 -0
  143. package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +141 -7
  144. package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +52 -39
  145. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +7 -1
  146. package/Tests/App/Dashboard/GettingStartedTaskRoutes.test.tsx +331 -0
  147. package/Tests/App/Dashboard/HomeOverviewStatsFirstRun.test.tsx +925 -0
  148. package/Tests/App/Dashboard/HostDocumentationCard.test.tsx +225 -0
  149. package/Tests/App/Dashboard/HostSetupGuide.test.ts +1183 -0
  150. package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +69 -45
  151. package/Tests/App/Dashboard/IncidentCreateTemplateCustomFieldSettings.test.tsx +745 -0
  152. package/Tests/App/Dashboard/IncidentCreateTemplateOwners.test.tsx +414 -0
  153. package/Tests/App/Dashboard/IncidentCustomFieldCreateSettingsForm.test.ts +1042 -0
  154. package/Tests/App/Dashboard/IncidentCustomFieldSettingsCard.test.tsx +1816 -0
  155. package/Tests/App/Dashboard/IncidentFormPlan.test.ts +89 -0
  156. package/Tests/App/Dashboard/IncidentFormShareLinkCard.test.tsx +846 -0
  157. package/Tests/App/Dashboard/IncidentFormsPages.test.tsx +1324 -0
  158. package/Tests/App/Dashboard/IncidentTemplateCustomFieldSettings.test.tsx +630 -0
  159. package/Tests/App/Dashboard/IncidentsSideMenu.test.tsx +13 -0
  160. package/Tests/App/Dashboard/IncomingEmailMonitorLinkVerifyHint.test.tsx +104 -0
  161. package/Tests/App/Dashboard/InvestigationConversation.test.tsx +1107 -0
  162. package/Tests/App/Dashboard/InvestigationConversationData.test.ts +546 -0
  163. package/Tests/App/Dashboard/InvestigationEvidenceList.test.tsx +170 -5
  164. package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +207 -2
  165. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +729 -34
  166. package/Tests/App/Dashboard/InvestigationPanelStatus.test.tsx +9 -21
  167. package/Tests/App/Dashboard/InvestigationReportView.test.tsx +274 -37
  168. package/Tests/App/Dashboard/InvestigationStatusBadge.test.tsx +130 -0
  169. package/Tests/App/Dashboard/IoTDocumentationCard.test.tsx +218 -0
  170. package/Tests/App/Dashboard/IoTSetupGuide.test.ts +1124 -0
  171. package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +20 -6
  172. package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +107 -83
  173. package/Tests/App/Dashboard/KubernetesSetupGuide.test.ts +787 -0
  174. package/Tests/App/Dashboard/MessageQueueBreadcrumbs.test.tsx +228 -0
  175. package/Tests/App/Dashboard/MessageQueueBrokerHealth.test.tsx +1538 -0
  176. package/Tests/App/Dashboard/MessageQueueBrokerHealthMetricChart.test.tsx +969 -0
  177. package/Tests/App/Dashboard/MessageQueueBrokerHealthReads.test.ts +889 -0
  178. package/Tests/App/Dashboard/MessageQueueBrokerMetricsReach.test.ts +236 -0
  179. package/Tests/App/Dashboard/MessageQueueDocumentationCard.test.tsx +479 -0
  180. package/Tests/App/Dashboard/MessageQueueListPages.test.tsx +1232 -0
  181. package/Tests/App/Dashboard/MessageQueueMetricDescriptions.test.ts +244 -0
  182. package/Tests/App/Dashboard/MessageQueueMonitorLink.test.ts +952 -0
  183. package/Tests/App/Dashboard/MessageQueueMonitorLinkMonitorCreate.test.tsx +859 -0
  184. package/Tests/App/Dashboard/MessageQueueOverview.test.tsx +1773 -0
  185. package/Tests/App/Dashboard/MessageQueueOverviewPresentation.test.ts +196 -0
  186. package/Tests/App/Dashboard/MessageQueuePresentation.test.ts +799 -0
  187. package/Tests/App/Dashboard/MessageQueueProducersConsumers.test.tsx +680 -0
  188. package/Tests/App/Dashboard/MessageQueueRoutes.test.tsx +383 -0
  189. package/Tests/App/Dashboard/MessageQueueRulePages.test.tsx +278 -0
  190. package/Tests/App/Dashboard/MessageQueueSetupGuide.test.ts +2778 -0
  191. package/Tests/App/Dashboard/MessageQueueSideMenus.test.tsx +273 -0
  192. package/Tests/App/Dashboard/MessageQueueSpanPopulations.test.ts +930 -0
  193. package/Tests/App/Dashboard/MessageQueueSpanStore.ts +351 -0
  194. package/Tests/App/Dashboard/MessageQueueTelemetryScope.test.ts +375 -0
  195. package/Tests/App/Dashboard/MessageQueueTelemetryScopeTabs.test.tsx +668 -0
  196. package/Tests/App/Dashboard/MessageQueueViewPages.test.tsx +685 -0
  197. package/Tests/App/Dashboard/MonitorAndHomeNotOperationalMenus.test.tsx +342 -0
  198. package/Tests/App/Dashboard/MonitorCreateFirstRunDefaults.test.tsx +623 -0
  199. package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +442 -0
  200. package/Tests/App/Dashboard/MonitorLogsPageViewSummary.test.tsx +814 -0
  201. package/Tests/App/Dashboard/MonitorSetupCardBodies.test.tsx +246 -0
  202. package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +8 -0
  203. package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +27 -12
  204. package/Tests/App/Dashboard/PlanGatedPage.test.tsx +292 -0
  205. package/Tests/App/Dashboard/PodmanSetupGuide.test.ts +948 -0
  206. package/Tests/App/Dashboard/ProductOrientation.test.tsx +5 -4
  207. package/Tests/App/Dashboard/ProductSettingsSectionCollapsed.test.tsx +266 -0
  208. package/Tests/App/Dashboard/ProxmoxSetupGuide.test.ts +1318 -0
  209. package/Tests/App/Dashboard/ResourceAiAgentAttentionServerParity.test.ts +555 -0
  210. package/Tests/App/Dashboard/ResourceAiAgentPage.test.tsx +298 -9
  211. package/Tests/App/Dashboard/ResourceAiAgentStatus.test.ts +453 -0
  212. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +16 -0
  213. package/Tests/App/Dashboard/ResourceConnectionGuideCard.test.tsx +444 -0
  214. package/Tests/App/Dashboard/ResourceConnectionGuideOverviewsInfra.test.tsx +798 -0
  215. package/Tests/App/Dashboard/ResourceConnectionGuideOverviewsPlatforms.test.tsx +717 -0
  216. package/Tests/App/Dashboard/ResourceConnectionGuides.test.ts +981 -0
  217. package/Tests/App/Dashboard/ResourceConnectionState.test.ts +132 -0
  218. package/Tests/App/Dashboard/RumDocumentationCard.test.tsx +293 -0
  219. package/Tests/App/Dashboard/RumSessionReplayRetentionSettings.test.tsx +467 -0
  220. package/Tests/App/Dashboard/RumSetupGuide.test.ts +758 -0
  221. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +25 -0
  222. package/Tests/App/Dashboard/ServerlessDocumentationCard.test.tsx +254 -0
  223. package/Tests/App/Dashboard/ServerlessSetupGuide.test.ts +1074 -0
  224. package/Tests/App/Dashboard/SetupGuide.test.ts +246 -0
  225. package/Tests/App/Dashboard/SetupGuideCard.test.tsx +756 -0
  226. package/Tests/App/Dashboard/SloListSideMenu.test.tsx +120 -0
  227. package/Tests/App/Dashboard/SsoPages.test.tsx +833 -0
  228. package/Tests/App/Dashboard/TelemetryDocumentation.test.tsx +253 -0
  229. package/Tests/App/Dashboard/TelemetryRetentionShells.test.tsx +730 -0
  230. package/Tests/App/Dashboard/VMwareSetupGuide.test.ts +1033 -0
  231. package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +87 -7
  232. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +28 -9
  233. package/Tests/Models/DatabaseModels/IncidentFormModel.test.ts +1250 -0
  234. package/Tests/Models/DatabaseModels/MessageQueueModels.test.ts +1983 -0
  235. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  236. package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +190 -14
  237. package/Tests/Models/IncidentTemplateCustomFieldSettingsColumn.test.ts +291 -0
  238. package/Tests/Server/API/AIInvestigationConversationAPI.test.ts +412 -0
  239. package/Tests/Server/API/GlobalConfigLicense.test.ts +38 -9
  240. package/Tests/Server/API/IncidentFormAPI.test.ts +942 -0
  241. package/Tests/Server/API/IncidentFormPublicRoutes.test.ts +1653 -0
  242. package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -1
  243. package/Tests/Server/API/{SsoProviderListingsEdition.test.ts → SsoProviderListings.test.ts} +164 -110
  244. package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +76 -54
  245. package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +125 -42
  246. package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +36 -10
  247. package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +51 -9
  248. package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +13 -8
  249. package/Tests/Server/Enterprise/SsoInCommunityEdition.test.ts +540 -0
  250. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  251. package/Tests/Server/Infrastructure/Postgres/AddIncidentFormsMigration.test.ts +438 -0
  252. package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleNotificationChannelsMigration.test.ts +255 -0
  253. package/Tests/Server/Infrastructure/Postgres/MessageQueueTablesMigration.test.ts +637 -0
  254. package/Tests/Server/Middleware/IncidentFormRateLimit.test.ts +1630 -0
  255. package/Tests/Server/Middleware/UserAuthorization.test.ts +16 -17
  256. package/Tests/Server/Middleware/UserAuthorizationSsoEnforcement.test.ts +544 -0
  257. package/Tests/Server/Services/AIServiceDailyBudget.test.ts +37 -8
  258. package/Tests/Server/Services/IncidentCreatedFeedTitle.test.ts +212 -0
  259. package/Tests/Server/Services/IncidentCustomFieldDeleteFormQuestions.test.ts +297 -0
  260. package/Tests/Server/Services/IncidentFormOwnersCreatedNotification.test.ts +497 -0
  261. package/Tests/Server/Services/IncidentFormPostgres.test.ts +958 -0
  262. package/Tests/Server/Services/IncidentFormPublicForm.test.ts +1260 -0
  263. package/Tests/Server/Services/IncidentFormPublicSubmission.test.ts +2520 -0
  264. package/Tests/Server/Services/IncidentFormRegistration.test.ts +159 -0
  265. package/Tests/Server/Services/IncidentFormService.test.ts +663 -0
  266. package/Tests/Server/Services/IncidentFormSubmissionService.test.ts +430 -0
  267. package/Tests/Server/Services/IncidentFormSubmissionTemplateBranch.test.ts +473 -0
  268. package/Tests/Server/Services/IncidentGroupingTemplateSubstitution.test.ts +81 -0
  269. package/Tests/Server/Services/IncidentTemplateCustomFieldSettings.test.ts +262 -0
  270. package/Tests/Server/Services/IncidentTemplateOwnersOnDeclare.test.ts +566 -0
  271. package/Tests/Server/Services/IncidentTitleMarkdownSinks.test.ts +640 -0
  272. package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +3 -3
  273. package/Tests/Server/Services/MessageQueueOwnerServices.test.ts +440 -0
  274. package/Tests/Server/Services/MessageQueueRegistries.test.ts +577 -0
  275. package/Tests/Server/Services/MessageQueueRuleEngines.test.ts +900 -0
  276. package/Tests/Server/Services/MessageQueueService.test.ts +2210 -0
  277. package/Tests/Server/Services/MessageQueueSqlPostgres.test.ts +1271 -0
  278. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +28 -1
  279. package/Tests/Server/Services/OpenTelemetryRetentionOverrideEdition.test.ts +474 -0
  280. package/Tests/Server/Services/OpenTelemetryServiceResolutionCache.test.ts +21 -0
  281. package/Tests/Server/Services/OwnerRowUniqueness.test.ts +3 -0
  282. package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +0 -1
  283. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +56 -6
  284. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -1
  285. package/Tests/Server/Services/SsoRequirementReads.test.ts +454 -0
  286. package/Tests/Server/Services/SsoRequirementWrites.test.ts +529 -0
  287. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +3 -3
  288. package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +107 -89
  289. package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +1068 -176
  290. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +1962 -200
  291. package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +654 -297
  292. package/Tests/Server/Types/Database/Permissions/RetentionOverrideColumns.test.ts +784 -0
  293. package/Tests/Server/Types/Database/Permissions/SsoScalePlanGate.test.ts +418 -0
  294. package/Tests/Server/Utils/AI/AIAlertGating.test.ts +0 -1
  295. package/Tests/Server/Utils/AI/AIInvestigationEngineRunLimits.test.ts +136 -0
  296. package/Tests/Server/Utils/AI/AIInvestigationQueue.test.ts +15 -14
  297. package/Tests/Server/Utils/AI/AgentContextCompactor.test.ts +351 -0
  298. package/Tests/Server/Utils/AI/ChatAgentRunnerSharedThread.test.ts +842 -0
  299. package/Tests/Server/Utils/AI/ChatRunCancellation.test.ts +195 -0
  300. package/Tests/Server/Utils/AI/CodeFixReadiness.test.ts +10 -57
  301. package/Tests/Server/Utils/AI/FixRunBudget.test.ts +20 -24
  302. package/Tests/Server/Utils/AI/Insights/InvestigationQueueInsightSubject.test.ts +7 -30
  303. package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +225 -29
  304. package/Tests/Server/Utils/AI/InvestigationInfrastructureAccessWiring.test.ts +266 -25
  305. package/Tests/Server/Utils/AI/InvestigationThread.test.ts +580 -0
  306. package/Tests/Server/Utils/AI/InvestigationThreadService.test.ts +801 -0
  307. package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +525 -43
  308. package/Tests/Server/Utils/AI/KubectlJobRunner.test.ts +4 -2
  309. package/Tests/Server/Utils/AI/LLMServiceUnreachableProvider.test.ts +435 -0
  310. package/Tests/Server/Utils/AI/ObservabilityAssistantLimits.test.ts +462 -0
  311. package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +14 -25
  312. package/Tests/Server/Utils/AI/RemediationExecutionRunnerResourceMode.test.ts +2 -0
  313. package/Tests/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.test.ts +297 -15
  314. package/Tests/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.test.ts +10 -6
  315. package/Tests/Server/Utils/AI/ToolOutputPager.test.ts +382 -0
  316. package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +23 -4
  317. package/Tests/Server/Utils/AI/Toolbox/LogTools.test.ts +25 -6
  318. package/Tests/Server/Utils/AI/Toolbox/Serializer.test.ts +14 -3
  319. package/Tests/Server/Utils/EditionEnforcement.test.ts +107 -147
  320. package/Tests/Server/Utils/{CommunityEditionSsoReport.test.ts → RelaxedScimTeamLocksReport.test.ts} +212 -208
  321. package/Tests/Server/Utils/SameOriginRequest.test.ts +372 -0
  322. package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +20 -0
  323. package/Tests/Server/Utils/Telemetry/MessageQueueDiscovery.test.ts +2590 -0
  324. package/Tests/Server/Utils/Telemetry/MessageQueueSpanQueryTwin.ts +246 -0
  325. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +23 -0
  326. package/Tests/Types/AI/AIAgentRunLimits.test.ts +255 -0
  327. package/Tests/Types/Code/JsonSyntax.test.ts +1300 -0
  328. package/Tests/Types/CustomField/CustomFieldCreateSettings.test.ts +1113 -0
  329. package/Tests/Types/CustomField/CustomFieldMappingCatalog.test.ts +91 -0
  330. package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +30 -0
  331. package/Tests/Types/Incident/IncidentFormIpAllowlist.test.ts +227 -0
  332. package/Tests/Types/Incident/IncidentFormPublic.test.ts +1916 -0
  333. package/Tests/Types/MessageQueue/MessageQueueEntityKey.test.ts +916 -0
  334. package/Tests/Types/MessageQueue/MessageQueueIdentity.test.ts +567 -0
  335. package/Tests/Types/MessageQueue/MessageQueueManualIdentity.test.ts +686 -0
  336. package/Tests/Types/MessageQueue/MessageQueueMetricCatalog.test.ts +1726 -0
  337. package/Tests/Types/MessageQueue/MessageQueueMetricResolution.test.ts +1758 -0
  338. package/Tests/Types/MessageQueue/MessagingInstrumentationFixtures.test.ts +4686 -0
  339. package/Tests/Types/MessageQueue/MessagingSystem.test.ts +840 -0
  340. package/Tests/Types/MessageQueue/MessagingTelemetryFixtures.ts +3287 -0
  341. package/Tests/Types/MessageQueue/MessagingTelemetryResolver.test.ts +2966 -0
  342. package/Tests/Types/Monitor/MessageQueueAlertTemplates.test.ts +5401 -0
  343. package/Tests/Types/Monitor/MetricEngineFold.ts +154 -0
  344. package/Tests/Types/ResourceAiAgent/ResourceAiAccess.test.ts +5 -3
  345. package/Tests/Types/Team/ComplianceRule.test.ts +588 -37
  346. package/Tests/Types/Team/TeamComplianceStatus.test.ts +64 -11
  347. package/Tests/Types/XML.test.ts +51 -0
  348. package/Tests/UI/Components/CodeEditor/CodeEditor.test.tsx +2362 -0
  349. package/Tests/UI/Components/CodeEditor/CodeEditorCommands.test.ts +810 -0
  350. package/Tests/UI/Components/CodeEditor/CodeEditorHighlight.test.ts +338 -0
  351. package/Tests/UI/Components/CodeEditor/CodeEditorLanguages.test.ts +222 -0
  352. package/Tests/UI/Components/CodeEditor/CodeEditorStyles.test.ts +807 -0
  353. package/Tests/UI/Components/CodeEditor/NoMonaco.test.ts +245 -0
  354. package/Tests/UI/Components/CodeEditor/YamlEditor.test.tsx +729 -0
  355. package/Tests/UI/Components/CopyTextButton.test.tsx +76 -0
  356. package/Tests/UI/Components/EditionLabelLapseCopy.ts +51 -14
  357. package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +131 -23
  358. package/Tests/UI/Components/ErrorMessageAction.test.tsx +142 -0
  359. package/Tests/UI/Components/FacetSidebarInScope.test.tsx +459 -0
  360. package/Tests/UI/Components/FacetValuesInScope.test.ts +270 -0
  361. package/Tests/UI/Components/Forms/FormFieldCodeEditors.test.tsx +294 -0
  362. package/Tests/UI/Components/Forms/FormFieldMarkdown.test.tsx +126 -0
  363. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +22 -75
  364. package/Tests/UI/Components/Forms/ModelFormOnBeforeCreateMiscData.test.tsx +254 -0
  365. package/Tests/UI/Components/Icon.test.tsx +32 -0
  366. package/Tests/UI/Components/Input.test.tsx +31 -0
  367. package/Tests/UI/Components/LockedFilterChipScope.test.tsx +501 -0
  368. package/Tests/UI/Components/MarkdownConverters.test.ts +253 -1
  369. package/Tests/UI/Components/MarkdownEditor.test.tsx +2785 -2
  370. package/Tests/UI/Components/MarkdownEditorHistory.test.ts +243 -0
  371. package/Tests/UI/Components/MarkdownListEditing.test.ts +1118 -0
  372. package/Tests/UI/Components/MarkdownPaste.test.ts +1180 -0
  373. package/Tests/UI/Components/MarkdownViewerCopy.test.tsx +466 -0
  374. package/Tests/UI/Components/MarkdownVisualEditing.test.ts +1089 -0
  375. package/Tests/UI/Components/ModelTable/BaseModelTableEmptyStateAction.test.tsx +623 -0
  376. package/Tests/UI/Components/ResetObjectID.test.tsx +282 -0
  377. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  378. package/Tests/UI/Components/Table/EmptyTableMessage.test.ts +439 -0
  379. package/Tests/UI/Components/Table/TableEmptyState.test.tsx +352 -0
  380. package/Tests/UI/Components/TableRowMobileTitles.test.tsx +78 -0
  381. package/Tests/UI/Components/TextArea.test.tsx +19 -0
  382. package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +12 -29
  383. package/Tests/UI/Components/fixtures/MarkdownPasteFixtures.ts +460 -0
  384. package/Tests/UI/EsbuildConfig.test.ts +115 -39
  385. package/Tests/UI/EsbuildEnterpriseAlias.test.ts +4 -4
  386. package/Tests/UI/Monitor/MonitorLogSummaryRender.test.tsx +377 -0
  387. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +81 -1
  388. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +17 -0
  389. package/Tests/Utils/LabelRuleImportExport.test.ts +5 -0
  390. package/Tests/Utils/Markdown/DashboardMarkdownRenderer.ts +81 -0
  391. package/Tests/Utils/Markdown/UntrustedMarkdown.test.ts +1339 -0
  392. package/Tests/Utils/Monitor/MonitorLogSummaryUtil.test.ts +862 -0
  393. package/Types/AI/AIAgentRunLimits.ts +146 -0
  394. package/Types/Code/CodeType.ts +2 -0
  395. package/Types/Code/JsonSyntax.ts +923 -0
  396. package/Types/CustomField/CustomFieldCreateSettings.ts +549 -0
  397. package/Types/CustomField/CustomFieldMappingCatalog.ts +77 -0
  398. package/Types/CustomField/CustomFieldValueValidator.ts +10 -7
  399. package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +1 -1
  400. package/Types/Icon/IconProp.ts +2 -0
  401. package/Types/Incident/IncidentFormIpAllowlist.ts +206 -0
  402. package/Types/Incident/IncidentFormPublic.ts +1145 -0
  403. package/Types/Kubernetes/KubernetesClusterAiAccess.ts +10 -3
  404. package/Types/MessageQueue/MessageQueueIdentity.ts +256 -0
  405. package/Types/MessageQueue/MessageQueueManualIdentity.ts +325 -0
  406. package/Types/MessageQueue/MessageQueueMetricCatalog.ts +1357 -0
  407. package/Types/MessageQueue/MessagingSystem.ts +518 -0
  408. package/Types/MessageQueue/MessagingTelemetryResolver.ts +2311 -0
  409. package/Types/Monitor/MessageQueueAlertTemplates.ts +2128 -0
  410. package/Types/Permission.ts +310 -0
  411. package/Types/ResourceAiAgent/ResourceAiAccess.ts +10 -4
  412. package/Types/Rules/RuleCriteriaFieldRegistry.ts +12 -0
  413. package/Types/Rules/RuleRun.ts +4 -0
  414. package/Types/Team/ComplianceNotificationChannel.ts +4 -3
  415. package/Types/Team/ComplianceRule.ts +71 -18
  416. package/Types/Team/ComplianceRuleType.ts +2 -2
  417. package/Types/Team/TeamComplianceStatus.ts +6 -2
  418. package/Types/Telemetry/EntityType.ts +16 -0
  419. package/Types/Telemetry/LockedFilterDetail.ts +22 -0
  420. package/Types/XML.ts +26 -0
  421. package/UI/Components/CodeEditor/CodeEditor.css +379 -0
  422. package/UI/Components/CodeEditor/CodeEditor.tsx +1128 -294
  423. package/UI/Components/CodeEditor/CodeEditorCommands.ts +774 -0
  424. package/UI/Components/CodeEditor/CodeEditorHighlight.ts +106 -0
  425. package/UI/Components/CodeEditor/CodeEditorLanguages.ts +164 -0
  426. package/UI/Components/CodeEditor/YamlEditor.tsx +28 -326
  427. package/UI/Components/CopyTextButton/CopyTextButton.tsx +5 -1
  428. package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +6 -25
  429. package/UI/Components/EditionLabel/EditionLabel.tsx +14 -13
  430. package/UI/Components/ErrorMessage/ErrorMessage.tsx +11 -1
  431. package/UI/Components/Forms/Fields/FormField.tsx +8 -0
  432. package/UI/Components/Forms/Types/Field.ts +8 -0
  433. package/UI/Components/Icon/Icon.tsx +18 -0
  434. package/UI/Components/Input/Input.tsx +11 -4
  435. package/UI/Components/LogsViewer/LogsViewer.tsx +4 -0
  436. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +21 -3
  437. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  438. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +877 -79
  439. package/UI/Components/Markdown.tsx/MarkdownEditorHistory.ts +284 -0
  440. package/UI/Components/Markdown.tsx/MarkdownListEditing.ts +1362 -0
  441. package/UI/Components/Markdown.tsx/MarkdownPaste.ts +1326 -0
  442. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +106 -10
  443. package/UI/Components/Markdown.tsx/MarkdownVisualEditing.ts +1032 -0
  444. package/UI/Components/ModelTable/BaseModelTable.tsx +85 -7
  445. package/UI/Components/ResetObjectID/ResetObjectID.tsx +35 -9
  446. package/UI/Components/Table/EmptyTableMessage.ts +81 -0
  447. package/UI/Components/Table/Table.tsx +50 -21
  448. package/UI/Components/Table/TableRow.tsx +9 -1
  449. package/UI/Components/TelemetryViewer/FacetVisibility.ts +97 -2
  450. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +13 -0
  451. package/UI/Components/TelemetryViewer/components/LockedFilterChip.tsx +214 -7
  452. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +21 -3
  453. package/UI/Components/TextArea/TextArea.tsx +9 -2
  454. package/UI/Components/Workflow/ModelFieldPicker.tsx +10 -12
  455. package/UI/Styles/Theme.css +8 -0
  456. package/UI/esbuild-config.js +2 -32
  457. package/Utils/LabelRuleImportExport.ts +2 -0
  458. package/Utils/Markdown/UntrustedMarkdown.ts +1513 -0
  459. package/Utils/Monitor/IncomingEmailMonitorRequestUtil.ts +41 -0
  460. package/Utils/Monitor/MonitorLogSummaryUtil.ts +212 -0
  461. package/Utils/Telemetry/EntityKey.ts +48 -0
  462. package/build/dist/Models/DatabaseModels/AIConversation.js +125 -1
  463. package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -1
  464. package/build/dist/Models/DatabaseModels/CephCluster.js +12 -0
  465. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  466. package/build/dist/Models/DatabaseModels/CloudResource.js +12 -0
  467. package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
  468. package/build/dist/Models/DatabaseModels/DatabaseServer.js +12 -0
  469. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -1
  470. package/build/dist/Models/DatabaseModels/DockerHost.js +12 -0
  471. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  472. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +12 -0
  473. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
  474. package/build/dist/Models/DatabaseModels/GlobalOidc.js +0 -4
  475. package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
  476. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +0 -4
  477. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -1
  478. package/build/dist/Models/DatabaseModels/GlobalSso.js +0 -4
  479. package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
  480. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +0 -4
  481. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -1
  482. package/build/dist/Models/DatabaseModels/Host.js +12 -0
  483. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  484. package/build/dist/Models/DatabaseModels/IncidentForm.js +605 -0
  485. package/build/dist/Models/DatabaseModels/IncidentForm.js.map +1 -0
  486. package/build/dist/Models/DatabaseModels/IncidentFormSubmission.js +299 -0
  487. package/build/dist/Models/DatabaseModels/IncidentFormSubmission.js.map +1 -0
  488. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +60 -0
  489. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  490. package/build/dist/Models/DatabaseModels/Index.js +14 -0
  491. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  492. package/build/dist/Models/DatabaseModels/IoTFleet.js +12 -0
  493. package/build/dist/Models/DatabaseModels/IoTFleet.js.map +1 -1
  494. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +12 -0
  495. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  496. package/build/dist/Models/DatabaseModels/MessageQueue.js +1106 -0
  497. package/build/dist/Models/DatabaseModels/MessageQueue.js.map +1 -0
  498. package/build/dist/Models/DatabaseModels/MessageQueueLabelRule.js +562 -0
  499. package/build/dist/Models/DatabaseModels/MessageQueueLabelRule.js.map +1 -0
  500. package/build/dist/Models/DatabaseModels/MessageQueueOwnerRule.js +643 -0
  501. package/build/dist/Models/DatabaseModels/MessageQueueOwnerRule.js.map +1 -0
  502. package/build/dist/Models/DatabaseModels/MessageQueueOwnerTeam.js +506 -0
  503. package/build/dist/Models/DatabaseModels/MessageQueueOwnerTeam.js.map +1 -0
  504. package/build/dist/Models/DatabaseModels/MessageQueueOwnerUser.js +505 -0
  505. package/build/dist/Models/DatabaseModels/MessageQueueOwnerUser.js.map +1 -0
  506. package/build/dist/Models/DatabaseModels/PodmanHost.js +12 -0
  507. package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
  508. package/build/dist/Models/DatabaseModels/Project.js +61 -0
  509. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  510. package/build/dist/Models/DatabaseModels/ProjectOidc.js +0 -4
  511. package/build/dist/Models/DatabaseModels/ProjectOidc.js.map +1 -1
  512. package/build/dist/Models/DatabaseModels/ProjectSso.js +0 -4
  513. package/build/dist/Models/DatabaseModels/ProjectSso.js.map +1 -1
  514. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +12 -0
  515. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  516. package/build/dist/Models/DatabaseModels/RumApplication.js +12 -0
  517. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  518. package/build/dist/Models/DatabaseModels/ServerlessFunction.js +12 -0
  519. package/build/dist/Models/DatabaseModels/ServerlessFunction.js.map +1 -1
  520. package/build/dist/Models/DatabaseModels/Service.js +12 -0
  521. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  522. package/build/dist/Models/DatabaseModels/StatusPageOidc.js +0 -4
  523. package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
  524. package/build/dist/Models/DatabaseModels/StatusPageSso.js +0 -4
  525. package/build/dist/Models/DatabaseModels/StatusPageSso.js.map +1 -1
  526. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +49 -3
  527. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
  528. package/build/dist/Models/DatabaseModels/VMwareVCenter.js +12 -0
  529. package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -1
  530. package/build/dist/Server/API/AIChatAPI.js +9 -103
  531. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  532. package/build/dist/Server/API/AIInvestigationAPI.js +3 -3
  533. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  534. package/build/dist/Server/API/AIInvestigationConversationAPI.js +162 -0
  535. package/build/dist/Server/API/AIInvestigationConversationAPI.js.map +1 -0
  536. package/build/dist/Server/API/IncidentFormAPI.js +245 -0
  537. package/build/dist/Server/API/IncidentFormAPI.js.map +1 -0
  538. package/build/dist/Server/API/ProjectOIDC.js +16 -24
  539. package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
  540. package/build/dist/Server/API/ProjectSSO.js +16 -24
  541. package/build/dist/Server/API/ProjectSSO.js.map +1 -1
  542. package/build/dist/Server/API/StatusPageAPI.js +42 -75
  543. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  544. package/build/dist/Server/Enterprise/EnterpriseEdition.js +79 -25
  545. package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -1
  546. package/build/dist/Server/Enterprise/EnterpriseFeature.js +24 -3
  547. package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -1
  548. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +1 -1
  549. package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -1
  550. package/build/dist/Server/EnvironmentConfig.js +1 -1
  551. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796400000000-AddIncidentForms.js +63 -0
  552. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796400000000-AddIncidentForms.js.map +1 -0
  553. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796500000000-AddTeamComplianceRuleNotificationChannels.js +41 -0
  554. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796500000000-AddTeamComplianceRuleNotificationChannels.js.map +1 -0
  555. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796600000000-AddAIInvestigationConversationAndTimeLimits.js +40 -0
  556. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796600000000-AddAIInvestigationConversationAndTimeLimits.js.map +1 -0
  557. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796700000000-AddMessageQueueTables.js +183 -0
  558. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796700000000-AddMessageQueueTables.js.map +1 -0
  559. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  560. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  561. package/build/dist/Server/Middleware/IncidentFormRateLimit.js +559 -0
  562. package/build/dist/Server/Middleware/IncidentFormRateLimit.js.map +1 -0
  563. package/build/dist/Server/Middleware/UserAuthorization.js +11 -30
  564. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  565. package/build/dist/Server/Services/AIService.js +10 -13
  566. package/build/dist/Server/Services/AIService.js.map +1 -1
  567. package/build/dist/Server/Services/IncidentCustomFieldService.js +83 -0
  568. package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
  569. package/build/dist/Server/Services/IncidentEpisodeMemberService.js +8 -4
  570. package/build/dist/Server/Services/IncidentEpisodeMemberService.js.map +1 -1
  571. package/build/dist/Server/Services/IncidentEpisodeService.js +10 -1
  572. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  573. package/build/dist/Server/Services/IncidentFormService.js +1202 -0
  574. package/build/dist/Server/Services/IncidentFormService.js.map +1 -0
  575. package/build/dist/Server/Services/IncidentFormSubmissionService.js +91 -0
  576. package/build/dist/Server/Services/IncidentFormSubmissionService.js.map +1 -0
  577. package/build/dist/Server/Services/IncidentGroupingEngineService.js +20 -8
  578. package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
  579. package/build/dist/Server/Services/IncidentService.js +106 -4
  580. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  581. package/build/dist/Server/Services/IncidentTemplateService.js +18 -0
  582. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  583. package/build/dist/Server/Services/Index.js +14 -0
  584. package/build/dist/Server/Services/Index.js.map +1 -1
  585. package/build/dist/Server/Services/MessageQueueLabelRuleEngineService.js +262 -0
  586. package/build/dist/Server/Services/MessageQueueLabelRuleEngineService.js.map +1 -0
  587. package/build/dist/Server/Services/MessageQueueLabelRuleService.js +13 -0
  588. package/build/dist/Server/Services/MessageQueueLabelRuleService.js.map +1 -0
  589. package/build/dist/Server/Services/MessageQueueOwnerRuleEngineService.js +334 -0
  590. package/build/dist/Server/Services/MessageQueueOwnerRuleEngineService.js.map +1 -0
  591. package/build/dist/Server/Services/MessageQueueOwnerRuleService.js +13 -0
  592. package/build/dist/Server/Services/MessageQueueOwnerRuleService.js.map +1 -0
  593. package/build/dist/Server/Services/MessageQueueOwnerTeamService.js +62 -0
  594. package/build/dist/Server/Services/MessageQueueOwnerTeamService.js.map +1 -0
  595. package/build/dist/Server/Services/MessageQueueOwnerUserService.js +62 -0
  596. package/build/dist/Server/Services/MessageQueueOwnerUserService.js.map +1 -0
  597. package/build/dist/Server/Services/MessageQueueService.js +1140 -0
  598. package/build/dist/Server/Services/MessageQueueService.js.map +1 -0
  599. package/build/dist/Server/Services/OpenTelemetryIngestService.js +25 -4
  600. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  601. package/build/dist/Server/Services/ProjectService.js +0 -54
  602. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  603. package/build/dist/Server/Services/StatusPagePrivateUserService.js +1 -4
  604. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  605. package/build/dist/Server/Services/StatusPageService.js +0 -46
  606. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  607. package/build/dist/Server/Services/TeamComplianceSettingService.js +120 -31
  608. package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
  609. package/build/dist/Server/Services/UserNotificationRuleService.js +8 -1
  610. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  611. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +12 -3
  612. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
  613. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +6 -3
  614. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  615. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +109 -37
  616. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
  617. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +16 -0
  618. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  619. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +3 -3
  620. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +9 -6
  621. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
  622. package/build/dist/Server/Utils/AI/Chat/AgentContextCompactor.js +128 -0
  623. package/build/dist/Server/Utils/AI/Chat/AgentContextCompactor.js.map +1 -0
  624. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +192 -50
  625. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  626. package/build/dist/Server/Utils/AI/Chat/ChatRunCancellation.js +133 -0
  627. package/build/dist/Server/Utils/AI/Chat/ChatRunCancellation.js.map +1 -0
  628. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +53 -13
  629. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
  630. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +1 -1
  631. package/build/dist/Server/Utils/AI/Chat/ToolOutputPager.js +206 -0
  632. package/build/dist/Server/Utils/AI/Chat/ToolOutputPager.js.map +1 -0
  633. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +53 -26
  634. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
  635. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +4 -2
  636. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
  637. package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js +1 -29
  638. package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js.map +1 -1
  639. package/build/dist/Server/Utils/AI/CodeFix/FixRunBudget.js +24 -24
  640. package/build/dist/Server/Utils/AI/CodeFix/FixRunBudget.js.map +1 -1
  641. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +7 -2
  642. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  643. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +29 -14
  644. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  645. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js +38 -14
  646. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js.map +1 -1
  647. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js +2 -0
  648. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js.map +1 -1
  649. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +40 -10
  650. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  651. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +19 -7
  652. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
  653. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +19 -7
  654. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
  655. package/build/dist/Server/Utils/AI/SRE/InvestigationQueue.js +15 -18
  656. package/build/dist/Server/Utils/AI/SRE/InvestigationQueue.js.map +1 -1
  657. package/build/dist/Server/Utils/AI/SRE/InvestigationThread.js +412 -0
  658. package/build/dist/Server/Utils/AI/SRE/InvestigationThread.js.map +1 -0
  659. package/build/dist/Server/Utils/AI/SRE/InvestigationThreadService.js +495 -0
  660. package/build/dist/Server/Utils/AI/SRE/InvestigationThreadService.js.map +1 -0
  661. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +10 -3
  662. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -1
  663. package/build/dist/Server/Utils/EditionEnforcement.js +20 -158
  664. package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -1
  665. package/build/dist/Server/Utils/LLM/LLMService.js +193 -54
  666. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  667. package/build/dist/Server/Utils/RelaxedScimTeamLocksReport.js +160 -0
  668. package/build/dist/Server/Utils/RelaxedScimTeamLocksReport.js.map +1 -0
  669. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +25 -0
  670. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  671. package/build/dist/Server/Utils/SameOriginRequest.js +151 -0
  672. package/build/dist/Server/Utils/SameOriginRequest.js.map +1 -0
  673. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +2 -0
  674. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -1
  675. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +4 -1
  676. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  677. package/build/dist/Server/Utils/Telemetry/MessageQueueDiscovery.js +1015 -0
  678. package/build/dist/Server/Utils/Telemetry/MessageQueueDiscovery.js.map +1 -0
  679. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +4 -1
  680. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  681. package/build/dist/Server/Utils/VendorAssets.js +3 -3
  682. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +3 -1
  683. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  684. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +3 -1
  685. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
  686. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +10 -1
  687. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  688. package/build/dist/Types/AI/AIAgentRunLimits.js +91 -0
  689. package/build/dist/Types/AI/AIAgentRunLimits.js.map +1 -0
  690. package/build/dist/Types/Code/CodeType.js +2 -0
  691. package/build/dist/Types/Code/CodeType.js.map +1 -1
  692. package/build/dist/Types/Code/JsonSyntax.js +604 -0
  693. package/build/dist/Types/Code/JsonSyntax.js.map +1 -0
  694. package/build/dist/Types/CustomField/CustomFieldCreateSettings.js +310 -0
  695. package/build/dist/Types/CustomField/CustomFieldCreateSettings.js.map +1 -0
  696. package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js +38 -0
  697. package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js.map +1 -1
  698. package/build/dist/Types/CustomField/CustomFieldValueValidator.js +10 -7
  699. package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -1
  700. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +1 -1
  701. package/build/dist/Types/Icon/IconProp.js +2 -0
  702. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  703. package/build/dist/Types/Incident/IncidentFormIpAllowlist.js +119 -0
  704. package/build/dist/Types/Incident/IncidentFormIpAllowlist.js.map +1 -0
  705. package/build/dist/Types/Incident/IncidentFormPublic.js +678 -0
  706. package/build/dist/Types/Incident/IncidentFormPublic.js.map +1 -0
  707. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +10 -3
  708. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
  709. package/build/dist/Types/MessageQueue/MessageQueueIdentity.js +184 -0
  710. package/build/dist/Types/MessageQueue/MessageQueueIdentity.js.map +1 -0
  711. package/build/dist/Types/MessageQueue/MessageQueueManualIdentity.js +190 -0
  712. package/build/dist/Types/MessageQueue/MessageQueueManualIdentity.js.map +1 -0
  713. package/build/dist/Types/MessageQueue/MessageQueueMetricCatalog.js +1057 -0
  714. package/build/dist/Types/MessageQueue/MessageQueueMetricCatalog.js.map +1 -0
  715. package/build/dist/Types/MessageQueue/MessagingSystem.js +394 -0
  716. package/build/dist/Types/MessageQueue/MessagingSystem.js.map +1 -0
  717. package/build/dist/Types/MessageQueue/MessagingTelemetryResolver.js +1768 -0
  718. package/build/dist/Types/MessageQueue/MessagingTelemetryResolver.js.map +1 -0
  719. package/build/dist/Types/Monitor/MessageQueueAlertTemplates.js +1207 -0
  720. package/build/dist/Types/Monitor/MessageQueueAlertTemplates.js.map +1 -0
  721. package/build/dist/Types/Permission.js +274 -0
  722. package/build/dist/Types/Permission.js.map +1 -1
  723. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js +10 -4
  724. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js.map +1 -1
  725. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +12 -0
  726. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  727. package/build/dist/Types/Rules/RuleRun.js +4 -0
  728. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  729. package/build/dist/Types/Team/ComplianceNotificationChannel.js +4 -3
  730. package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -1
  731. package/build/dist/Types/Team/ComplianceRule.js +48 -14
  732. package/build/dist/Types/Team/ComplianceRule.js.map +1 -1
  733. package/build/dist/Types/Team/ComplianceRuleType.js +2 -2
  734. package/build/dist/Types/Telemetry/EntityType.js +16 -0
  735. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  736. package/build/dist/Types/XML.js +23 -0
  737. package/build/dist/Types/XML.js.map +1 -1
  738. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +641 -266
  739. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  740. package/build/dist/UI/Components/CodeEditor/CodeEditorCommands.js +464 -0
  741. package/build/dist/UI/Components/CodeEditor/CodeEditorCommands.js.map +1 -0
  742. package/build/dist/UI/Components/CodeEditor/CodeEditorHighlight.js +69 -0
  743. package/build/dist/UI/Components/CodeEditor/CodeEditorHighlight.js.map +1 -0
  744. package/build/dist/UI/Components/CodeEditor/CodeEditorLanguages.js +108 -0
  745. package/build/dist/UI/Components/CodeEditor/CodeEditorLanguages.js.map +1 -0
  746. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +14 -186
  747. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -1
  748. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +5 -1
  749. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
  750. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +5 -14
  751. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -1
  752. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +14 -13
  753. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  754. package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js +1 -1
  755. package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js.map +1 -1
  756. package/build/dist/UI/Components/Forms/Fields/FormField.js +8 -3
  757. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  758. package/build/dist/UI/Components/Icon/Icon.js +8 -0
  759. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  760. package/build/dist/UI/Components/Input/Input.js +11 -3
  761. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  762. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +2 -1
  763. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  764. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +14 -4
  765. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  766. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  767. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  768. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +710 -65
  769. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
  770. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditorHistory.js +238 -0
  771. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditorHistory.js.map +1 -0
  772. package/build/dist/UI/Components/Markdown.tsx/MarkdownListEditing.js +1028 -0
  773. package/build/dist/UI/Components/Markdown.tsx/MarkdownListEditing.js.map +1 -0
  774. package/build/dist/UI/Components/Markdown.tsx/MarkdownPaste.js +1088 -0
  775. package/build/dist/UI/Components/Markdown.tsx/MarkdownPaste.js.map +1 -0
  776. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +70 -10
  777. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  778. package/build/dist/UI/Components/Markdown.tsx/MarkdownVisualEditing.js +887 -0
  779. package/build/dist/UI/Components/Markdown.tsx/MarkdownVisualEditing.js.map +1 -0
  780. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +57 -6
  781. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  782. package/build/dist/UI/Components/ResetObjectID/ResetObjectID.js +7 -5
  783. package/build/dist/UI/Components/ResetObjectID/ResetObjectID.js.map +1 -1
  784. package/build/dist/UI/Components/Table/EmptyTableMessage.js +51 -0
  785. package/build/dist/UI/Components/Table/EmptyTableMessage.js.map +1 -0
  786. package/build/dist/UI/Components/Table/Table.js +30 -11
  787. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  788. package/build/dist/UI/Components/Table/TableRow.js +9 -2
  789. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  790. package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js +63 -1
  791. package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js.map +1 -1
  792. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +4 -2
  793. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  794. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js +91 -10
  795. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js.map +1 -1
  796. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +14 -4
  797. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  798. package/build/dist/UI/Components/TextArea/TextArea.js +9 -3
  799. package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
  800. package/build/dist/UI/Components/Workflow/ModelFieldPicker.js +1 -2
  801. package/build/dist/UI/Components/Workflow/ModelFieldPicker.js.map +1 -1
  802. package/build/dist/Utils/LabelRuleImportExport.js +2 -0
  803. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  804. package/build/dist/Utils/Markdown/UntrustedMarkdown.js +988 -0
  805. package/build/dist/Utils/Markdown/UntrustedMarkdown.js.map +1 -0
  806. package/build/dist/Utils/Monitor/IncomingEmailMonitorRequestUtil.js +34 -0
  807. package/build/dist/Utils/Monitor/IncomingEmailMonitorRequestUtil.js.map +1 -0
  808. package/build/dist/Utils/Monitor/MonitorLogSummaryUtil.js +148 -0
  809. package/build/dist/Utils/Monitor/MonitorLogSummaryUtil.js.map +1 -0
  810. package/build/dist/Utils/Telemetry/EntityKey.js +36 -0
  811. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  812. package/package.json +1 -3
  813. package/Server/Utils/CommunityEditionSsoReport.ts +0 -361
  814. package/Tests/App/Accounts/LoginSsoEdition.test.tsx +0 -616
  815. package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +0 -217
  816. package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +0 -271
  817. package/Tests/App/Dashboard/CloudDocumentationMarkdown.test.ts +0 -215
  818. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +0 -784
  819. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +0 -518
  820. package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +0 -419
  821. package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +0 -769
  822. package/Tests/UI/Components/CodeEditor.test.tsx +0 -638
  823. package/Tests/UI/Components/MonacoLoader.test.ts +0 -67
  824. package/Tests/UI/Components/MonacoRuntime.test.ts +0 -121
  825. package/Tests/UI/Components/Telemetry/TelemetryRetentionConfigForm.test.tsx +0 -329
  826. package/Tests/UI/Components/YamlEditor.test.tsx +0 -847
  827. package/UI/Components/CodeEditor/MonacoLoader.ts +0 -36
  828. package/UI/Components/Telemetry/TelemetryRetentionConfigForm.tsx +0 -319
  829. package/UI/Components/Telemetry/TelemetryRetentionConfigSummary.tsx +0 -223
  830. package/build/dist/Server/Utils/CommunityEditionSsoReport.js +0 -223
  831. package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +0 -1
  832. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +0 -34
  833. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +0 -1
  834. package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigForm.js +0 -171
  835. package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigForm.js.map +0 -1
  836. package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigSummary.js +0 -119
  837. package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigSummary.js.map +0 -1
@@ -0,0 +1,2520 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ jest,
7
+ test,
8
+ } from "@jest/globals";
9
+
10
+ /*
11
+ * What a public submission turns into (IncidentFormService.submitPublicForm).
12
+ *
13
+ * A stranger's request becomes an incident that pages on-call, so every test
14
+ * here pins exactly what reaches IncidentService.create - field by field,
15
+ * with nothing extra - and what happens around it:
16
+ *
17
+ * - the checks, in order: link, form on, plan, network, captcha (only when
18
+ * the instance has one on), the answers, the severity;
19
+ * - the incident: the form's project whatever the body says, the
20
+ * reporter's title and description, the severity (the reporter's choice
21
+ * when allowed, else the form's, else the template's, else a 400), the
22
+ * template id so IncidentService applies the template, the answers to
23
+ * the fields the form asks and to nothing else, never on a status page
24
+ * or to its subscribers - and the template's owners, for IncidentService
25
+ * to add once the incident's channels exist, and to notify;
26
+ * - afterwards: the submission recorded, the reporter named in a private
27
+ * note with every value Markdown-escaped - and a failure in either
28
+ * logged, never thrown, because the incident already stands;
29
+ * - the answer: the incident's number and the form's success message.
30
+ *
31
+ * Only the database and the captcha provider are stubbed.
32
+ */
33
+
34
+ jest.mock("../../../Server/Utils/Logger", () => {
35
+ return {
36
+ __esModule: true,
37
+ default: {
38
+ debug: jest.fn(),
39
+ info: jest.fn(),
40
+ warn: jest.fn(),
41
+ error: jest.fn(),
42
+ },
43
+ getLogAttributesFromRequest: jest.fn().mockReturnValue({}),
44
+ };
45
+ });
46
+
47
+ import Incident from "../../../Models/DatabaseModels/Incident";
48
+ import IncidentCustomField from "../../../Models/DatabaseModels/IncidentCustomField";
49
+ import IncidentForm from "../../../Models/DatabaseModels/IncidentForm";
50
+ import IncidentFormSubmission from "../../../Models/DatabaseModels/IncidentFormSubmission";
51
+ import IncidentInternalNote from "../../../Models/DatabaseModels/IncidentInternalNote";
52
+ import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
53
+ import IncidentTemplate from "../../../Models/DatabaseModels/IncidentTemplate";
54
+ import IncidentTemplateOwnerTeam from "../../../Models/DatabaseModels/IncidentTemplateOwnerTeam";
55
+ import IncidentTemplateOwnerUser from "../../../Models/DatabaseModels/IncidentTemplateOwnerUser";
56
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
57
+ import IncidentFormRateLimit, {
58
+ INCIDENT_FORM_RATE_LIMIT_UNAVAILABLE_MESSAGE,
59
+ INCIDENT_FORM_TOTAL_RATE_LIMIT_MESSAGE,
60
+ IncidentFormCeilingException,
61
+ } from "../../../Server/Middleware/IncidentFormRateLimit";
62
+ import IncidentCustomFieldService from "../../../Server/Services/IncidentCustomFieldService";
63
+ import IncidentFormService, {
64
+ INCIDENT_FORM_NETWORK_NOT_ALLOWED_MESSAGE,
65
+ INCIDENT_FORM_NOT_AVAILABLE_MESSAGE,
66
+ INCIDENT_FORM_NO_SEVERITY_MESSAGE,
67
+ INCIDENT_FORM_SUBMIT_FAILED_MESSAGE,
68
+ INCIDENT_FORM_TITLE_TOO_LONG_MESSAGE,
69
+ getIncidentFormReporterNote,
70
+ neutralizeIncidentFormReport,
71
+ } from "../../../Server/Services/IncidentFormService";
72
+ import IncidentFormSubmissionService from "../../../Server/Services/IncidentFormSubmissionService";
73
+ import IncidentInternalNoteService from "../../../Server/Services/IncidentInternalNoteService";
74
+ import IncidentService from "../../../Server/Services/IncidentService";
75
+ import IncidentSeverityService from "../../../Server/Services/IncidentSeverityService";
76
+ import IncidentTemplateOwnerTeamService from "../../../Server/Services/IncidentTemplateOwnerTeamService";
77
+ import IncidentTemplateOwnerUserService from "../../../Server/Services/IncidentTemplateOwnerUserService";
78
+ import IncidentTemplateService from "../../../Server/Services/IncidentTemplateService";
79
+ import Markdown, { MarkdownContentType } from "../../../Server/Types/Markdown";
80
+ import CaptchaUtil from "../../../Server/Utils/Captcha";
81
+ import logger from "../../../Server/Utils/Logger";
82
+ import SlackUtil from "../../../Server/Utils/Workspace/Slack/Slack";
83
+ import CustomFieldMappingSourceResource from "../../../Types/CustomField/CustomFieldMappingSourceResource";
84
+ import CustomFieldType from "../../../Types/CustomField/CustomFieldType";
85
+ import Email from "../../../Types/Email";
86
+ import BadDataException from "../../../Types/Exception/BadDataException";
87
+ import Exception from "../../../Types/Exception/Exception";
88
+ import ForbiddenException from "../../../Types/Exception/ForbiddenException";
89
+ import NotFoundException from "../../../Types/Exception/NotFoundException";
90
+ import ServerException from "../../../Types/Exception/ServerException";
91
+ import ServiceUnavailableException from "../../../Types/Exception/ServiceUnavailableException";
92
+ import {
93
+ IncidentFormFieldSetting,
94
+ PublicIncidentFormSubmissionResult,
95
+ ValidatedIncidentFormSubmission,
96
+ formatIncidentFormSubmissionErrors,
97
+ } from "../../../Types/Incident/IncidentFormPublic";
98
+ import { JSONObject } from "../../../Types/JSON";
99
+ import ObjectID from "../../../Types/ObjectID";
100
+ import {
101
+ hrefsOf,
102
+ renderAsDashboard,
103
+ } from "../../Utils/Markdown/DashboardMarkdownRenderer";
104
+ import { Lexer, Token, Tokens, marked } from "marked";
105
+
106
+ type MockedFn = ReturnType<typeof jest.fn>;
107
+
108
+ const PROJECT_ID: ObjectID = new ObjectID(
109
+ "5d7f3c0a-4c55-4d3e-9a3e-2d4a7d1c9f01",
110
+ );
111
+ const OTHER_PROJECT_ID: string = "5d7f3c0a-4c55-4d3e-9a3e-2d4a7d1c9f02";
112
+ const FORM_ID: string = "a1b2c3d4-0000-4000-8000-0000000000f1";
113
+ const SHARE_KEY: string = "7c9e6679-7425-40de-944b-e07fc1f90ae7";
114
+ const OTHER_SHARE_KEY: string = "0f8fad5b-d9cb-469f-a165-70867728950e";
115
+ const FORM_SEVERITY_ID: string = "b0000000-0000-4000-8000-0000000000f1";
116
+ const MINOR_SEVERITY_ID: string = "b0000000-0000-4000-8000-0000000000f2";
117
+ const TEMPLATE_ID: string = "c0000000-0000-4000-8000-0000000000f1";
118
+ const INCIDENT_ID: string = "e0000000-0000-4000-8000-0000000000f1";
119
+ const OWNER_USER_IDS: Array<string> = [
120
+ "f0000000-0000-4000-8000-0000000000a1",
121
+ "f0000000-0000-4000-8000-0000000000a2",
122
+ ];
123
+ const OWNER_TEAM_ID: string = "f0000000-0000-4000-8000-0000000000b1";
124
+ const CLIENT_IP: string = "203.0.113.7";
125
+ const CAPTCHA_IP: string = "203.0.113.8";
126
+
127
+ // A column read back from Postgres as NULL, which the model's types omit.
128
+ function setNull(
129
+ model: IncidentForm | Incident | IncidentTemplate,
130
+ column: string,
131
+ ): void {
132
+ (model as unknown as Record<string, unknown>)[column] = null;
133
+ }
134
+
135
+ function buildForm(data: Partial<IncidentForm> = {}): IncidentForm {
136
+ const form: IncidentForm = new IncidentForm();
137
+ form._id = FORM_ID;
138
+ form.projectId = PROJECT_ID;
139
+ form.name = "Report a Problem";
140
+ form.isEnabled = true;
141
+ form.shareKey = new ObjectID(SHARE_KEY);
142
+ form.incidentSeverityId = new ObjectID(FORM_SEVERITY_ID);
143
+ form.allowReporterToChooseSeverity = false;
144
+ form.descriptionSetting = IncidentFormFieldSetting.Optional;
145
+ form.customFieldSettings = {};
146
+ form.isReporterDetailsRequired = true;
147
+ form.successMessage = "Thanks - **we are on it**.";
148
+ form.ipWhitelist = "";
149
+ Object.assign(form, data);
150
+ return form;
151
+ }
152
+
153
+ function withTemplate(data: Partial<IncidentForm> = {}): IncidentForm {
154
+ return buildForm({ incidentTemplateId: new ObjectID(TEMPLATE_ID), ...data });
155
+ }
156
+
157
+ function customField(data: {
158
+ name: string;
159
+ variableKey: string;
160
+ customFieldType: CustomFieldType;
161
+ sortOrder: number;
162
+ dropdownOptions?: string;
163
+ }): IncidentCustomField {
164
+ const field: IncidentCustomField = new IncidentCustomField();
165
+ field._id = ObjectID.generate().toString();
166
+ field.projectId = PROJECT_ID;
167
+ field.name = data.name;
168
+ field.variableKey = data.variableKey;
169
+ field.customFieldType = data.customFieldType;
170
+ field.sortOrder = data.sortOrder;
171
+ field.showOnCreate = true;
172
+ field.isRequiredOnCreate = true;
173
+
174
+ if (data.dropdownOptions !== undefined) {
175
+ field.dropdownOptions = data.dropdownOptions;
176
+ }
177
+
178
+ return field;
179
+ }
180
+
181
+ const PROJECT_FIELDS: Array<IncidentCustomField> = [
182
+ customField({
183
+ name: "Impact",
184
+ variableKey: "impact",
185
+ customFieldType: CustomFieldType.Dropdown,
186
+ sortOrder: 1,
187
+ dropdownOptions: "Low\nHigh",
188
+ }),
189
+ customField({
190
+ name: "Region",
191
+ variableKey: "region",
192
+ customFieldType: CustomFieldType.Text,
193
+ sortOrder: 2,
194
+ }),
195
+ customField({
196
+ name: "Acknowledged",
197
+ variableKey: "acknowledged",
198
+ customFieldType: CustomFieldType.Boolean,
199
+ sortOrder: 3,
200
+ }),
201
+ customField({
202
+ name: "Secret Escalation Path",
203
+ variableKey: "secret_escalation_path",
204
+ customFieldType: CustomFieldType.Text,
205
+ sortOrder: 4,
206
+ }),
207
+ customField({
208
+ name: "Details",
209
+ variableKey: "details",
210
+ customFieldType: CustomFieldType.Markdown,
211
+ sortOrder: 5,
212
+ }),
213
+ customField({
214
+ name: "Notes",
215
+ variableKey: "notes",
216
+ customFieldType: CustomFieldType.LongText,
217
+ sortOrder: 6,
218
+ }),
219
+ customField({
220
+ name: "Systems",
221
+ variableKey: "systems",
222
+ customFieldType: CustomFieldType.MultiSelectDropdown,
223
+ sortOrder: 7,
224
+ dropdownOptions: "API\nWeb",
225
+ }),
226
+ ];
227
+
228
+ const FORM_QUESTIONS: Record<string, string> = {
229
+ impact: "Required",
230
+ region: "Optional",
231
+ secret_escalation_path: "Hidden",
232
+ };
233
+
234
+ function severity(id: string, name: string, order: number): IncidentSeverity {
235
+ const row: IncidentSeverity = new IncidentSeverity();
236
+ row._id = id;
237
+ row.name = name;
238
+ row.order = order;
239
+ row.projectId = PROJECT_ID;
240
+ return row;
241
+ }
242
+
243
+ const PROJECT_SEVERITIES: Array<IncidentSeverity> = [
244
+ severity(FORM_SEVERITY_ID, "Critical", 1),
245
+ severity(MINOR_SEVERITY_ID, "Minor", 2),
246
+ ];
247
+
248
+ function ownerUser(userId: string): IncidentTemplateOwnerUser {
249
+ const owner: IncidentTemplateOwnerUser = new IncidentTemplateOwnerUser();
250
+ owner.userId = new ObjectID(userId);
251
+ return owner;
252
+ }
253
+
254
+ function ownerTeam(teamId: string): IncidentTemplateOwnerTeam {
255
+ const owner: IncidentTemplateOwnerTeam = new IncidentTemplateOwnerTeam();
256
+ owner.teamId = new ObjectID(teamId);
257
+ return owner;
258
+ }
259
+
260
+ function templateWithSeverity(severityId: string | null): IncidentTemplate {
261
+ const template: IncidentTemplate = new IncidentTemplate();
262
+ template._id = TEMPLATE_ID;
263
+
264
+ if (severityId) {
265
+ template.incidentSeverityId = new ObjectID(severityId);
266
+ } else {
267
+ setNull(template, "incidentSeverityId");
268
+ }
269
+
270
+ return template;
271
+ }
272
+
273
+ const VALID_ANSWERS: JSONObject = {
274
+ title: " Checkout is down ",
275
+ description: "Every order fails\n\n at step 3",
276
+ reporterName: "Jane Doe",
277
+ reporterEmail: "Jane.Doe@Example.com",
278
+ };
279
+
280
+ let storedForm: IncidentForm | null = null;
281
+ let formFindOneBy: MockedFn;
282
+ let customFieldFindBy: MockedFn;
283
+ let severityFindBy: MockedFn;
284
+ let templateFindOneBy: MockedFn;
285
+ let incidentCreate: MockedFn;
286
+ let addOwners: MockedFn;
287
+ let ownerUserFindBy: MockedFn;
288
+ let ownerTeamFindBy: MockedFn;
289
+ let submissionCreate: MockedFn;
290
+ let noteCreate: MockedFn;
291
+ let captchaEnabled: MockedFn;
292
+ let verifyCaptcha: MockedFn;
293
+ let reserveFormSubmission: MockedFn;
294
+ let createdIncident: Incident;
295
+
296
+ beforeEach(() => {
297
+ storedForm = buildForm();
298
+
299
+ createdIncident = new Incident();
300
+ createdIncident._id = INCIDENT_ID;
301
+ createdIncident.projectId = PROJECT_ID;
302
+ createdIncident.incidentNumber = 42;
303
+ createdIncident.incidentNumberWithPrefix = "INC-42";
304
+
305
+ formFindOneBy = jest
306
+ .spyOn(IncidentFormService, "findOneBy")
307
+ .mockImplementation((async (findBy: {
308
+ query: { shareKey?: ObjectID };
309
+ }): Promise<IncidentForm | null> => {
310
+ if (
311
+ storedForm &&
312
+ findBy.query.shareKey?.toString() === storedForm.shareKey?.toString()
313
+ ) {
314
+ return storedForm;
315
+ }
316
+
317
+ return null;
318
+ }) as never) as unknown as MockedFn;
319
+
320
+ jest
321
+ .spyOn(IncidentFormService, "isProjectOnPlan")
322
+ .mockResolvedValue(true as never);
323
+
324
+ customFieldFindBy = jest
325
+ .spyOn(IncidentCustomFieldService, "findBy")
326
+ .mockResolvedValue(PROJECT_FIELDS as never) as unknown as MockedFn;
327
+
328
+ severityFindBy = jest
329
+ .spyOn(IncidentSeverityService, "findBy")
330
+ .mockResolvedValue(PROJECT_SEVERITIES as never) as unknown as MockedFn;
331
+
332
+ templateFindOneBy = jest
333
+ .spyOn(IncidentTemplateService, "findOneBy")
334
+ .mockResolvedValue(
335
+ templateWithSeverity(MINOR_SEVERITY_ID) as never,
336
+ ) as unknown as MockedFn;
337
+
338
+ incidentCreate = jest
339
+ .spyOn(IncidentService, "create")
340
+ .mockImplementation((async (): Promise<Incident> => {
341
+ return createdIncident;
342
+ }) as never) as unknown as MockedFn;
343
+
344
+ addOwners = jest
345
+ .spyOn(IncidentService, "addOwners")
346
+ .mockResolvedValue(undefined as never) as unknown as MockedFn;
347
+
348
+ ownerUserFindBy = jest
349
+ .spyOn(IncidentTemplateOwnerUserService, "findBy")
350
+ .mockResolvedValue(
351
+ OWNER_USER_IDS.map(ownerUser) as never,
352
+ ) as unknown as MockedFn;
353
+
354
+ ownerTeamFindBy = jest
355
+ .spyOn(IncidentTemplateOwnerTeamService, "findBy")
356
+ .mockResolvedValue([
357
+ ownerTeam(OWNER_TEAM_ID),
358
+ ] as never) as unknown as MockedFn;
359
+
360
+ submissionCreate = jest
361
+ .spyOn(IncidentFormSubmissionService, "create")
362
+ .mockImplementation((async (createBy: {
363
+ data: IncidentFormSubmission;
364
+ }): Promise<IncidentFormSubmission> => {
365
+ return createBy.data;
366
+ }) as never) as unknown as MockedFn;
367
+
368
+ noteCreate = jest
369
+ .spyOn(IncidentInternalNoteService, "create")
370
+ .mockImplementation((async (createBy: {
371
+ data: IncidentInternalNote;
372
+ }): Promise<IncidentInternalNote> => {
373
+ return createBy.data;
374
+ }) as never) as unknown as MockedFn;
375
+
376
+ captchaEnabled = jest
377
+ .spyOn(CaptchaUtil, "isCaptchaEnabled")
378
+ .mockReturnValue(false) as unknown as MockedFn;
379
+
380
+ verifyCaptcha = jest
381
+ .spyOn(CaptchaUtil, "verifyCaptcha")
382
+ .mockResolvedValue(undefined as never) as unknown as MockedFn;
383
+
384
+ /*
385
+ * The form's hourly ceiling is counted in Redis, which this suite has
386
+ * none of (left real, it would fail closed with a 503); what it is asked,
387
+ * and when, is pinned below.
388
+ */
389
+ reserveFormSubmission = jest
390
+ .spyOn(IncidentFormRateLimit, "reserveFormSubmission")
391
+ .mockResolvedValue(undefined as never) as unknown as MockedFn;
392
+ });
393
+
394
+ afterEach(() => {
395
+ jest.restoreAllMocks();
396
+ jest.clearAllMocks();
397
+ });
398
+
399
+ function submit(
400
+ data: {
401
+ answers?: unknown;
402
+ captchaToken?: string;
403
+ shareKey?: string | undefined;
404
+ clientIp?: string | undefined;
405
+ } = {},
406
+ ): Promise<PublicIncidentFormSubmissionResult> {
407
+ const request: { data: unknown; captchaToken?: string } = {
408
+ data: "answers" in data ? data.answers : VALID_ANSWERS,
409
+ };
410
+
411
+ if (data.captchaToken !== undefined) {
412
+ request.captchaToken = data.captchaToken;
413
+ }
414
+
415
+ return IncidentFormService.submitPublicForm({
416
+ shareKey: "shareKey" in data ? data.shareKey : SHARE_KEY,
417
+ request: request as never,
418
+ clientIp: "clientIp" in data ? data.clientIp : CLIENT_IP,
419
+ captchaRemoteIp: CAPTCHA_IP,
420
+ });
421
+ }
422
+
423
+ async function refusal(
424
+ promise: Promise<unknown>,
425
+ ): Promise<Exception | undefined> {
426
+ try {
427
+ await promise;
428
+ } catch (err) {
429
+ return err as Exception;
430
+ }
431
+
432
+ return undefined;
433
+ }
434
+
435
+ // The call IncidentService.create received.
436
+ function createCall(): {
437
+ data: Incident;
438
+ props: Record<string, unknown>;
439
+ miscDataProps?: JSONObject | undefined;
440
+ } {
441
+ expect(incidentCreate).toHaveBeenCalledTimes(1);
442
+
443
+ return incidentCreate.mock.calls[0]![0] as {
444
+ data: Incident;
445
+ props: Record<string, unknown>;
446
+ miscDataProps?: JSONObject | undefined;
447
+ };
448
+ }
449
+
450
+ /*
451
+ * Every column set on the incident handed to IncidentService.create, as
452
+ * plain values. Asserting this whole object, rather than a column or two,
453
+ * is what proves nothing else in the request reached the incident. (Only
454
+ * table columns: the model's own bookkeeping, such as isPermissionIf, is
455
+ * never written.)
456
+ */
457
+ function createdColumns(): Record<string, unknown> {
458
+ const incident: Incident = createCall().data;
459
+ const columns: Record<string, unknown> = {};
460
+
461
+ for (const [key, value] of Object.entries(incident)) {
462
+ if (value === undefined || !incident.hasColumn(key)) {
463
+ continue;
464
+ }
465
+
466
+ columns[key] = value instanceof ObjectID ? value.toString() : value;
467
+ }
468
+
469
+ return columns;
470
+ }
471
+
472
+ const PLAIN_INCIDENT: Record<string, unknown> = {
473
+ projectId: PROJECT_ID.toString(),
474
+ title: "Checkout is down",
475
+ description: "Every order fails\n\n at step 3",
476
+ incidentSeverityId: FORM_SEVERITY_ID,
477
+ customFields: {},
478
+ isVisibleOnStatusPage: false,
479
+ shouldStatusPageSubscribersBeNotifiedOnIncidentCreated: false,
480
+ };
481
+
482
+ describe("IncidentFormService.submitPublicForm - the incident it declares", () => {
483
+ test("a form without a template: exactly the reporter's incident, in the form's project, as root", async () => {
484
+ await submit();
485
+
486
+ expect(createdColumns()).toEqual(PLAIN_INCIDENT);
487
+ expect(createCall().props).toEqual({ isRoot: true });
488
+ expect(createCall().data).toBeInstanceOf(Incident);
489
+ });
490
+
491
+ test("a form with a template: the same, plus the template's id for IncidentService to apply", async () => {
492
+ storedForm = withTemplate();
493
+
494
+ await submit();
495
+
496
+ expect(createdColumns()).toEqual({
497
+ ...PLAIN_INCIDENT,
498
+ createdIncidentTemplateId: TEMPLATE_ID,
499
+ });
500
+ expect(typeof createCall().data.createdIncidentTemplateId).toBe("string");
501
+ });
502
+
503
+ /*
504
+ * IncidentService only applies a template when no state is given, so a
505
+ * state here would silently drop the template's monitors, owners' policies
506
+ * and everything else it sets.
507
+ */
508
+ test("never sets a state, so IncidentService's template branch runs", async () => {
509
+ storedForm = withTemplate();
510
+
511
+ await submit();
512
+
513
+ expect(createCall().data.currentIncidentStateId).toBeUndefined();
514
+ expect(createCall().data.currentIncidentState).toBeUndefined();
515
+ });
516
+
517
+ test("names no creating user: nobody signed in to send it", async () => {
518
+ await submit();
519
+
520
+ expect(createCall().data.createdByUserId).toBeUndefined();
521
+ expect(createCall().data.createdByUser).toBeUndefined();
522
+ expect(createCall().props["userId"]).toBeUndefined();
523
+ expect(createCall().props["tenantId"]).toBeUndefined();
524
+ });
525
+
526
+ test("keeps the incident off every status page, and quiet to their subscribers, whatever the template says", async () => {
527
+ storedForm = withTemplate();
528
+
529
+ await submit();
530
+
531
+ expect(createCall().data.isVisibleOnStatusPage).toBe(false);
532
+ expect(
533
+ createCall().data.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated,
534
+ ).toBe(false);
535
+ });
536
+
537
+ /*
538
+ * The body is a stranger's. Nothing in it but the answers the form asks
539
+ * for may reach the incident - least of all which project it lands in.
540
+ */
541
+ test("ignores everything in the body the form does not ask for", async () => {
542
+ await submit({
543
+ answers: {
544
+ ...VALID_ANSWERS,
545
+ projectId: OTHER_PROJECT_ID,
546
+ currentIncidentStateId: ObjectID.generate().toString(),
547
+ isPrivate: true,
548
+ isVisibleOnStatusPage: true,
549
+ shouldStatusPageSubscribersBeNotifiedOnIncidentCreated: true,
550
+ createdByUserId: ObjectID.generate().toString(),
551
+ createdIncidentTemplateId: ObjectID.generate().toString(),
552
+ monitors: [{ _id: ObjectID.generate().toString() }],
553
+ rootCause: "planted",
554
+ incidentSeverityId: MINOR_SEVERITY_ID,
555
+ },
556
+ });
557
+
558
+ expect(createdColumns()).toEqual(PLAIN_INCIDENT);
559
+ });
560
+ });
561
+
562
+ describe("IncidentFormService.submitPublicForm - severity", () => {
563
+ test("uses the reporter's choice when the form lets them choose", async () => {
564
+ storedForm = buildForm({ allowReporterToChooseSeverity: true });
565
+
566
+ await submit({
567
+ answers: {
568
+ ...VALID_ANSWERS,
569
+ incidentSeverityId: MINOR_SEVERITY_ID.toUpperCase(),
570
+ },
571
+ });
572
+
573
+ expect(createdColumns()["incidentSeverityId"]).toBe(MINOR_SEVERITY_ID);
574
+ expect(createCall().data.incidentSeverityId).toBeInstanceOf(ObjectID);
575
+ });
576
+
577
+ test("checks the choice against the form's project's severities, read as root", async () => {
578
+ storedForm = buildForm({ allowReporterToChooseSeverity: true });
579
+
580
+ await submit({
581
+ answers: { ...VALID_ANSWERS, incidentSeverityId: MINOR_SEVERITY_ID },
582
+ });
583
+
584
+ const findBy: {
585
+ query: Record<string, unknown>;
586
+ props: Record<string, unknown>;
587
+ } = severityFindBy.mock.calls[0]![0] as never;
588
+
589
+ expect(findBy.query).toEqual({ projectId: PROJECT_ID });
590
+ expect(findBy.props).toEqual({ isRoot: true });
591
+ });
592
+
593
+ test("refuses a choice that is not one of the project's severities, declaring nothing", async () => {
594
+ storedForm = buildForm({ allowReporterToChooseSeverity: true });
595
+
596
+ const error: Exception | undefined = await refusal(
597
+ submit({
598
+ answers: {
599
+ ...VALID_ANSWERS,
600
+ incidentSeverityId: ObjectID.generate().toString(),
601
+ },
602
+ }),
603
+ );
604
+
605
+ expect(error).toBeInstanceOf(BadDataException);
606
+ expect(error?.code).toBe(400);
607
+ expect(error?.message).toBe(
608
+ "Severity must be one of the severities this form lists.",
609
+ );
610
+ expect(incidentCreate).not.toHaveBeenCalled();
611
+ });
612
+
613
+ test("uses the form's severity when the reporter may choose but did not", async () => {
614
+ storedForm = buildForm({ allowReporterToChooseSeverity: true });
615
+
616
+ await submit({ answers: { ...VALID_ANSWERS, incidentSeverityId: "" } });
617
+
618
+ expect(createdColumns()["incidentSeverityId"]).toBe(FORM_SEVERITY_ID);
619
+ });
620
+
621
+ test("ignores a choice, and reads no severities, when the form sets the severity", async () => {
622
+ await submit({
623
+ answers: { ...VALID_ANSWERS, incidentSeverityId: MINOR_SEVERITY_ID },
624
+ });
625
+
626
+ expect(createdColumns()["incidentSeverityId"]).toBe(FORM_SEVERITY_ID);
627
+ expect(severityFindBy).not.toHaveBeenCalled();
628
+ });
629
+
630
+ test("ignores even an invalid choice when the form does not offer one", async () => {
631
+ await submit({
632
+ answers: { ...VALID_ANSWERS, incidentSeverityId: "not-a-severity" },
633
+ });
634
+
635
+ expect(createdColumns()["incidentSeverityId"]).toBe(FORM_SEVERITY_ID);
636
+ });
637
+
638
+ /*
639
+ * Deleting the form's severity clears the column. With a template that
640
+ * has a severity, the incident is declared without one here and
641
+ * IncidentService's template branch fills it in.
642
+ */
643
+ test("leaves the severity to the template when the form's own was deleted", async () => {
644
+ storedForm = withTemplate();
645
+ setNull(storedForm, "incidentSeverityId");
646
+
647
+ await submit();
648
+
649
+ expect(createdColumns()).toEqual({
650
+ ...PLAIN_INCIDENT,
651
+ incidentSeverityId: undefined,
652
+ createdIncidentTemplateId: TEMPLATE_ID,
653
+ });
654
+ expect(createCall().data.incidentSeverityId).toBeUndefined();
655
+ });
656
+
657
+ test("looks the template's severity up in the form's project, as root", async () => {
658
+ storedForm = withTemplate();
659
+ setNull(storedForm, "incidentSeverityId");
660
+
661
+ await submit();
662
+
663
+ expect(templateFindOneBy).toHaveBeenCalledWith({
664
+ query: { _id: TEMPLATE_ID, projectId: PROJECT_ID },
665
+ select: { incidentSeverityId: true },
666
+ props: { isRoot: true },
667
+ });
668
+ });
669
+
670
+ test("does not read the template when the form has a severity", async () => {
671
+ storedForm = withTemplate();
672
+
673
+ await submit();
674
+
675
+ expect(templateFindOneBy).not.toHaveBeenCalled();
676
+ });
677
+
678
+ test.each([
679
+ [
680
+ "no template",
681
+ (): IncidentForm => {
682
+ return buildForm();
683
+ },
684
+ null,
685
+ ],
686
+ [
687
+ "a template without a severity",
688
+ (): IncidentForm => {
689
+ return withTemplate();
690
+ },
691
+ templateWithSeverity(null),
692
+ ],
693
+ [
694
+ "a template that is not in the form's project",
695
+ (): IncidentForm => {
696
+ return withTemplate();
697
+ },
698
+ null,
699
+ ],
700
+ ])(
701
+ "refuses, declaring nothing, when no severity is left: %s",
702
+ async (
703
+ _label: string,
704
+ form: () => IncidentForm,
705
+ template: IncidentTemplate | null,
706
+ ) => {
707
+ storedForm = form();
708
+ setNull(storedForm, "incidentSeverityId");
709
+ templateFindOneBy.mockResolvedValue(template as never);
710
+
711
+ const error: Exception | undefined = await refusal(submit());
712
+
713
+ expect(error).toBeInstanceOf(BadDataException);
714
+ expect(error?.code).toBe(400);
715
+ expect(error?.message).toBe(INCIDENT_FORM_NO_SEVERITY_MESSAGE);
716
+ expect(incidentCreate).not.toHaveBeenCalled();
717
+ },
718
+ );
719
+ });
720
+
721
+ describe("IncidentFormService.submitPublicForm - title and description", () => {
722
+ test("trims the title and keeps it on one line", async () => {
723
+ await submit({
724
+ answers: { ...VALID_ANSWERS, title: "\n Checkout\tis down \r\n" },
725
+ });
726
+
727
+ expect(createCall().data.title).toBe("Checkout is down");
728
+ });
729
+
730
+ test.each([
731
+ ["missing", undefined],
732
+ ["blank", " "],
733
+ ])(
734
+ "refuses a %s title, declaring nothing",
735
+ async (_label: string, title: string | undefined) => {
736
+ const answers: JSONObject = { ...VALID_ANSWERS };
737
+
738
+ if (title === undefined) {
739
+ delete answers["title"];
740
+ } else {
741
+ answers["title"] = title;
742
+ }
743
+
744
+ const error: Exception | undefined = await refusal(submit({ answers }));
745
+
746
+ expect(error).toBeInstanceOf(BadDataException);
747
+ expect(error?.message).toBe("Title is required.");
748
+ expect(incidentCreate).not.toHaveBeenCalled();
749
+ },
750
+ );
751
+
752
+ test("a hidden description question: whatever was sent is ignored, so a template's description can apply", async () => {
753
+ storedForm = withTemplate({
754
+ descriptionSetting: IncidentFormFieldSetting.Hidden,
755
+ });
756
+
757
+ // VALID_ANSWERS carries a description; the form does not ask for one.
758
+ await submit();
759
+
760
+ expect(createCall().data.description).toBeUndefined();
761
+ });
762
+
763
+ test.each([
764
+ ["left out", undefined],
765
+ ["empty", ""],
766
+ ["only whitespace", " \n\n "],
767
+ ])(
768
+ "an optional description %s is left undefined, not sent empty",
769
+ async (_label: string, description: string | undefined) => {
770
+ const answers: JSONObject = { ...VALID_ANSWERS };
771
+
772
+ if (description === undefined) {
773
+ delete answers["description"];
774
+ } else {
775
+ answers["description"] = description;
776
+ }
777
+
778
+ storedForm = withTemplate();
779
+
780
+ await submit({ answers });
781
+
782
+ expect(createCall().data.description).toBeUndefined();
783
+ },
784
+ );
785
+
786
+ test("a required description that is missing is refused, declaring nothing", async () => {
787
+ storedForm = buildForm({
788
+ descriptionSetting: IncidentFormFieldSetting.Required,
789
+ });
790
+
791
+ const answers: JSONObject = { ...VALID_ANSWERS };
792
+ delete answers["description"];
793
+
794
+ const error: Exception | undefined = await refusal(submit({ answers }));
795
+
796
+ expect(error).toBeInstanceOf(BadDataException);
797
+ expect(error?.message).toBe("Description is required.");
798
+ expect(incidentCreate).not.toHaveBeenCalled();
799
+ });
800
+
801
+ test("a required description that was written is declared as written", async () => {
802
+ storedForm = buildForm({
803
+ descriptionSetting: IncidentFormFieldSetting.Required,
804
+ });
805
+
806
+ await submit();
807
+
808
+ expect(createCall().data.description).toBe(
809
+ "Every order fails\n\n at step 3",
810
+ );
811
+ });
812
+ });
813
+
814
+ describe("IncidentFormService.submitPublicForm - custom fields", () => {
815
+ test("stores the answers to the fields the form asks, and drops every other key", async () => {
816
+ storedForm = buildForm({ customFieldSettings: FORM_QUESTIONS });
817
+
818
+ await submit({
819
+ answers: {
820
+ ...VALID_ANSWERS,
821
+ customFields: {
822
+ Impact: "High",
823
+ Region: " EU-West ",
824
+ "Secret Escalation Path": "planted",
825
+ Acknowledged: true,
826
+ "No Such Field": "planted",
827
+ },
828
+ },
829
+ });
830
+
831
+ expect(createCall().data.customFields).toEqual({
832
+ Impact: "High",
833
+ Region: "EU-West",
834
+ });
835
+ });
836
+
837
+ test("reads the project's fields from the form's own project, as root", async () => {
838
+ storedForm = buildForm({ customFieldSettings: FORM_QUESTIONS });
839
+
840
+ await submit({
841
+ answers: { ...VALID_ANSWERS, customFields: { Impact: "Low" } },
842
+ });
843
+
844
+ const findBy: {
845
+ query: Record<string, unknown>;
846
+ props: Record<string, unknown>;
847
+ } = customFieldFindBy.mock.calls[0]![0] as never;
848
+
849
+ expect(findBy.query).toEqual({ projectId: PROJECT_ID });
850
+ expect(findBy.props).toEqual({ isRoot: true });
851
+ });
852
+
853
+ test("leaves an unanswered optional field out, so a template's value for it still applies", async () => {
854
+ storedForm = withTemplate({ customFieldSettings: FORM_QUESTIONS });
855
+
856
+ await submit({
857
+ answers: {
858
+ ...VALID_ANSWERS,
859
+ customFields: { Impact: "Low", Region: "" },
860
+ },
861
+ });
862
+
863
+ expect(createCall().data.customFields).toEqual({ Impact: "Low" });
864
+ });
865
+
866
+ test("refuses an unanswered required field, declaring nothing", async () => {
867
+ storedForm = buildForm({ customFieldSettings: FORM_QUESTIONS });
868
+
869
+ const error: Exception | undefined = await refusal(
870
+ submit({ answers: { ...VALID_ANSWERS, customFields: { Region: "EU" } } }),
871
+ );
872
+
873
+ expect(error).toBeInstanceOf(BadDataException);
874
+ expect(error?.message).toBe("Impact is required.");
875
+ expect(incidentCreate).not.toHaveBeenCalled();
876
+ });
877
+
878
+ test("refuses an answer that is not one of a dropdown's options", async () => {
879
+ storedForm = buildForm({ customFieldSettings: FORM_QUESTIONS });
880
+
881
+ const error: Exception | undefined = await refusal(
882
+ submit({
883
+ answers: { ...VALID_ANSWERS, customFields: { Impact: "Apocalyptic" } },
884
+ }),
885
+ );
886
+
887
+ expect(error).toBeInstanceOf(BadDataException);
888
+ expect(error?.message).toContain("Apocalyptic");
889
+ expect(incidentCreate).not.toHaveBeenCalled();
890
+ });
891
+
892
+ test("a required yes/no question must be ticked", async () => {
893
+ storedForm = buildForm({
894
+ customFieldSettings: { acknowledged: "Required" },
895
+ });
896
+
897
+ const error: Exception | undefined = await refusal(
898
+ submit({
899
+ answers: { ...VALID_ANSWERS, customFields: { Acknowledged: false } },
900
+ }),
901
+ );
902
+
903
+ expect(error?.message).toBe("Acknowledged must be checked.");
904
+
905
+ await submit({
906
+ answers: { ...VALID_ANSWERS, customFields: { Acknowledged: "true" } },
907
+ });
908
+
909
+ expect(createCall().data.customFields).toEqual({ Acknowledged: true });
910
+ });
911
+
912
+ /*
913
+ * One anonymous request of a million multi-select entries must not cost
914
+ * the shared API process seconds of work, nor come back (and be logged)
915
+ * as a refusal the size of the request.
916
+ */
917
+ test("refuses a multi-select answer of a million entries at once, with one short message, declaring nothing", async () => {
918
+ storedForm = buildForm({ customFieldSettings: { systems: "Optional" } });
919
+
920
+ const started: number = Date.now();
921
+
922
+ const error: Exception | undefined = await refusal(
923
+ submit({
924
+ answers: {
925
+ ...VALID_ANSWERS,
926
+ customFields: {
927
+ Systems: Array.from(
928
+ { length: 1_000_000 },
929
+ (_value: unknown, index: number): string => {
930
+ return `x${index}`;
931
+ },
932
+ ),
933
+ },
934
+ },
935
+ }),
936
+ );
937
+
938
+ expect(Date.now() - started).toBeLessThan(2000);
939
+ expect(error).toBeInstanceOf(BadDataException);
940
+ expect(error?.message).toBe("Systems cannot have more than 100 choices.");
941
+ expect(incidentCreate).not.toHaveBeenCalled();
942
+ });
943
+
944
+ /*
945
+ * Nothing the form's page sends is an object. Checked as an API write
946
+ * would check it, an object would be turned into text twice before it
947
+ * was refused - seconds of the server's time for one with a million
948
+ * keys - so it is refused before anything reads it.
949
+ */
950
+ test("refuses an object answer before reading it, with one short message, declaring nothing", async () => {
951
+ storedForm = buildForm({ customFieldSettings: FORM_QUESTIONS });
952
+
953
+ const answer: Record<string, string> = {};
954
+
955
+ for (let index: number = 0; index < 1_000_000; index++) {
956
+ answer[`k${index}`] = "EU";
957
+ }
958
+
959
+ const started: number = Date.now();
960
+
961
+ const error: Exception | undefined = await refusal(
962
+ submit({
963
+ answers: {
964
+ ...VALID_ANSWERS,
965
+ customFields: { Impact: "High", Region: answer },
966
+ },
967
+ }),
968
+ );
969
+
970
+ expect(Date.now() - started).toBeLessThan(2000);
971
+ expect(error).toBeInstanceOf(BadDataException);
972
+ expect(error?.message).toBe("Region takes one answer, not an object.");
973
+ expect(incidentCreate).not.toHaveBeenCalled();
974
+ expect(reserveFormSubmission).not.toHaveBeenCalled();
975
+ });
976
+
977
+ /*
978
+ * A field mapped from a monitor field takes the monitor's value once the
979
+ * incident has a monitor - and a form's incident gets its template's
980
+ * monitors. Such a question is not asked then, so it cannot be required,
981
+ * and an answer sent anyway is dropped like any the form does not ask.
982
+ */
983
+ describe("a field copied from a monitor", () => {
984
+ const VENDOR: IncidentCustomField = (() => {
985
+ const field: IncidentCustomField = customField({
986
+ name: "Vendor",
987
+ variableKey: "vendor",
988
+ customFieldType: CustomFieldType.Text,
989
+ sortOrder: 8,
990
+ });
991
+ field.mapFromResourceType = CustomFieldMappingSourceResource.Monitor;
992
+ field.mapFromCustomFieldName = "Vendor";
993
+ return field;
994
+ })();
995
+
996
+ function templateWithMonitor(): IncidentTemplate {
997
+ const template: IncidentTemplate =
998
+ templateWithSeverity(MINOR_SEVERITY_ID);
999
+ const monitor: Monitor = new Monitor();
1000
+ monitor._id = "e1000000-0000-4000-8000-0000000000f1";
1001
+ template.monitors = [monitor];
1002
+ return template;
1003
+ }
1004
+
1005
+ beforeEach(() => {
1006
+ customFieldFindBy.mockResolvedValue([...PROJECT_FIELDS, VENDOR] as never);
1007
+ templateFindOneBy.mockResolvedValue(templateWithMonitor() as never);
1008
+ });
1009
+
1010
+ test("is not required when the form's template attaches a monitor, and an answer to it is dropped", async () => {
1011
+ storedForm = withTemplate({
1012
+ customFieldSettings: { vendor: "Required", region: "Optional" },
1013
+ });
1014
+
1015
+ await submit({
1016
+ answers: {
1017
+ ...VALID_ANSWERS,
1018
+ customFields: { Vendor: "Azure", Region: "EU" },
1019
+ },
1020
+ });
1021
+
1022
+ expect(createCall().data.customFields).toEqual({ Region: "EU" });
1023
+ });
1024
+
1025
+ test("is still required, and kept, on a form without a template", async () => {
1026
+ storedForm = buildForm({
1027
+ customFieldSettings: { vendor: "Required" },
1028
+ });
1029
+
1030
+ const error: Exception | undefined = await refusal(
1031
+ submit({ answers: { ...VALID_ANSWERS, customFields: {} } }),
1032
+ );
1033
+
1034
+ expect(error?.message).toBe("Vendor is required.");
1035
+
1036
+ await submit({
1037
+ answers: { ...VALID_ANSWERS, customFields: { Vendor: "Azure" } },
1038
+ });
1039
+
1040
+ expect(createCall().data.customFields).toEqual({ Vendor: "Azure" });
1041
+ expect(templateFindOneBy).not.toHaveBeenCalled();
1042
+ });
1043
+ });
1044
+
1045
+ test("ignores custom field answers entirely, and reads no fields, when the form asks none", async () => {
1046
+ await submit({
1047
+ answers: {
1048
+ ...VALID_ANSWERS,
1049
+ customFields: { Impact: "High", Region: "EU" },
1050
+ },
1051
+ });
1052
+
1053
+ expect(createCall().data.customFields).toEqual({});
1054
+ expect(customFieldFindBy).not.toHaveBeenCalled();
1055
+ });
1056
+ });
1057
+
1058
+ /*
1059
+ * A stranger's report is posted to the project's Slack and Teams channels
1060
+ * and rendered for every responder and in owners' emails, and nobody reads
1061
+ * it over first. What the incident stores must not act on its own there:
1062
+ * no Slack mention, no image fetched from the reporter's server, no mermaid
1063
+ * diagram run - while reading exactly as the reporter typed it.
1064
+ */
1065
+ describe("IncidentFormService.submitPublicForm - nothing the reporter wrote acts on its own", () => {
1066
+ const WORD_JOINER: string = "\u2060";
1067
+
1068
+ function withoutJoiners(text: string | undefined): string {
1069
+ return (text || "").split(WORD_JOINER).join("");
1070
+ }
1071
+
1072
+ function lexedTokens(markdown: string): Array<Token> {
1073
+ const tokens: Array<Token> = [];
1074
+
1075
+ marked.walkTokens(marked.lexer(markdown), (token: Token): void => {
1076
+ tokens.push(token);
1077
+ });
1078
+
1079
+ return tokens;
1080
+ }
1081
+
1082
+ function expectNothingFetchedOrRun(markdown: string): void {
1083
+ const tokens: Array<Token> = lexedTokens(markdown);
1084
+
1085
+ expect(
1086
+ tokens.filter((token: Token): boolean => {
1087
+ return token.type === "image";
1088
+ }),
1089
+ ).toEqual([]);
1090
+ expect(
1091
+ tokens.filter((token: Token): boolean => {
1092
+ return (
1093
+ token.type === "code" &&
1094
+ ((token as Tokens.Code).lang || "")
1095
+ .toLowerCase()
1096
+ .startsWith("mermaid")
1097
+ );
1098
+ }),
1099
+ ).toEqual([]);
1100
+ }
1101
+
1102
+ function slackText(markdown: string): string {
1103
+ return JSON.stringify(
1104
+ SlackUtil.getMarkdownBlocks({
1105
+ payloadMarkdownBlock: {
1106
+ _type: "WorkspacePayloadMarkdown",
1107
+ text: markdown,
1108
+ },
1109
+ }),
1110
+ );
1111
+ }
1112
+
1113
+ const LIVE_SLACK_SEQUENCE: RegExp = /<(![a-z]|@[A-Z0-9]|#C)/;
1114
+
1115
+ const HOSTILE_DESCRIPTION: string =
1116
+ "<!here> every order fails <@U0123ABC>\n\n" +
1117
+ "![](https://tracker.example/p.png)\n\n" +
1118
+ "See ![shot][r]\n\n" +
1119
+ "[r]: https://tracker.example/q.png\n\n" +
1120
+ "```mermaid\ngraph TD\n A-->B\n```\n\n" +
1121
+ "[the checkout page](https://shop.example/checkout)";
1122
+
1123
+ test("the title and description keep their text, lose their mentions, images and diagrams", async () => {
1124
+ await submit({
1125
+ answers: {
1126
+ ...VALID_ANSWERS,
1127
+ title: "<!channel> Checkout is down",
1128
+ description: HOSTILE_DESCRIPTION,
1129
+ },
1130
+ });
1131
+
1132
+ const created: Incident = createCall().data;
1133
+
1134
+ expect(created.title).not.toMatch(/<[!@#]/);
1135
+ expect(withoutJoiners(created.title)).toBe("<!channel> Checkout is down");
1136
+
1137
+ expect(created.description).not.toMatch(/<[!@#]/);
1138
+ expectNothingFetchedOrRun(created.description || "");
1139
+ expect(slackText(created.description || "")).not.toMatch(
1140
+ LIVE_SLACK_SEQUENCE,
1141
+ );
1142
+
1143
+ // The reporter's link to the broken page still works.
1144
+ expect(
1145
+ lexedTokens(created.description || "")
1146
+ .filter((token: Token): boolean => {
1147
+ return token.type === "link";
1148
+ })
1149
+ .map((token: Token): string => {
1150
+ return (token as Tokens.Link).href;
1151
+ }),
1152
+ ).toEqual([
1153
+ "https://tracker.example/p.png",
1154
+ "https://tracker.example/q.png",
1155
+ "https://shop.example/checkout",
1156
+ ]);
1157
+ });
1158
+
1159
+ test("the unneutralised description really would have fetched, run and mentioned", () => {
1160
+ const tokens: Array<Token> = lexedTokens(HOSTILE_DESCRIPTION);
1161
+
1162
+ expect(
1163
+ tokens.filter((token: Token): boolean => {
1164
+ return token.type === "image";
1165
+ }),
1166
+ ).toHaveLength(2);
1167
+ expect(slackText(HOSTILE_DESCRIPTION)).toMatch(LIVE_SLACK_SEQUENCE);
1168
+ });
1169
+
1170
+ /*
1171
+ * The dashboard reads a description with another parser than marked
1172
+ * (remark, with footnotes), and shows an image in each of these where
1173
+ * marked sees code or a link definition. What is stored must hold no
1174
+ * image syntax left open, whichever reading is right.
1175
+ */
1176
+ test("a description the renderers read differently still stores no image", async () => {
1177
+ const description: string =
1178
+ "[a]: https://x.example\n ![x](https://tracker.example/p.png)\n\n" +
1179
+ "See[^1]\n\n[^1]: ![y](https://tracker.example/q.png)";
1180
+
1181
+ // marked finds no image in it: the dashboard would have shown two.
1182
+ expect(
1183
+ lexedTokens(description).filter((token: Token): boolean => {
1184
+ return token.type === "image";
1185
+ }),
1186
+ ).toEqual([]);
1187
+
1188
+ await submit({
1189
+ answers: { ...VALID_ANSWERS, description: description },
1190
+ });
1191
+
1192
+ const stored: string = createCall().data.description || "";
1193
+
1194
+ expect(stored).not.toMatch(/(^|[^\\])(\\\\)*!\[/);
1195
+ expect(withoutJoiners(stored).replace(/\\!\[/g, "![")).toBe(description);
1196
+ });
1197
+
1198
+ test("a Markdown answer is neutralised like the description, text answers lose their mentions, and choices are left as chosen", async () => {
1199
+ storedForm = buildForm({
1200
+ customFieldSettings: {
1201
+ details: "Optional",
1202
+ notes: "Optional",
1203
+ region: "Optional",
1204
+ impact: "Optional",
1205
+ },
1206
+ });
1207
+
1208
+ await submit({
1209
+ answers: {
1210
+ ...VALID_ANSWERS,
1211
+ customFields: {
1212
+ Details: "Screenshot: ![](https://tracker.example/p.png) <!here>",
1213
+ Notes: "Ping <#C0123ABC> and <@U0123ABC>",
1214
+ Region: "<!everyone> EU",
1215
+ Impact: "High",
1216
+ },
1217
+ },
1218
+ });
1219
+
1220
+ const customFields: JSONObject = createCall().data.customFields || {};
1221
+
1222
+ expectNothingFetchedOrRun(String(customFields["Details"]));
1223
+ expect(String(customFields["Details"])).not.toMatch(/<[!@#]/);
1224
+ expect(customFields["Notes"]).not.toMatch(/<[!@#]/);
1225
+ expect(withoutJoiners(String(customFields["Notes"]))).toBe(
1226
+ "Ping <#C0123ABC> and <@U0123ABC>",
1227
+ );
1228
+ expect(withoutJoiners(String(customFields["Region"]))).toBe(
1229
+ "<!everyone> EU",
1230
+ );
1231
+ expect(customFields["Region"]).not.toMatch(/<[!@#]/);
1232
+ expect(customFields["Impact"]).toBe("High");
1233
+ });
1234
+
1235
+ test("ordinary answers are stored exactly as typed", async () => {
1236
+ storedForm = buildForm({ customFieldSettings: FORM_QUESTIONS });
1237
+
1238
+ await submit({
1239
+ answers: {
1240
+ ...VALID_ANSWERS,
1241
+ description: "Checkout fails at **step 3**:\n\n```\nError 502\n```",
1242
+ customFields: { Impact: "High", Region: "EU-West (a < b)" },
1243
+ },
1244
+ });
1245
+
1246
+ expect(createCall().data.title).toBe("Checkout is down");
1247
+ expect(createCall().data.description).toBe(
1248
+ "Checkout fails at **step 3**:\n\n```\nError 502\n```",
1249
+ );
1250
+ expect(createCall().data.customFields).toEqual({
1251
+ Impact: "High",
1252
+ Region: "EU-West (a < b)",
1253
+ });
1254
+ });
1255
+
1256
+ /*
1257
+ * Breaking a sequence adds an invisible character, and Incident.title is
1258
+ * a varchar(500): a title the validator accepted can outgrow it.
1259
+ */
1260
+ test("refuses a title that no longer fits the incident once its mentions are broken, declaring nothing", async () => {
1261
+ const title: string = "<!".repeat(250);
1262
+
1263
+ const error: Exception | undefined = await refusal(
1264
+ submit({ answers: { ...VALID_ANSWERS, title: title } }),
1265
+ );
1266
+
1267
+ expect(title.length).toBe(500);
1268
+ expect(error).toBeInstanceOf(BadDataException);
1269
+ expect(error?.message).toBe(INCIDENT_FORM_TITLE_TOO_LONG_MESSAGE);
1270
+ expect(error?.message).toBe("Title cannot be more than 500 characters.");
1271
+ expect(incidentCreate).not.toHaveBeenCalled();
1272
+ });
1273
+
1274
+ test("refuses a title that no longer fits once its image syntax is broken, declaring nothing", async () => {
1275
+ const title: string = "![".repeat(250);
1276
+
1277
+ const error: Exception | undefined = await refusal(
1278
+ submit({ answers: { ...VALID_ANSWERS, title: title } }),
1279
+ );
1280
+
1281
+ expect(error?.message).toBe(INCIDENT_FORM_TITLE_TOO_LONG_MESSAGE);
1282
+ expect(incidentCreate).not.toHaveBeenCalled();
1283
+ expect(reserveFormSubmission).not.toHaveBeenCalled();
1284
+ });
1285
+
1286
+ /*
1287
+ * The title is plain text, but much of OneUptime places it into Markdown
1288
+ * as it is: a grouping rule makes it (its first 50 characters) the title
1289
+ * of a new episode, whose feed items quote it, and so does the incident's
1290
+ * own "Added to episode" item. So image syntax in it is broken as its
1291
+ * mentions are - invisibly, so it reads as typed everywhere.
1292
+ */
1293
+ test("an image in the title is stored broken, so no feed that quotes the title shows it", async () => {
1294
+ const typed: string =
1295
+ "<!channel> ![](https://tracker.example/p.png) Checkout is down";
1296
+
1297
+ await submit({ answers: { ...VALID_ANSWERS, title: typed } });
1298
+
1299
+ const title: string = createCall().data.title || "";
1300
+
1301
+ expect(title).not.toMatch(/!\[|<[!@#]/);
1302
+ expect(withoutJoiners(title)).toBe(typed);
1303
+
1304
+ const quoted: Array<string> = [
1305
+ `#### Episode #1 Created\n\n**${title.substring(0, 50)}**\n\n`,
1306
+ `**Incident INC-42** added to episode: ${title}`,
1307
+ `Added to **Episode #1**: ${title.substring(0, 50)}`,
1308
+ ];
1309
+
1310
+ for (const markdown of quoted) {
1311
+ expectNothingFetchedOrRun(markdown);
1312
+ expect(slackText(markdown)).not.toMatch(LIVE_SLACK_SEQUENCE);
1313
+ }
1314
+
1315
+ for (const html of renderAsDashboard(quoted)) {
1316
+ expect(html).not.toContain("<img");
1317
+ }
1318
+
1319
+ // Unbroken, the same shapes really do show the image.
1320
+ expect(
1321
+ lexedTokens(`**Incident INC-42** added to episode: ${typed}`).filter(
1322
+ (token: Token): boolean => {
1323
+ return token.type === "image";
1324
+ },
1325
+ ),
1326
+ ).toHaveLength(1);
1327
+ });
1328
+
1329
+ test("the submission record keeps the reporter's name as typed", async () => {
1330
+ await submit({
1331
+ answers: { ...VALID_ANSWERS, reporterName: "<@U0123ABC>" },
1332
+ });
1333
+
1334
+ const createBy: { data: IncidentFormSubmission } = submissionCreate.mock
1335
+ .calls[0]![0] as never;
1336
+
1337
+ expect(createBy.data.reporterName).toBe("<@U0123ABC>");
1338
+ });
1339
+
1340
+ test("the private note mentions nobody in Slack, whatever the names say", async () => {
1341
+ storedForm = buildForm({ name: "<!channel> Reports" });
1342
+
1343
+ await submit({
1344
+ answers: {
1345
+ ...VALID_ANSWERS,
1346
+ reporterName: "<@U0123ABC> <!here>",
1347
+ reporterEmail: "#ops@corp.example",
1348
+ },
1349
+ });
1350
+
1351
+ const note: string = (
1352
+ noteCreate.mock.calls[0]![0] as { data: IncidentInternalNote }
1353
+ ).data.note!;
1354
+
1355
+ expect(note).not.toMatch(/<[!@#]/);
1356
+ expect(slackText(note)).not.toMatch(LIVE_SLACK_SEQUENCE);
1357
+ // The "#" escaped in the link, where it would begin a fragment.
1358
+ expect(slackText(note)).toContain(
1359
+ "<mailto:%23ops@corp.example|#ops@corp.example>",
1360
+ );
1361
+
1362
+ const html: string = await Markdown.convertToHTML(
1363
+ note,
1364
+ MarkdownContentType.Email,
1365
+ );
1366
+
1367
+ expect(html).toContain('href="mailto:%23ops@corp.example"');
1368
+ });
1369
+ });
1370
+
1371
+ describe("neutralizeIncidentFormReport", () => {
1372
+ test("leaves the reporter's name and address, and an answer to a field it is not told about, alone", () => {
1373
+ const report: ValidatedIncidentFormSubmission =
1374
+ neutralizeIncidentFormReport({
1375
+ answers: {
1376
+ title: "<!here>",
1377
+ reporterName: "<@U1>",
1378
+ reporterEmail: "jane@example.com",
1379
+ customFields: { Unlisted: "<!channel>" },
1380
+ },
1381
+ askedDefinitions: [],
1382
+ });
1383
+
1384
+ expect(report.title).toBe("<\u2060!here>");
1385
+ expect(report.reporterName).toBe("<@U1>");
1386
+ expect(report.reporterEmail).toBe("jane@example.com");
1387
+ expect(report.customFields).toEqual({ Unlisted: "<!channel>" });
1388
+ });
1389
+
1390
+ test("treats a field of a type this version does not know as text, as the form asks it", () => {
1391
+ const report: ValidatedIncidentFormSubmission =
1392
+ neutralizeIncidentFormReport({
1393
+ answers: {
1394
+ title: "Down",
1395
+ customFields: { Legacy: "<!channel>", Count: "3" },
1396
+ },
1397
+ askedDefinitions: [
1398
+ { name: "Legacy", customFieldType: null },
1399
+ { name: "Count", customFieldType: CustomFieldType.Number },
1400
+ ],
1401
+ });
1402
+
1403
+ expect(report.customFields).toEqual({
1404
+ Legacy: "<\u2060!channel>",
1405
+ Count: "3",
1406
+ });
1407
+ });
1408
+
1409
+ test("never mutates the validated answers", () => {
1410
+ const answers: ValidatedIncidentFormSubmission = {
1411
+ title: "<!here>",
1412
+ description: "![x](https://t.example/p.png)",
1413
+ customFields: { Notes: "<@U1>" },
1414
+ };
1415
+
1416
+ neutralizeIncidentFormReport({
1417
+ answers: answers,
1418
+ askedDefinitions: [
1419
+ { name: "Notes", customFieldType: CustomFieldType.LongText },
1420
+ ],
1421
+ });
1422
+
1423
+ expect(answers).toEqual({
1424
+ title: "<!here>",
1425
+ description: "![x](https://t.example/p.png)",
1426
+ customFields: { Notes: "<@U1>" },
1427
+ });
1428
+ });
1429
+
1430
+ test("stores an answer to a field named __proto__ as an answer", () => {
1431
+ const customFields: JSONObject = {};
1432
+ Object.defineProperty(customFields, "__proto__", {
1433
+ value: "<!here>",
1434
+ enumerable: true,
1435
+ writable: true,
1436
+ configurable: true,
1437
+ });
1438
+
1439
+ const report: ValidatedIncidentFormSubmission =
1440
+ neutralizeIncidentFormReport({
1441
+ answers: { title: "Down", customFields: customFields },
1442
+ askedDefinitions: [
1443
+ { name: "__proto__", customFieldType: CustomFieldType.Text },
1444
+ ],
1445
+ });
1446
+
1447
+ expect(Object.getPrototypeOf(report.customFields)).toBe(Object.prototype);
1448
+ expect(
1449
+ Object.getOwnPropertyDescriptor(report.customFields, "__proto__")?.value,
1450
+ ).toBe("<\u2060!here>");
1451
+ });
1452
+ });
1453
+
1454
+ describe("IncidentFormService.submitPublicForm - the reporter", () => {
1455
+ test.each([
1456
+ ["no name", { reporterName: undefined }, "Your Name is required."],
1457
+ ["no email", { reporterEmail: undefined }, "Your Email is required."],
1458
+ [
1459
+ "an address that is not one",
1460
+ { reporterEmail: "jane at example" },
1461
+ "Your Email is not a valid email address.",
1462
+ ],
1463
+ ])(
1464
+ "refuses %s when the form requires the reporter's details",
1465
+ async (
1466
+ _label: string,
1467
+ change: Record<string, string | undefined>,
1468
+ message: string,
1469
+ ) => {
1470
+ const answers: JSONObject = { ...VALID_ANSWERS };
1471
+
1472
+ for (const [key, value] of Object.entries(change)) {
1473
+ if (value === undefined) {
1474
+ delete answers[key];
1475
+ } else {
1476
+ answers[key] = value;
1477
+ }
1478
+ }
1479
+
1480
+ const error: Exception | undefined = await refusal(submit({ answers }));
1481
+
1482
+ expect(error).toBeInstanceOf(BadDataException);
1483
+ expect(error?.message).toBe(message);
1484
+ expect(incidentCreate).not.toHaveBeenCalled();
1485
+ },
1486
+ );
1487
+
1488
+ test("reports every problem at once, as one message", async () => {
1489
+ const error: Exception | undefined = await refusal(
1490
+ submit({ answers: { title: "", reporterEmail: "nope" } }),
1491
+ );
1492
+
1493
+ expect(error?.message).toBe(
1494
+ formatIncidentFormSubmissionErrors([
1495
+ "Title is required.",
1496
+ "Your Name is required.",
1497
+ "Your Email is not a valid email address.",
1498
+ ]),
1499
+ );
1500
+ });
1501
+
1502
+ test("refuses a body without answers", async () => {
1503
+ const error: Exception | undefined = await refusal(
1504
+ submit({ answers: undefined }),
1505
+ );
1506
+
1507
+ expect(error).toBeInstanceOf(BadDataException);
1508
+ expect(error?.message).toBe(
1509
+ "The submission must be an object holding the form's answers.",
1510
+ );
1511
+ expect(incidentCreate).not.toHaveBeenCalled();
1512
+ });
1513
+ });
1514
+
1515
+ /*
1516
+ * The template's owners go with the create, for IncidentService to add once
1517
+ * the incident's Slack and Teams channels exist - so their own hooks invite
1518
+ * them to those channels - and to notify. Added by the form after the
1519
+ * create returned, they raced the channel's creation and were never
1520
+ * invited (IncidentTemplateOwnersOnDeclare.test.ts pins the other half).
1521
+ */
1522
+ describe("IncidentFormService.submitPublicForm - the template's owners", () => {
1523
+ function ids(value: unknown): Array<string> {
1524
+ return (value as Array<ObjectID>).map((id: ObjectID): string => {
1525
+ return id.toString();
1526
+ });
1527
+ }
1528
+
1529
+ test("are handed to the create, to be added once the incident's channels exist, and notified", async () => {
1530
+ storedForm = withTemplate();
1531
+
1532
+ await submit();
1533
+
1534
+ const miscDataProps: JSONObject = createCall().miscDataProps!;
1535
+
1536
+ expect(Object.keys(miscDataProps).sort()).toEqual(
1537
+ ["notifyOwners", "ownerTeams", "ownerUsers"].sort(),
1538
+ );
1539
+ expect(ids(miscDataProps["ownerUsers"])).toEqual(OWNER_USER_IDS);
1540
+ expect(ids(miscDataProps["ownerTeams"])).toEqual([OWNER_TEAM_ID]);
1541
+ expect(miscDataProps["notifyOwners"]).toBe(true);
1542
+ expect(createCall().props).toEqual({ isRoot: true });
1543
+ });
1544
+
1545
+ test("are never added by the form itself, after the create returned", async () => {
1546
+ storedForm = withTemplate();
1547
+
1548
+ await submit();
1549
+
1550
+ expect(addOwners).not.toHaveBeenCalled();
1551
+ });
1552
+
1553
+ test("are read for the form's template in the form's project, as root", async () => {
1554
+ storedForm = withTemplate();
1555
+
1556
+ await submit();
1557
+
1558
+ for (const findBy of [
1559
+ ownerUserFindBy.mock.calls[0]![0],
1560
+ ownerTeamFindBy.mock.calls[0]![0],
1561
+ ] as Array<{
1562
+ query: Record<string, unknown>;
1563
+ props: Record<string, unknown>;
1564
+ }>) {
1565
+ expect(String(findBy.query["incidentTemplateId"])).toBe(TEMPLATE_ID);
1566
+ expect(String(findBy.query["projectId"])).toBe(PROJECT_ID.toString());
1567
+ expect(findBy.props).toEqual({ isRoot: true });
1568
+ }
1569
+ });
1570
+
1571
+ test("with only owner users, hands them and no teams", async () => {
1572
+ storedForm = withTemplate();
1573
+ ownerTeamFindBy.mockResolvedValue([] as never);
1574
+
1575
+ await submit();
1576
+
1577
+ expect(createCall().miscDataProps).toEqual({
1578
+ ownerUsers: expect.any(Array),
1579
+ notifyOwners: true,
1580
+ });
1581
+ expect(ids(createCall().miscDataProps!["ownerUsers"])).toEqual(
1582
+ OWNER_USER_IDS,
1583
+ );
1584
+ });
1585
+
1586
+ test("hands nothing when the template has no owners", async () => {
1587
+ storedForm = withTemplate();
1588
+ ownerUserFindBy.mockResolvedValue([] as never);
1589
+ ownerTeamFindBy.mockResolvedValue([] as never);
1590
+
1591
+ await submit();
1592
+
1593
+ expect(createCall()).not.toHaveProperty("miscDataProps");
1594
+ });
1595
+
1596
+ test("reads no owners, and hands none, for a form without a template", async () => {
1597
+ await submit();
1598
+
1599
+ expect(ownerUserFindBy).not.toHaveBeenCalled();
1600
+ expect(ownerTeamFindBy).not.toHaveBeenCalled();
1601
+ expect(createCall()).not.toHaveProperty("miscDataProps");
1602
+ });
1603
+
1604
+ test("a failure reading them is logged, and the incident is declared without them", async () => {
1605
+ storedForm = withTemplate();
1606
+ ownerTeamFindBy.mockRejectedValue(new Error("read failed"));
1607
+
1608
+ await expect(submit()).resolves.toEqual({
1609
+ incidentNumber: "INC-42",
1610
+ successMessage: "Thanks - **we are on it**.",
1611
+ });
1612
+ expect(createCall()).not.toHaveProperty("miscDataProps");
1613
+ expect(logger.error).toHaveBeenCalledWith(
1614
+ expect.stringContaining(
1615
+ "could not read the owners of the template of incident form",
1616
+ ),
1617
+ expect.objectContaining({ incidentFormId: FORM_ID }),
1618
+ );
1619
+ expect(noteCreate).toHaveBeenCalledTimes(1);
1620
+ });
1621
+ });
1622
+
1623
+ describe("IncidentFormService.submitPublicForm - the submission record", () => {
1624
+ test("names the form, the incident and the reporter, with the email lowercased", async () => {
1625
+ await submit();
1626
+
1627
+ expect(submissionCreate).toHaveBeenCalledTimes(1);
1628
+
1629
+ const createBy: {
1630
+ data: IncidentFormSubmission;
1631
+ props: Record<string, unknown>;
1632
+ } = submissionCreate.mock.calls[0]![0] as never;
1633
+
1634
+ expect(createBy.props).toEqual({ isRoot: true });
1635
+ expect(createBy.data).toBeInstanceOf(IncidentFormSubmission);
1636
+ expect(createBy.data.projectId?.toString()).toBe(PROJECT_ID.toString());
1637
+ expect(createBy.data.incidentFormId?.toString()).toBe(FORM_ID);
1638
+ expect(createBy.data.incidentId?.toString()).toBe(INCIDENT_ID);
1639
+ expect(createBy.data.reporterName).toBe("Jane Doe");
1640
+ expect(createBy.data.reporterEmail).toBeInstanceOf(Email);
1641
+ expect(createBy.data.reporterEmail?.toString()).toBe(
1642
+ "jane.doe@example.com",
1643
+ );
1644
+ });
1645
+
1646
+ test("has no reporter for an anonymous report", async () => {
1647
+ storedForm = buildForm({ isReporterDetailsRequired: false });
1648
+
1649
+ await submit({ answers: { title: "Checkout is down" } });
1650
+
1651
+ const createBy: { data: IncidentFormSubmission } = submissionCreate.mock
1652
+ .calls[0]![0] as never;
1653
+
1654
+ expect(createBy.data.reporterName).toBeUndefined();
1655
+ expect(createBy.data.reporterEmail).toBeUndefined();
1656
+ expect(createBy.data.incidentId?.toString()).toBe(INCIDENT_ID);
1657
+ });
1658
+ });
1659
+
1660
+ describe("IncidentFormService.submitPublicForm - the private note", () => {
1661
+ function noteCall(): {
1662
+ data: IncidentInternalNote;
1663
+ props: Record<string, unknown>;
1664
+ } {
1665
+ expect(noteCreate).toHaveBeenCalledTimes(1);
1666
+
1667
+ return noteCreate.mock.calls[0]![0] as never;
1668
+ }
1669
+
1670
+ test("is a private note on the incident, written as root", async () => {
1671
+ await submit();
1672
+
1673
+ expect(noteCall().props).toEqual({ isRoot: true });
1674
+ expect(noteCall().data).toBeInstanceOf(IncidentInternalNote);
1675
+ expect(noteCall().data.projectId?.toString()).toBe(PROJECT_ID.toString());
1676
+ expect(noteCall().data.incidentId?.toString()).toBe(INCIDENT_ID);
1677
+ expect(noteCall().data.createdByUserId).toBeUndefined();
1678
+ });
1679
+
1680
+ test("names the form and the reporter", async () => {
1681
+ await submit();
1682
+
1683
+ expect(noteCall().data.note).toBe(
1684
+ "Reported through the incident form **Report a Problem** by Jane Doe (<jane.doe@example.com>).",
1685
+ );
1686
+ });
1687
+
1688
+ test("says anonymously when the reporter gave no details", async () => {
1689
+ storedForm = buildForm({ isReporterDetailsRequired: false });
1690
+
1691
+ await submit({ answers: { title: "Checkout is down" } });
1692
+
1693
+ expect(noteCall().data.note).toBe(
1694
+ "Reported anonymously through the incident form **Report a Problem**.",
1695
+ );
1696
+ });
1697
+
1698
+ /*
1699
+ * The note is posted as it is - nobody reads it over first - so a
1700
+ * reporter's name must arrive as the characters they typed: never a link
1701
+ * to click, never an image fetched when a responder opens the incident,
1702
+ * never restyling the sentence.
1703
+ */
1704
+ test("escapes a hostile name and email so they read as typed", async () => {
1705
+ storedForm = buildForm({ isReporterDetailsRequired: false });
1706
+
1707
+ await submit({
1708
+ answers: {
1709
+ title: "Checkout is down",
1710
+ reporterName:
1711
+ "[x](javascript:alert(1)) **bold** ![p](https://t.example/p) <img>",
1712
+ reporterEmail: "a_b+c@example.com",
1713
+ },
1714
+ });
1715
+
1716
+ const note: string = noteCall().data.note || "";
1717
+
1718
+ expect(note).toBe(
1719
+ "Reported through the incident form **Report a Problem** by " +
1720
+ "\\[x\\]\\(javascript:alert\\(1\\)\\) \\*\\*bold\\*\\* \\!\\[p\\]\\(https://t.example/p\\) \\<img\\> " +
1721
+ "(<a_b+c@example.com>).",
1722
+ );
1723
+ expect(note).not.toContain("[x](");
1724
+ expect(note).not.toContain("**bold**");
1725
+ expect(note).not.toContain("![p](");
1726
+ });
1727
+ });
1728
+
1729
+ describe("getIncidentFormReporterNote", () => {
1730
+ test.each([
1731
+ [
1732
+ "a name and an email",
1733
+ { reporterName: "Jane", reporterEmail: "jane@example.com" },
1734
+ "Reported through the incident form **Report a Problem** by Jane (<jane@example.com>).",
1735
+ ],
1736
+ [
1737
+ "only a name",
1738
+ { reporterName: "Jane" },
1739
+ "Reported through the incident form **Report a Problem** by Jane.",
1740
+ ],
1741
+ [
1742
+ "only an email",
1743
+ { reporterEmail: "jane@example.com" },
1744
+ "Reported through the incident form **Report a Problem** by <jane@example.com>.",
1745
+ ],
1746
+ [
1747
+ "neither",
1748
+ {},
1749
+ "Reported anonymously through the incident form **Report a Problem**.",
1750
+ ],
1751
+ [
1752
+ "blank details",
1753
+ { reporterName: " ", reporterEmail: null },
1754
+ "Reported anonymously through the incident form **Report a Problem**.",
1755
+ ],
1756
+ ])(
1757
+ "with %s",
1758
+ (
1759
+ _label: string,
1760
+ reporter: { reporterName?: string | null; reporterEmail?: string | null },
1761
+ note: string,
1762
+ ) => {
1763
+ expect(
1764
+ getIncidentFormReporterNote({
1765
+ formName: "Report a Problem",
1766
+ ...reporter,
1767
+ }),
1768
+ ).toBe(note);
1769
+ },
1770
+ );
1771
+
1772
+ test("escapes the form's name too, which sits inside the note's own bold", () => {
1773
+ expect(
1774
+ getIncidentFormReporterNote({ formName: "IT ** Help_desk [EU]" }),
1775
+ ).toBe(
1776
+ "Reported anonymously through the incident form **IT \\*\\* Help\\_desk \\[EU\\]**.",
1777
+ );
1778
+ });
1779
+
1780
+ test("reads well without a form name", () => {
1781
+ expect(getIncidentFormReporterNote({ reporterName: "Jane" })).toBe(
1782
+ "Reported through an incident form by Jane.",
1783
+ );
1784
+ });
1785
+
1786
+ test("keeps a name on one line", () => {
1787
+ expect(
1788
+ getIncidentFormReporterNote({
1789
+ formName: "Form",
1790
+ reporterName: "Jane\n# Heading",
1791
+ }),
1792
+ ).toBe("Reported through the incident form **Form** by Jane \\# Heading.");
1793
+ });
1794
+
1795
+ /*
1796
+ * The note reaches the owners by email (SendNotePostedNotification renders
1797
+ * it with marked) and the incident's Slack channels (slackify-markdown).
1798
+ * Written backslash-escaped, the address made marked restart its link
1799
+ * after each escape: mary\-jane.watson@... linked mailto:jane.watson@...,
1800
+ * a colleague's mailbox. Every link must be the whole address the reporter
1801
+ * gave - hyphen, underscore, plus, apostrophe or hyphenated domain.
1802
+ */
1803
+ describe("links the reporter's whole address", () => {
1804
+ const ADDRESSES: Array<string> = [
1805
+ "mary-jane.watson@corp.example",
1806
+ "first_last@company.com",
1807
+ "ops+alerts@company.com",
1808
+ "jane@my-company.com",
1809
+ "o'brien@company.com",
1810
+ "a_b+c@example.com",
1811
+ ];
1812
+
1813
+ const REPORTERS: Array<[string, string | undefined]> = [];
1814
+
1815
+ for (const address of ADDRESSES) {
1816
+ REPORTERS.push([address, "Jane Doe"]);
1817
+ REPORTERS.push([address, undefined]);
1818
+ }
1819
+
1820
+ function hrefs(html: string): Array<string> {
1821
+ return Array.from(html.matchAll(/href="([^"]*)"/g)).map(
1822
+ (match: RegExpMatchArray): string => {
1823
+ return match[1]!;
1824
+ },
1825
+ );
1826
+ }
1827
+
1828
+ test.each(REPORTERS)(
1829
+ "in the owners' email: %s (name %s)",
1830
+ async (address: string, reporterName: string | undefined) => {
1831
+ const note: string = getIncidentFormReporterNote({
1832
+ formName: "Report a Problem",
1833
+ reporterName: reporterName,
1834
+ reporterEmail: address,
1835
+ });
1836
+
1837
+ const html: string = await Markdown.convertToHTML(
1838
+ note,
1839
+ MarkdownContentType.Email,
1840
+ );
1841
+
1842
+ expect(hrefs(html)).toEqual([
1843
+ `mailto:${address.replace(/'/g, "&#39;")}`,
1844
+ ]);
1845
+ // The address reads as typed, too.
1846
+ expect(html).toContain(`>${address.replace(/'/g, "&#39;")}</a>`);
1847
+ },
1848
+ );
1849
+
1850
+ test.each(REPORTERS)(
1851
+ "in Slack: %s (name %s)",
1852
+ (address: string, reporterName: string | undefined) => {
1853
+ const note: string = getIncidentFormReporterNote({
1854
+ formName: "Report a Problem",
1855
+ reporterName: reporterName,
1856
+ reporterEmail: address,
1857
+ });
1858
+
1859
+ const sections: string = JSON.stringify(
1860
+ SlackUtil.getMarkdownBlocks({
1861
+ payloadMarkdownBlock: {
1862
+ _type: "WorkspacePayloadMarkdown",
1863
+ text: note,
1864
+ },
1865
+ }),
1866
+ );
1867
+
1868
+ expect(sections).toContain(
1869
+ JSON.stringify(`<mailto:${address}|${address}>`).slice(1, -1),
1870
+ );
1871
+ },
1872
+ );
1873
+
1874
+ // Where a mail client sends a mailto: link: its path, nothing after it.
1875
+ function mailtoAddress(href: string): string {
1876
+ const link: URL = new URL(href);
1877
+
1878
+ expect(link.protocol).toBe("mailto:");
1879
+ expect(link.search).toBe("");
1880
+ expect(link.hash).toBe("");
1881
+
1882
+ return decodeURIComponent(link.pathname);
1883
+ }
1884
+
1885
+ // A link as slackify writes it: <address|text>.
1886
+ const SLACK_MAILTO_LINK: RegExp = /<mailto:([^|>]+)\|([^>]+)>/;
1887
+
1888
+ /*
1889
+ * An autolink cannot carry every valid address whole: in a mailto: link
1890
+ * a mail client reads "#" as a fragment, "?" as headers and "%41" as an
1891
+ * escape, so <a%41@example.com> would write to aA@example.com - and one
1892
+ * starting with "!" or "#" would open like a Slack control sequence.
1893
+ * The renderers differ too: marked (the owners' email) drops "^" from
1894
+ * its autolink rule, so <jane^doe@corp.example> linked doe@corp.example,
1895
+ * and micromark (the dashboard, and slackify) takes no "!" in one, so
1896
+ * <first.last!ops@corp.example> linked ops@corp.example. Such an address
1897
+ * is linked explicitly, what a mailto: link may not carry as it is
1898
+ * percent-encoded: it still writes to exactly the address given.
1899
+ */
1900
+ test.each([
1901
+ ["!bang@example.com"],
1902
+ ["#ops@corp.example"],
1903
+ ["a#b@example.com"],
1904
+ ["a?b@example.com"],
1905
+ ["a%41@example.com"],
1906
+ ["jane^doe@corp.example"],
1907
+ ["^jane@corp.example"],
1908
+ ["jane^@corp.example"],
1909
+ ["first.last!ops@corp.example"],
1910
+ ["a/b@example.com"],
1911
+ ["a=b@example.com"],
1912
+ ["a{b}@example.com"],
1913
+ ["a|b@example.com"],
1914
+ ["a`b@example.com"],
1915
+ ])(
1916
+ "links %s to itself, not to what a mail client would make of it",
1917
+ async (address: string) => {
1918
+ const note: string = getIncidentFormReporterNote({
1919
+ formName: "Report a Problem",
1920
+ reporterName: "Jane Doe",
1921
+ reporterEmail: address,
1922
+ });
1923
+
1924
+ expect(note).not.toContain(`<${address}>`);
1925
+
1926
+ const html: string = await Markdown.convertToHTML(
1927
+ note,
1928
+ MarkdownContentType.Email,
1929
+ );
1930
+
1931
+ const links: Array<string> = hrefs(html);
1932
+
1933
+ expect(links).toHaveLength(1);
1934
+ expect(mailtoAddress(links[0]!)).toBe(address);
1935
+ expect(html).toContain(`>${address}</a>`);
1936
+
1937
+ const slack: string = JSON.stringify(
1938
+ SlackUtil.getMarkdownBlocks({
1939
+ payloadMarkdownBlock: {
1940
+ _type: "WorkspacePayloadMarkdown",
1941
+ text: note,
1942
+ },
1943
+ }),
1944
+ );
1945
+
1946
+ const slackLink: RegExpExecArray | null = SLACK_MAILTO_LINK.exec(slack);
1947
+
1948
+ expect(slackLink).not.toBeNull();
1949
+ expect(mailtoAddress(`mailto:${slackLink![1]!}`)).toBe(address);
1950
+ expect(slackLink![2]).toBe(address);
1951
+ },
1952
+ );
1953
+
1954
+ // What a mailto: link may carry as it is (RFC 6068), the rest encoded.
1955
+ const MAILTO_ADDRESS: RegExp =
1956
+ /^(?:[A-Za-z0-9\-._~!$'()*+,;:@]|%[0-9A-F]{2})+$/;
1957
+
1958
+ // The address's link at the end of a note, written either way.
1959
+ const NOTE_EXPLICIT_LINK: RegExp = /\]\((mailto:[^)]*)\)\)\.$/;
1960
+ const NOTE_AUTOLINK: RegExp = /\(<([^>]+)>\)\.$/;
1961
+
1962
+ function withoutEntities(text: string): string {
1963
+ return text
1964
+ .replace(/&lt;/g, "<")
1965
+ .replace(/&gt;/g, ">")
1966
+ .replace(/&quot;/g, '"')
1967
+ .replace(/&#39;/g, "'")
1968
+ .replace(/&amp;/g, "&");
1969
+ }
1970
+
1971
+ /*
1972
+ * Every character an address may hold (WHOLE_EMAIL_ADDRESS), at the
1973
+ * start, inside and at the end of its local part: the owners' email,
1974
+ * Slack and the dashboard each link exactly the address given, and the
1975
+ * note's own mailto: link carries nothing a mail client could read as
1976
+ * anything but the address.
1977
+ */
1978
+ test("links an address holding any character an address may to exactly that address, in every renderer", async () => {
1979
+ const addresses: Array<string> = [];
1980
+
1981
+ for (const character of "!#$%&'*+/=?^_`{|}~-") {
1982
+ addresses.push(
1983
+ `${character}jane@corp.example`,
1984
+ `ja${character}ne@corp.example`,
1985
+ `jane${character}@corp.example`,
1986
+ );
1987
+ }
1988
+
1989
+ const notes: Array<string> = addresses.map((address: string): string => {
1990
+ return getIncidentFormReporterNote({
1991
+ formName: "Report a Problem",
1992
+ reporterName: "Jane Doe",
1993
+ reporterEmail: address,
1994
+ });
1995
+ });
1996
+
1997
+ const dashboard: Array<string> = renderAsDashboard(notes);
1998
+
1999
+ for (const [index, address] of addresses.entries()) {
2000
+ const note: string = notes[index]!;
2001
+
2002
+ const link: string =
2003
+ NOTE_EXPLICIT_LINK.exec(note)?.[1] ||
2004
+ `mailto:${NOTE_AUTOLINK.exec(note)![1]}`;
2005
+
2006
+ expect({ address: address, link: link }).toEqual({
2007
+ address: address,
2008
+ link: expect.stringMatching(/^mailto:/),
2009
+ });
2010
+ expect(link.slice("mailto:".length)).toMatch(MAILTO_ADDRESS);
2011
+
2012
+ const email: Array<string> = hrefsOf(
2013
+ await Markdown.convertToHTML(note, MarkdownContentType.Email),
2014
+ );
2015
+
2016
+ expect({ address: address, email: email.map(mailtoAddress) }).toEqual({
2017
+ address: address,
2018
+ email: [address],
2019
+ });
2020
+
2021
+ expect({
2022
+ address: address,
2023
+ dashboard: hrefsOf(dashboard[index]!).map(mailtoAddress),
2024
+ }).toEqual({ address: address, dashboard: [address] });
2025
+
2026
+ const slack: RegExpExecArray | null = SLACK_MAILTO_LINK.exec(
2027
+ JSON.parse(
2028
+ JSON.stringify(
2029
+ SlackUtil.getMarkdownBlocks({
2030
+ payloadMarkdownBlock: {
2031
+ _type: "WorkspacePayloadMarkdown",
2032
+ text: note,
2033
+ },
2034
+ }),
2035
+ ),
2036
+ )[0].text.text as string,
2037
+ );
2038
+
2039
+ expect({
2040
+ address: address,
2041
+ slack: slack
2042
+ ? [
2043
+ mailtoAddress(`mailto:${withoutEntities(slack[1]!)}`),
2044
+ withoutEntities(slack[2]!),
2045
+ ]
2046
+ : null,
2047
+ }).toEqual({ address: address, slack: [address, address] });
2048
+ }
2049
+ });
2050
+
2051
+ test("escapes a value that is not one whole address, rather than linking it", () => {
2052
+ expect(
2053
+ getIncidentFormReporterNote({
2054
+ formName: "Form",
2055
+ reporterEmail: "Jane <jane@example.com>",
2056
+ }),
2057
+ ).toBe(
2058
+ "Reported through the incident form **Form** by Jane \\<jane@example.com\\>.",
2059
+ );
2060
+ });
2061
+ });
2062
+ });
2063
+
2064
+ describe("IncidentFormService.submitPublicForm - after the incident exists", () => {
2065
+ test("a failure recording the submission is logged, and the note still happens", async () => {
2066
+ submissionCreate.mockRejectedValue(new Error("insert failed"));
2067
+
2068
+ await expect(submit()).resolves.toHaveProperty("incidentNumber", "INC-42");
2069
+ expect(noteCreate).toHaveBeenCalledTimes(1);
2070
+ expect(logger.error).toHaveBeenCalledWith(
2071
+ expect.stringContaining("could not record the submission for incident"),
2072
+ expect.anything(),
2073
+ );
2074
+ });
2075
+
2076
+ test("a failure writing the note is logged, not thrown", async () => {
2077
+ noteCreate.mockRejectedValue(new Error("insert failed"));
2078
+
2079
+ await expect(submit()).resolves.toHaveProperty("incidentNumber", "INC-42");
2080
+ expect(logger.error).toHaveBeenCalledWith(
2081
+ expect.stringContaining("could not add the reporter's private note to"),
2082
+ expect.anything(),
2083
+ );
2084
+ });
2085
+
2086
+ test("everything after the incident failing still answers with its number", async () => {
2087
+ storedForm = withTemplate();
2088
+ submissionCreate.mockRejectedValue(new Error("b"));
2089
+ noteCreate.mockRejectedValue(new Error("c"));
2090
+
2091
+ await expect(submit()).resolves.toEqual({
2092
+ incidentNumber: "INC-42",
2093
+ successMessage: "Thanks - **we are on it**.",
2094
+ });
2095
+ });
2096
+ });
2097
+
2098
+ describe("IncidentFormService.submitPublicForm - when the incident cannot be declared", () => {
2099
+ test("answers with a message the reporter can act on, logs the reason, and does nothing else", async () => {
2100
+ incidentCreate.mockRejectedValue(
2101
+ new BadDataException(
2102
+ "Created incident state not found for this project. Please add created incident state from settings.",
2103
+ ),
2104
+ );
2105
+
2106
+ const error: Exception | undefined = await refusal(submit());
2107
+
2108
+ expect(error).toBeInstanceOf(ServerException);
2109
+ expect(error?.code).toBe(500);
2110
+ expect(error?.message).toBe(INCIDENT_FORM_SUBMIT_FAILED_MESSAGE);
2111
+ expect(logger.error).toHaveBeenCalled();
2112
+ expect(addOwners).not.toHaveBeenCalled();
2113
+ expect(submissionCreate).not.toHaveBeenCalled();
2114
+ expect(noteCreate).not.toHaveBeenCalled();
2115
+ });
2116
+
2117
+ test("never hands the reporter an internal message or id", async () => {
2118
+ const internalId: string = ObjectID.generate().toString();
2119
+ incidentCreate.mockRejectedValue(
2120
+ new BadDataException(`Incident Severity ${internalId} is not in project`),
2121
+ );
2122
+
2123
+ const error: Exception | undefined = await refusal(submit());
2124
+
2125
+ expect(error?.message).not.toContain(internalId);
2126
+ });
2127
+ });
2128
+
2129
+ describe("IncidentFormService.submitPublicForm - the answer", () => {
2130
+ test("is the incident's number and the form's success message", async () => {
2131
+ expect(await submit()).toEqual({
2132
+ incidentNumber: "INC-42",
2133
+ successMessage: "Thanks - **we are on it**.",
2134
+ });
2135
+ });
2136
+
2137
+ test("numbers an incident of a project without a prefix with a #", async () => {
2138
+ setNull(createdIncident, "incidentNumberWithPrefix");
2139
+
2140
+ expect((await submit()).incidentNumber).toBe("#42");
2141
+ });
2142
+
2143
+ test("leaves the number out when the incident came back without one", async () => {
2144
+ setNull(createdIncident, "incidentNumberWithPrefix");
2145
+ setNull(createdIncident, "incidentNumber");
2146
+
2147
+ expect(await submit()).not.toHaveProperty("incidentNumber");
2148
+ });
2149
+
2150
+ test.each([
2151
+ ["empty", ""],
2152
+ ["blank", " \n "],
2153
+ ["unset", null],
2154
+ ])(
2155
+ "leaves out a success message that is %s",
2156
+ async (_label: string, successMessage: string | null) => {
2157
+ storedForm = buildForm();
2158
+ (storedForm as unknown as Record<string, unknown>)["successMessage"] =
2159
+ successMessage;
2160
+
2161
+ expect(await submit()).toEqual({ incidentNumber: "INC-42" });
2162
+ },
2163
+ );
2164
+
2165
+ test("never includes the incident's id, the form's id or its project", async () => {
2166
+ const sent: string = JSON.stringify(await submit());
2167
+
2168
+ expect(sent).not.toContain(INCIDENT_ID);
2169
+ expect(sent).not.toContain(FORM_ID);
2170
+ expect(sent).not.toContain(PROJECT_ID.toString());
2171
+ });
2172
+ });
2173
+
2174
+ describe("IncidentFormService.submitPublicForm - the checks before anything is declared", () => {
2175
+ test("verifies the captcha, when the instance has one on, with the token and the client address", async () => {
2176
+ captchaEnabled.mockReturnValue(true);
2177
+
2178
+ await submit({ captchaToken: "token-from-hcaptcha" });
2179
+
2180
+ expect(verifyCaptcha).toHaveBeenCalledWith({
2181
+ token: "token-from-hcaptcha",
2182
+ remoteIp: CAPTCHA_IP,
2183
+ });
2184
+ });
2185
+
2186
+ test("does not verify a captcha when the instance has none, even if a token is sent", async () => {
2187
+ await submit({ captchaToken: "token-from-hcaptcha" });
2188
+
2189
+ expect(verifyCaptcha).not.toHaveBeenCalled();
2190
+ expect(incidentCreate).toHaveBeenCalledTimes(1);
2191
+ });
2192
+
2193
+ test("refuses a failed captcha before reading or declaring anything", async () => {
2194
+ captchaEnabled.mockReturnValue(true);
2195
+ storedForm = buildForm({
2196
+ customFieldSettings: FORM_QUESTIONS,
2197
+ allowReporterToChooseSeverity: true,
2198
+ });
2199
+ verifyCaptcha.mockRejectedValue(
2200
+ new BadDataException("Captcha verification failed. Please try again."),
2201
+ );
2202
+
2203
+ const error: Exception | undefined = await refusal(submit());
2204
+
2205
+ expect(error).toBeInstanceOf(BadDataException);
2206
+ expect(error?.message).toBe(
2207
+ "Captcha verification failed. Please try again.",
2208
+ );
2209
+ expect(customFieldFindBy).not.toHaveBeenCalled();
2210
+ expect(severityFindBy).not.toHaveBeenCalled();
2211
+ expect(incidentCreate).not.toHaveBeenCalled();
2212
+ });
2213
+
2214
+ test("checks the captcha before the answers", async () => {
2215
+ captchaEnabled.mockReturnValue(true);
2216
+ verifyCaptcha.mockRejectedValue(
2217
+ new BadDataException(
2218
+ "Captcha token is missing. Please complete the verification challenge.",
2219
+ ),
2220
+ );
2221
+
2222
+ const error: Exception | undefined = await refusal(
2223
+ submit({ answers: { title: "" } }),
2224
+ );
2225
+
2226
+ expect(error?.message).toBe(
2227
+ "Captcha token is missing. Please complete the verification challenge.",
2228
+ );
2229
+ });
2230
+
2231
+ test.each([
2232
+ ["a junk link", { shareKey: "junk" }],
2233
+ ["an unknown link", { shareKey: OTHER_SHARE_KEY }],
2234
+ ])(
2235
+ "refuses %s as not available, without the captcha provider or a create",
2236
+ async (_label: string, change: { shareKey: string }) => {
2237
+ captchaEnabled.mockReturnValue(true);
2238
+
2239
+ const error: Exception | undefined = await refusal(submit(change));
2240
+
2241
+ expect(error).toBeInstanceOf(NotFoundException);
2242
+ expect(error?.message).toBe(INCIDENT_FORM_NOT_AVAILABLE_MESSAGE);
2243
+ expect(verifyCaptcha).not.toHaveBeenCalled();
2244
+ expect(incidentCreate).not.toHaveBeenCalled();
2245
+ },
2246
+ );
2247
+
2248
+ test("refuses a form that is turned off as not available", async () => {
2249
+ storedForm = buildForm({ isEnabled: false });
2250
+
2251
+ const error: Exception | undefined = await refusal(submit());
2252
+
2253
+ expect(error).toBeInstanceOf(NotFoundException);
2254
+ expect(error?.message).toBe(INCIDENT_FORM_NOT_AVAILABLE_MESSAGE);
2255
+ expect(incidentCreate).not.toHaveBeenCalled();
2256
+ });
2257
+
2258
+ test("refuses a form of a project off plan with the same answer as an unknown link", async () => {
2259
+ jest
2260
+ .spyOn(IncidentFormService, "isProjectOnPlan")
2261
+ .mockResolvedValue(false as never);
2262
+
2263
+ const offPlan: Exception | undefined = await refusal(submit());
2264
+ const unknown: Exception | undefined = await refusal(
2265
+ submit({ shareKey: OTHER_SHARE_KEY }),
2266
+ );
2267
+
2268
+ expect(offPlan).toBeInstanceOf(NotFoundException);
2269
+ expect([offPlan?.code, offPlan?.message]).toEqual([
2270
+ unknown?.code,
2271
+ unknown?.message,
2272
+ ]);
2273
+ expect(incidentCreate).not.toHaveBeenCalled();
2274
+ });
2275
+
2276
+ test("refuses a network the form does not allow, before the captcha", async () => {
2277
+ captchaEnabled.mockReturnValue(true);
2278
+ storedForm = buildForm({ ipWhitelist: "198.51.100.0/24" });
2279
+
2280
+ const error: Exception | undefined = await refusal(submit());
2281
+
2282
+ expect(error).toBeInstanceOf(ForbiddenException);
2283
+ expect(error?.message).toBe(INCIDENT_FORM_NETWORK_NOT_ALLOWED_MESSAGE);
2284
+ expect(verifyCaptcha).not.toHaveBeenCalled();
2285
+ expect(incidentCreate).not.toHaveBeenCalled();
2286
+ });
2287
+
2288
+ test("lets an allowed network through to declare", async () => {
2289
+ storedForm = buildForm({ ipWhitelist: `198.51.100.0/24\n${CLIENT_IP}` });
2290
+
2291
+ await submit();
2292
+
2293
+ expect(incidentCreate).toHaveBeenCalledTimes(1);
2294
+ });
2295
+
2296
+ test("refuses a request without an address when the form has an allowlist", async () => {
2297
+ storedForm = buildForm({ ipWhitelist: CLIENT_IP });
2298
+
2299
+ expect(await refusal(submit({ clientIp: undefined }))).toBeInstanceOf(
2300
+ ForbiddenException,
2301
+ );
2302
+ });
2303
+
2304
+ test("looks the form up once, by its share key, as root", async () => {
2305
+ await submit();
2306
+
2307
+ expect(formFindOneBy).toHaveBeenCalledTimes(1);
2308
+
2309
+ const findBy: {
2310
+ query: Record<string, unknown>;
2311
+ props: Record<string, unknown>;
2312
+ } = formFindOneBy.mock.calls[0]![0] as never;
2313
+
2314
+ expect(Object.keys(findBy.query)).toEqual(["shareKey"]);
2315
+ expect(String(findBy.query["shareKey"])).toBe(SHARE_KEY);
2316
+ expect(findBy.props).toEqual({ isRoot: true });
2317
+ });
2318
+ });
2319
+
2320
+ /*
2321
+ * The form's hourly ceiling bounds the incidents - and pages - its link can
2322
+ * cause, so only a submission about to declare one may spend it. Were a
2323
+ * refused request to count, anyone holding the link - outside the IP
2324
+ * allowlist, or unable to solve the captcha - could use it up and lock the
2325
+ * form for everybody.
2326
+ */
2327
+ describe("IncidentFormService.submitPublicForm - the form's hourly ceiling", () => {
2328
+ test("is spent once, for the form's link, after every check and right before the incident is declared", async () => {
2329
+ await submit();
2330
+
2331
+ expect(reserveFormSubmission).toHaveBeenCalledTimes(1);
2332
+ expect(reserveFormSubmission).toHaveBeenCalledWith({ shareKey: SHARE_KEY });
2333
+ expect(reserveFormSubmission.mock.invocationCallOrder[0]!).toBeLessThan(
2334
+ incidentCreate.mock.invocationCallOrder[0]!,
2335
+ );
2336
+ });
2337
+
2338
+ test.each([
2339
+ [
2340
+ "a link no form holds",
2341
+ (): void => {
2342
+ storedForm = null;
2343
+ },
2344
+ VALID_ANSWERS,
2345
+ ],
2346
+ [
2347
+ "a form that is turned off",
2348
+ (): void => {
2349
+ storedForm = buildForm({ isEnabled: false });
2350
+ },
2351
+ VALID_ANSWERS,
2352
+ ],
2353
+ [
2354
+ "a project off plan",
2355
+ (): void => {
2356
+ jest
2357
+ .spyOn(IncidentFormService, "isProjectOnPlan")
2358
+ .mockResolvedValue(false as never);
2359
+ },
2360
+ VALID_ANSWERS,
2361
+ ],
2362
+ [
2363
+ "a network the form does not allow",
2364
+ (): void => {
2365
+ storedForm = buildForm({ ipWhitelist: "198.51.100.0/24" });
2366
+ },
2367
+ VALID_ANSWERS,
2368
+ ],
2369
+ [
2370
+ "a failed captcha",
2371
+ (): void => {
2372
+ captchaEnabled.mockReturnValue(true);
2373
+ verifyCaptcha.mockRejectedValue(
2374
+ new BadDataException(
2375
+ "Captcha verification failed. Please try again.",
2376
+ ),
2377
+ );
2378
+ },
2379
+ VALID_ANSWERS,
2380
+ ],
2381
+ [
2382
+ "answers that do not pass",
2383
+ (): void => {
2384
+ // The answers below have no title.
2385
+ },
2386
+ { ...VALID_ANSWERS, title: "" },
2387
+ ],
2388
+ [
2389
+ "no severity left",
2390
+ (): void => {
2391
+ storedForm = buildForm();
2392
+ setNull(storedForm, "incidentSeverityId");
2393
+ },
2394
+ VALID_ANSWERS,
2395
+ ],
2396
+ ])(
2397
+ "is never spent by a submission refused for %s",
2398
+ async (_label: string, arrange: () => void, answers: JSONObject) => {
2399
+ arrange();
2400
+
2401
+ const error: Exception | undefined = await refusal(submit({ answers }));
2402
+
2403
+ expect(error).toBeDefined();
2404
+ expect(reserveFormSubmission).not.toHaveBeenCalled();
2405
+ expect(incidentCreate).not.toHaveBeenCalled();
2406
+ },
2407
+ );
2408
+
2409
+ test("once the hour's allowance is used, nothing is declared and the refusal says when to come back", async () => {
2410
+ storedForm = withTemplate();
2411
+ reserveFormSubmission.mockRejectedValue(
2412
+ new IncidentFormCeilingException(1800),
2413
+ );
2414
+
2415
+ const error: Exception | undefined = await refusal(submit());
2416
+
2417
+ expect(error).toBeInstanceOf(IncidentFormCeilingException);
2418
+ expect(error?.code).toBe(429);
2419
+ expect(error?.message).toBe(INCIDENT_FORM_TOTAL_RATE_LIMIT_MESSAGE);
2420
+ expect((error as IncidentFormCeilingException).retryAfterSeconds).toBe(
2421
+ 1800,
2422
+ );
2423
+ expect(incidentCreate).not.toHaveBeenCalled();
2424
+ expect(ownerUserFindBy).not.toHaveBeenCalled();
2425
+ expect(submissionCreate).not.toHaveBeenCalled();
2426
+ expect(noteCreate).not.toHaveBeenCalled();
2427
+ });
2428
+
2429
+ test("when the ceiling cannot be counted, nothing is declared (503)", async () => {
2430
+ reserveFormSubmission.mockRejectedValue(
2431
+ new ServiceUnavailableException(
2432
+ INCIDENT_FORM_RATE_LIMIT_UNAVAILABLE_MESSAGE,
2433
+ ),
2434
+ );
2435
+
2436
+ const error: Exception | undefined = await refusal(submit());
2437
+
2438
+ expect(error?.code).toBe(503);
2439
+ expect(error?.message).toBe(INCIDENT_FORM_RATE_LIMIT_UNAVAILABLE_MESSAGE);
2440
+ expect(incidentCreate).not.toHaveBeenCalled();
2441
+ });
2442
+ });
2443
+
2444
+ /*
2445
+ * Making the description and the Rich text answers safe to show is the one
2446
+ * step whose cost grows with the Markdown in them, and it cannot refuse a
2447
+ * submission. So it is the last step before the incident is declared: a
2448
+ * submission refused on the way - for its title, its severity or the
2449
+ * form's ceiling - never pays for it, and every one that does is bounded by
2450
+ * that ceiling.
2451
+ */
2452
+ describe("IncidentFormService.submitPublicForm - reading the reporter's Markdown", () => {
2453
+ const WITH_AN_IMAGE: JSONObject = {
2454
+ ...VALID_ANSWERS,
2455
+ description: "Broken: ![x](https://tracker.example/p.png)",
2456
+ };
2457
+
2458
+ test("happens once the form's ceiling is spent, before the incident is declared", async () => {
2459
+ const lex: ReturnType<typeof jest.spyOn> = jest.spyOn(
2460
+ Lexer.prototype,
2461
+ "lex",
2462
+ );
2463
+
2464
+ await submit({ answers: WITH_AN_IMAGE });
2465
+
2466
+ expect(lex).toHaveBeenCalled();
2467
+ expect(reserveFormSubmission.mock.invocationCallOrder[0]!).toBeLessThan(
2468
+ lex.mock.invocationCallOrder[0]!,
2469
+ );
2470
+ expect(lex.mock.invocationCallOrder[0]!).toBeLessThan(
2471
+ incidentCreate.mock.invocationCallOrder[0]!,
2472
+ );
2473
+ expect(createCall().data.description).toBe(
2474
+ "Broken: \\![x](https://tracker.example/p.png)",
2475
+ );
2476
+ });
2477
+
2478
+ test.each([
2479
+ [
2480
+ "a title that no longer fits",
2481
+ (): void => {
2482
+ // The answers below have the title.
2483
+ },
2484
+ { ...WITH_AN_IMAGE, title: "<!".repeat(250) },
2485
+ ],
2486
+ [
2487
+ "no severity left",
2488
+ (): void => {
2489
+ storedForm = buildForm();
2490
+ setNull(storedForm, "incidentSeverityId");
2491
+ },
2492
+ WITH_AN_IMAGE,
2493
+ ],
2494
+ [
2495
+ "the form's ceiling",
2496
+ (): void => {
2497
+ reserveFormSubmission.mockRejectedValue(
2498
+ new IncidentFormCeilingException(1800),
2499
+ );
2500
+ },
2501
+ WITH_AN_IMAGE,
2502
+ ],
2503
+ ])(
2504
+ "never happens for a submission refused for %s",
2505
+ async (_label: string, arrange: () => void, answers: JSONObject) => {
2506
+ arrange();
2507
+
2508
+ const lex: ReturnType<typeof jest.spyOn> = jest.spyOn(
2509
+ Lexer.prototype,
2510
+ "lex",
2511
+ );
2512
+
2513
+ const error: Exception | undefined = await refusal(submit({ answers }));
2514
+
2515
+ expect(error).toBeDefined();
2516
+ expect(lex).not.toHaveBeenCalled();
2517
+ expect(incidentCreate).not.toHaveBeenCalled();
2518
+ },
2519
+ );
2520
+ });