@oneuptime/common 14.0.7 → 14.0.9

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 (1064) hide show
  1. package/Models/DatabaseModels/Incident.ts +267 -1
  2. package/Models/DatabaseModels/IncidentCustomField.ts +202 -0
  3. package/Models/DatabaseModels/IncidentEpisode.ts +30 -0
  4. package/Models/DatabaseModels/IncidentTemplate.ts +126 -1
  5. package/Models/DatabaseModels/Index.ts +2 -0
  6. package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
  7. package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
  8. package/Models/DatabaseModels/Project.ts +55 -5
  9. package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
  10. package/Models/DatabaseModels/RunnerJob.ts +75 -0
  11. package/Models/DatabaseModels/StatusPage.ts +55 -0
  12. package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
  13. package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
  14. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
  15. package/Server/API/AIChatAPI.ts +6 -3
  16. package/Server/API/AIInvestigationAPI.ts +17 -10
  17. package/Server/API/AutoRemediationAPI.ts +207 -25
  18. package/Server/API/IncidentAPI.ts +162 -0
  19. package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
  20. package/Server/API/MicrosoftTeamsAPI.ts +93 -14
  21. package/Server/API/ProxmoxResourceAPI.ts +244 -14
  22. package/Server/API/StatusPageAPI.ts +487 -171
  23. package/Server/API/StatusPageSubscriberAPI.ts +63 -15
  24. package/Server/Infrastructure/GlobalCache.ts +32 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +20 -0
  36. package/Server/Infrastructure/Semaphore.ts +10 -0
  37. package/Server/Middleware/IdentityRateLimit.ts +108 -3
  38. package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
  39. package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
  40. package/Server/Middleware/TelemetryIngest.ts +41 -18
  41. package/Server/Services/AIService.ts +70 -1
  42. package/Server/Services/AlertService.ts +352 -11
  43. package/Server/Services/AlertSeverityService.ts +65 -15
  44. package/Server/Services/AutoRemediationRuleService.ts +1 -1
  45. package/Server/Services/DatabaseService.ts +93 -10
  46. package/Server/Services/IncidentCustomFieldService.ts +276 -1
  47. package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
  48. package/Server/Services/IncidentEpisodeService.ts +15 -0
  49. package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
  50. package/Server/Services/IncidentPublicNoteService.ts +30 -1
  51. package/Server/Services/IncidentService.ts +1788 -142
  52. package/Server/Services/IncidentSeverityService.ts +66 -15
  53. package/Server/Services/IncidentStateTimelineService.ts +13 -0
  54. package/Server/Services/IncidentTemplateService.ts +109 -0
  55. package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
  56. package/Server/Services/KubernetesAiAgentService.ts +1634 -0
  57. package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
  58. package/Server/Services/KubernetesClusterService.ts +129 -21
  59. package/Server/Services/ProjectService.ts +32 -0
  60. package/Server/Services/ProxmoxResourceService.ts +322 -5
  61. package/Server/Services/RunnerJobService.ts +241 -104
  62. package/Server/Services/RunnerService.ts +1 -1
  63. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
  64. package/Server/Services/ScheduledMaintenanceService.ts +805 -19
  65. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
  66. package/Server/Services/SmsService.ts +10 -0
  67. package/Server/Services/StatusPageService.ts +65 -8
  68. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
  69. package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
  70. package/Server/Services/StatusPageSubscriberService.ts +1161 -64
  71. package/Server/Services/TeamComplianceSettingService.ts +1125 -9
  72. package/Server/Types/Markdown.ts +153 -6
  73. package/Server/Types/Workflow/Components/Email.ts +61 -2
  74. package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
  75. package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
  76. package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
  77. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +37 -15
  78. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +67 -24
  79. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +48 -5
  80. package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
  81. package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
  82. package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
  83. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +6 -1
  84. package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
  85. package/Server/Utils/DataSource/EgressGuard.ts +66 -21
  86. package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
  87. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
  88. package/Server/Utils/SSRFProtection.ts +76 -16
  89. package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
  90. package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
  91. package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
  92. package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
  93. package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
  94. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
  95. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
  96. package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
  97. package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
  98. package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
  99. package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
  100. package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
  101. package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
  102. package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
  103. package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
  104. package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
  105. package/Server/Utils/StatusPageResource.ts +51 -20
  106. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
  107. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
  108. package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
  109. package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
  110. package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
  111. package/Server/Utils/VM/VMAPI.ts +1 -0
  112. package/Server/Utils/VM/VMRunner.ts +10 -0
  113. package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
  114. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +684 -47
  115. package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
  116. package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
  117. package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
  118. package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
  119. package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
  120. package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
  121. package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
  122. package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
  123. package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
  124. package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
  125. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
  126. package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
  127. package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
  128. package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
  129. package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
  130. package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
  131. package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
  132. package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
  133. package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
  134. package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
  135. package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
  136. package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
  137. package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
  138. package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
  139. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +91 -19
  140. package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
  141. package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
  142. package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
  143. package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
  144. package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
  145. package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
  146. package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
  147. package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
  148. package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
  149. package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
  150. package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
  151. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
  152. package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
  153. package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
  154. package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
  155. package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
  156. package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
  157. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
  158. package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
  159. package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
  160. package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
  161. package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
  162. package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
  163. package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
  164. package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
  165. package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
  166. package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
  167. package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
  168. package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
  169. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
  170. package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
  171. package/Tests/App/Dashboard/EventNotes.test.tsx +778 -2
  172. package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
  173. package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
  174. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +828 -17
  175. package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
  176. package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
  177. package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
  178. package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
  179. package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
  180. package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
  181. package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
  182. package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
  183. package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
  184. package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
  185. package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
  186. package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
  187. package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
  188. package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
  189. package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
  190. package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
  191. package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
  192. package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
  193. package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
  194. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
  195. package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
  196. package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
  197. package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
  198. package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
  199. package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
  200. package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
  201. package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
  202. package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
  203. package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
  204. package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
  205. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
  206. package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
  207. package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
  208. package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
  209. package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
  210. package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
  211. package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
  212. package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
  213. package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
  214. package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
  215. package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
  216. package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
  217. package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
  218. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
  219. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
  220. package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
  221. package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
  222. package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
  223. package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
  224. package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
  225. package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
  226. package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
  227. package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
  228. package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
  229. package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
  230. package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
  231. package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
  232. package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
  233. package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
  234. package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
  235. package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
  236. package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
  237. package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
  238. package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
  239. package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
  240. package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
  241. package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
  242. package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
  243. package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
  244. package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
  245. package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
  246. package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
  247. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
  248. package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
  249. package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
  250. package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
  251. package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
  252. package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
  253. package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
  254. package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
  255. package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
  256. package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
  257. package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
  258. package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
  259. package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
  260. package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
  261. package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
  262. package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
  263. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
  264. package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
  265. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
  266. package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
  267. package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
  268. package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
  269. package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
  270. package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
  271. package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
  272. package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
  273. package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
  274. package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
  275. package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
  276. package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
  277. package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
  278. package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
  279. package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
  280. package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
  281. package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
  282. package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
  283. package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
  284. package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
  285. package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
  286. package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
  287. package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
  288. package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
  289. package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
  290. package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
  291. package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
  292. package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
  293. package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
  294. package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
  295. package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
  296. package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
  297. package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
  298. package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
  299. package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
  300. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
  301. package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
  302. package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
  303. package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
  304. package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
  305. package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
  306. package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
  307. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
  308. package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
  309. package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
  310. package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
  311. package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
  312. package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
  313. package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
  314. package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
  315. package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
  316. package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
  317. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
  318. package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
  319. package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
  320. package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
  321. package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
  322. package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
  323. package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
  324. package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
  325. package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
  326. package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
  327. package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
  328. package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
  329. package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
  330. package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
  331. package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
  332. package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
  333. package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
  334. package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
  335. package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
  336. package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
  337. package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
  338. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +61 -4
  339. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +98 -2
  340. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +105 -2
  341. package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
  342. package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
  343. package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
  344. package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
  345. package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
  346. package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
  347. package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
  348. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
  349. package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
  350. package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
  351. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
  352. package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
  353. package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
  354. package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
  355. package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
  356. package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
  357. package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
  358. package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
  359. package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
  360. package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
  361. package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
  362. package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
  363. package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
  364. package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
  365. package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
  366. package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
  367. package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
  368. package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
  369. package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
  370. package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
  371. package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
  372. package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
  373. package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
  374. package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
  375. package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
  376. package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
  377. package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
  378. package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
  379. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +48 -15
  380. package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
  381. package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
  382. package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
  383. package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
  384. package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
  385. package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
  386. package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
  387. package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
  388. package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
  389. package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
  390. package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
  391. package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
  392. package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
  393. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
  394. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
  395. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
  396. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
  397. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
  398. package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
  399. package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
  400. package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
  401. package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
  402. package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
  403. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
  404. package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
  405. package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
  406. package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
  407. package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
  408. package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
  409. package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
  410. package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
  411. package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
  412. package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
  413. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
  414. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
  415. package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
  416. package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
  417. package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
  418. package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
  419. package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
  420. package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
  421. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
  422. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
  423. package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
  424. package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
  425. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
  426. package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
  427. package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
  428. package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
  429. package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
  430. package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
  431. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
  432. package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
  433. package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
  434. package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
  435. package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
  436. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
  437. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
  438. package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
  439. package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
  440. package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
  441. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
  442. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
  443. package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
  444. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
  445. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
  446. package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
  447. package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
  448. package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
  449. package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
  450. package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
  451. package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
  452. package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
  453. package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
  454. package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
  455. package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
  456. package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
  457. package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
  458. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
  459. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +45 -3
  460. package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
  461. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
  462. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
  463. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
  464. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
  465. package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
  466. package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
  467. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
  468. package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
  469. package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
  470. package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
  471. package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
  472. package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
  473. package/Tests/Types/SafeHtml.test.ts +102 -0
  474. package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
  475. package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
  476. package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
  477. package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
  478. package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
  479. package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
  480. package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
  481. package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
  482. package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
  483. package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
  484. package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
  485. package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
  486. package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
  487. package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
  488. package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
  489. package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
  490. package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
  491. package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
  492. package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
  493. package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
  494. package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
  495. package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
  496. package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
  497. package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
  498. package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
  499. package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
  500. package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
  501. package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
  502. package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
  503. package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
  504. package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
  505. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
  506. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
  507. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
  508. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
  509. package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
  510. package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
  511. package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
  512. package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
  513. package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
  514. package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
  515. package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
  516. package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
  517. package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
  518. package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
  519. package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
  520. package/Tests/UI/Components/Input.test.tsx +80 -1
  521. package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
  522. package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
  523. package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
  524. package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
  525. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
  526. package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
  527. package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
  528. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
  529. package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
  530. package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
  531. package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
  532. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
  533. package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
  534. package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
  535. package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
  536. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
  537. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
  538. package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
  539. package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
  540. package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
  541. package/Tests/UI/Components/Toggle.test.tsx +66 -0
  542. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
  543. package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
  544. package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
  545. package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
  546. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
  547. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
  548. package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
  549. package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
  550. package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
  551. package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
  552. package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
  553. package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
  554. package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
  555. package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
  556. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
  557. package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
  558. package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
  559. package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
  560. package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
  561. package/Types/AI/AIRunType.ts +9 -4
  562. package/Types/AI/InvestigationNotStartedReason.ts +6 -0
  563. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +14 -16
  564. package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
  565. package/Types/CustomField/CustomFieldOrder.ts +55 -0
  566. package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
  567. package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
  568. package/Types/CustomField/CustomFieldType.ts +14 -0
  569. package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
  570. package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
  571. package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
  572. package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
  573. package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
  574. package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
  575. package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
  576. package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
  577. package/Types/SafeHtml.ts +93 -0
  578. package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
  579. package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
  580. package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
  581. package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
  582. package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
  583. package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
  584. package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
  585. package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
  586. package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
  587. package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
  588. package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
  589. package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
  590. package/Types/Team/ComplianceNotificationChannel.ts +22 -0
  591. package/Types/Team/ComplianceRule.ts +434 -0
  592. package/Types/Team/ComplianceRuleType.ts +21 -0
  593. package/Types/Team/TeamComplianceStatus.ts +81 -0
  594. package/Types/Telemetry/TelemetryIngestSurface.ts +28 -0
  595. package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
  596. package/UI/Components/ActionButton/RowActions.tsx +238 -0
  597. package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
  598. package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
  599. package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
  600. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
  601. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
  602. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
  603. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
  604. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
  605. package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
  606. package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
  607. package/UI/Components/Charts/Line/LineChart.tsx +32 -2
  608. package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
  609. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
  610. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
  611. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
  612. package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
  613. package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
  614. package/UI/Components/Charts/Utils/XAxis.ts +75 -0
  615. package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
  616. package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
  617. package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
  618. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
  619. package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
  620. package/UI/Components/Forms/Fields/FormField.tsx +10 -0
  621. package/UI/Components/Forms/ModelForm.tsx +70 -8
  622. package/UI/Components/Forms/Validation.ts +1 -1
  623. package/UI/Components/Input/Input.tsx +90 -57
  624. package/UI/Components/List/ListRow.tsx +8 -74
  625. package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
  626. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
  627. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
  628. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
  629. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
  630. package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
  631. package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
  632. package/UI/Components/ModelTable/BaseModelTable.tsx +21 -9
  633. package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
  634. package/UI/Components/ModelTable/ModelTable.tsx +6 -7
  635. package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
  636. package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
  637. package/UI/Components/OrderedStatesList/Item.tsx +7 -72
  638. package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
  639. package/UI/Components/Table/TableBody.tsx +8 -2
  640. package/UI/Components/Table/TableRow.tsx +20 -140
  641. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
  642. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
  643. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
  644. package/UI/Components/TelemetryViewer/types.ts +2 -0
  645. package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
  646. package/UI/Components/Toggle/Toggle.tsx +26 -0
  647. package/UI/Components/Tooltip/Tooltip.tsx +8 -1
  648. package/UI/Styles/Theme.css +14 -2
  649. package/UI/Utils/SensitiveUrlToken.ts +10 -5
  650. package/Utils/API.ts +247 -2
  651. package/Utils/AiRemediation/CommandPolicy.ts +1 -1
  652. package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
  653. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
  654. package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
  655. package/Utils/Markdown/MarkdownEscape.ts +65 -0
  656. package/Utils/ProbeNetworkFailureUtil.ts +673 -0
  657. package/build/dist/Models/DatabaseModels/Incident.js +267 -1
  658. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  659. package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
  660. package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
  661. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
  662. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  663. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
  664. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  665. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  666. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  667. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
  668. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
  669. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
  670. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  671. package/build/dist/Models/DatabaseModels/Project.js +56 -5
  672. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  673. package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
  674. package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
  675. package/build/dist/Models/DatabaseModels/RunnerJob.js +74 -0
  676. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  677. package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
  678. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  679. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
  680. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  681. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
  682. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
  683. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  684. package/build/dist/Server/API/AIChatAPI.js +6 -1
  685. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  686. package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
  687. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  688. package/build/dist/Server/API/AutoRemediationAPI.js +144 -24
  689. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  690. package/build/dist/Server/API/IncidentAPI.js +100 -1
  691. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  692. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
  693. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
  694. package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
  695. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  696. package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
  697. package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
  698. package/build/dist/Server/API/StatusPageAPI.js +417 -176
  699. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  700. package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
  701. package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
  702. package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
  703. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  704. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
  705. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
  706. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
  707. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
  708. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
  709. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
  710. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
  711. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
  712. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
  713. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
  714. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
  715. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
  716. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
  717. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
  718. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
  719. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
  720. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
  721. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
  722. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
  723. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
  724. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +20 -0
  725. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  726. package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
  727. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  728. package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
  729. package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
  730. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
  731. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
  732. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
  733. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
  734. package/build/dist/Server/Middleware/TelemetryIngest.js +34 -19
  735. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  736. package/build/dist/Server/Services/AIService.js +59 -1
  737. package/build/dist/Server/Services/AIService.js.map +1 -1
  738. package/build/dist/Server/Services/AlertService.js +248 -13
  739. package/build/dist/Server/Services/AlertService.js.map +1 -1
  740. package/build/dist/Server/Services/AlertSeverityService.js +51 -15
  741. package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
  742. package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
  743. package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
  744. package/build/dist/Server/Services/DatabaseService.js +56 -7
  745. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  746. package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
  747. package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
  748. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
  749. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  750. package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
  751. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  752. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
  753. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  754. package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
  755. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  756. package/build/dist/Server/Services/IncidentService.js +1251 -72
  757. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  758. package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
  759. package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
  760. package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
  761. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  762. package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
  763. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  764. package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
  765. package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
  766. package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
  767. package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
  768. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
  769. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
  770. package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
  771. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  772. package/build/dist/Server/Services/ProjectService.js +22 -0
  773. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  774. package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
  775. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  776. package/build/dist/Server/Services/RunnerJobService.js +178 -92
  777. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  778. package/build/dist/Server/Services/RunnerService.js +1 -1
  779. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  780. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
  781. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  782. package/build/dist/Server/Services/ScheduledMaintenanceService.js +582 -20
  783. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  784. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
  785. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  786. package/build/dist/Server/Services/SmsService.js +2 -22
  787. package/build/dist/Server/Services/SmsService.js.map +1 -1
  788. package/build/dist/Server/Services/StatusPageService.js +47 -9
  789. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  790. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
  791. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  792. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
  793. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
  794. package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
  795. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  796. package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
  797. package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
  798. package/build/dist/Server/Types/Markdown.js +110 -6
  799. package/build/dist/Server/Types/Markdown.js.map +1 -1
  800. package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
  801. package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
  802. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
  803. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
  804. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
  805. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
  806. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
  807. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
  808. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +29 -15
  809. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  810. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +56 -20
  811. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  812. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +39 -5
  813. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  814. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
  815. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
  816. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
  817. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
  818. package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
  819. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +4 -2
  820. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  821. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
  822. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
  823. package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
  824. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  825. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
  826. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
  827. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
  828. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  829. package/build/dist/Server/Utils/SSRFProtection.js +48 -6
  830. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  831. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
  832. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
  833. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
  834. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
  835. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
  836. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
  837. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
  838. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
  839. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
  840. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
  841. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
  842. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
  843. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
  844. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
  845. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
  846. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
  847. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
  848. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
  849. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
  850. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
  851. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
  852. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
  853. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
  854. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
  855. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
  856. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
  857. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
  858. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
  859. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
  860. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
  861. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
  862. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
  863. package/build/dist/Server/Utils/StatusPageResource.js +39 -7
  864. package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
  865. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
  866. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  867. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
  868. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  869. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
  870. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
  871. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
  872. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
  873. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
  874. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  875. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  876. package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
  877. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  878. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
  879. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
  880. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +505 -42
  881. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  882. package/build/dist/Types/AI/AIRunType.js +9 -4
  883. package/build/dist/Types/AI/AIRunType.js.map +1 -1
  884. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +10 -16
  885. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  886. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
  887. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
  888. package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
  889. package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
  890. package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
  891. package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
  892. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
  893. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
  894. package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
  895. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  896. package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
  897. package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
  898. package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
  899. package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
  900. package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
  901. package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
  902. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
  903. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
  904. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
  905. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
  906. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
  907. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
  908. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
  909. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
  910. package/build/dist/Types/SafeHtml.js +84 -0
  911. package/build/dist/Types/SafeHtml.js.map +1 -0
  912. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
  913. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
  914. package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
  915. package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
  916. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
  917. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
  918. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
  919. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
  920. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
  921. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
  922. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
  923. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
  924. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
  925. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
  926. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
  927. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
  928. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
  929. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
  930. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
  931. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
  932. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
  933. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
  934. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
  935. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
  936. package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
  937. package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
  938. package/build/dist/Types/Team/ComplianceRule.js +304 -0
  939. package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
  940. package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
  941. package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
  942. package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
  943. package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
  944. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +25 -0
  945. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  946. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
  947. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
  948. package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
  949. package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
  950. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
  951. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
  952. package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
  953. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  954. package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
  955. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  956. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
  957. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  958. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
  959. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  960. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
  961. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  962. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
  963. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  964. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
  965. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
  966. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
  967. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
  968. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
  969. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
  970. package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
  971. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  972. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
  973. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
  974. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
  975. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
  976. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
  977. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
  978. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
  979. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
  980. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
  981. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
  982. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
  983. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  984. package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
  985. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  986. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
  987. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
  988. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
  989. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
  990. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
  991. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
  992. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
  993. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  994. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
  995. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  996. package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
  997. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  998. package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
  999. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  1000. package/build/dist/UI/Components/Forms/Validation.js +1 -1
  1001. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  1002. package/build/dist/UI/Components/Input/Input.js +69 -54
  1003. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  1004. package/build/dist/UI/Components/List/ListRow.js +4 -39
  1005. package/build/dist/UI/Components/List/ListRow.js.map +1 -1
  1006. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
  1007. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  1008. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
  1009. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  1010. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
  1011. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  1012. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
  1013. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  1014. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
  1015. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  1016. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  1017. package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
  1018. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -2
  1019. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  1020. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
  1021. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  1022. package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
  1023. package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
  1024. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
  1025. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  1026. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
  1027. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
  1028. package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
  1029. package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
  1030. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
  1031. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  1032. package/build/dist/UI/Components/Table/TableBody.js +6 -2
  1033. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  1034. package/build/dist/UI/Components/Table/TableRow.js +12 -75
  1035. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  1036. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
  1037. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  1038. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
  1039. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  1040. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
  1041. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  1042. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
  1043. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
  1044. package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
  1045. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  1046. package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
  1047. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  1048. package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
  1049. package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
  1050. package/build/dist/Utils/API.js +177 -1
  1051. package/build/dist/Utils/API.js.map +1 -1
  1052. package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
  1053. package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
  1054. package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
  1055. package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
  1056. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
  1057. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
  1058. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
  1059. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
  1060. package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
  1061. package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
  1062. package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
  1063. package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
  1064. package/package.json +1 -1
@@ -0,0 +1,2971 @@
1
+ import Incident from "../../../Models/DatabaseModels/Incident";
2
+ import IncidentState from "../../../Models/DatabaseModels/IncidentState";
3
+ import IncidentTemplate from "../../../Models/DatabaseModels/IncidentTemplate";
4
+ import StatusPage from "../../../Models/DatabaseModels/StatusPage";
5
+ import { IncidentFeedEventType } from "../../../Models/DatabaseModels/IncidentFeed";
6
+ import AuditLogService from "../../../Server/Services/AuditLogService";
7
+ import CustomFieldMappingService from "../../../Server/Services/CustomFieldMappingService";
8
+ import IncidentFeedService from "../../../Server/Services/IncidentFeedService";
9
+ import IncidentService from "../../../Server/Services/IncidentService";
10
+ import IncidentStateService from "../../../Server/Services/IncidentStateService";
11
+ import IncidentTemplateService from "../../../Server/Services/IncidentTemplateService";
12
+ import ProjectService from "../../../Server/Services/ProjectService";
13
+ import StatusPageService from "../../../Server/Services/StatusPageService";
14
+ import UserService from "../../../Server/Services/UserService";
15
+ import DatabaseRequestType from "../../../Server/Types/BaseDatabase/DatabaseRequestType";
16
+ import CreateBy from "../../../Server/Types/Database/CreateBy";
17
+ import { OnCreate, OnUpdate } from "../../../Server/Types/Database/Hooks";
18
+ import BasePermission from "../../../Server/Types/Database/Permissions/BasePermission";
19
+ import ColumnPermissions from "../../../Server/Types/Database/Permissions/ColumnPermission";
20
+ import UpdateBy from "../../../Server/Types/Database/UpdateBy";
21
+ import ProjectScopedReferenceValidator from "../../../Server/Utils/Database/ProjectScopedReferenceValidator";
22
+ import IncidentWorkspaceMessages from "../../../Server/Utils/Workspace/WorkspaceMessages/Incident";
23
+ import URL from "../../../Types/API/URL";
24
+ import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
25
+ import Dictionary from "../../../Types/Dictionary";
26
+ import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
27
+ import { JSONObject } from "../../../Types/JSON";
28
+ import ObjectID from "../../../Types/ObjectID";
29
+ import Permission, {
30
+ UserTenantAccessPermission,
31
+ } from "../../../Types/Permission";
32
+ import IncidentCreatedRenotify from "../../../Types/StatusPage/IncidentCreatedRenotify";
33
+ import IncidentScopeAddedPagesNotification from "../../../Types/StatusPage/IncidentScopeAddedPagesNotification";
34
+ import StatusPageSubscriberNotificationStatus from "../../../Types/StatusPage/StatusPageSubscriberNotificationStatus";
35
+ import SubscriberNotificationResend from "../../../Types/StatusPage/SubscriberNotificationResend";
36
+ import StatusPageReadAccess from "../../../Server/Utils/StatusPage/StatusPageReadAccess";
37
+ import getJestMockFunction, { MockFunction } from "../../MockType";
38
+ import {
39
+ afterEach,
40
+ beforeEach,
41
+ describe,
42
+ expect,
43
+ jest,
44
+ test,
45
+ } from "@jest/globals";
46
+
47
+ /*
48
+ * An incident can be limited to some of the status pages its monitors reach
49
+ * (Incident.statusPages). IncidentService owns everything that follows from a
50
+ * write to that list:
51
+ *
52
+ * - isScopedToStatusPages is derived from the list and never taken from a
53
+ * client, and it is never recomputed from what the join table holds;
54
+ * - the job's record of notified pages is never taken from a client;
55
+ * - a non-root editor's list keeps the scoped pages they cannot read
56
+ * (status pages are label-scoped, and a list write replaces the list);
57
+ * - the incident-created notification can be queued again for the pages an
58
+ * edit adds, depending on where that notification is;
59
+ * - a template's pages are copied onto an incident declared from it;
60
+ * - the feed records what was added and removed;
61
+ * - a non-root incident member can do all of it: the hook writes computed
62
+ * columns into their update, and the column check runs after the hook.
63
+ *
64
+ * The database is stubbed: stored incidents are served by a stub of findBy,
65
+ * and which status pages the caller can read by a stub of the status page
66
+ * service's findBy.
67
+ */
68
+
69
+ const projectId: ObjectID = new ObjectID(
70
+ "5d7f3c0a-4c55-4d3e-9a3e-2d4a7d1c9e01",
71
+ );
72
+ const userId: ObjectID = new ObjectID("5d7f3c0a-4c55-4d3e-9a3e-2d4a7d1c9e02");
73
+ const incidentId: string = "a1b2c3d4-0000-4000-8000-000000000001";
74
+ const secondIncidentId: string = "a1b2c3d4-0000-4000-8000-000000000002";
75
+ const templateId: string = "a1b2c3d4-0000-4000-8000-0000000000aa";
76
+
77
+ const PAGE_A: string = "b0000000-0000-4000-8000-00000000000a";
78
+ const PAGE_B: string = "b0000000-0000-4000-8000-00000000000b";
79
+ const PAGE_C: string = "b0000000-0000-4000-8000-00000000000c";
80
+ // Pages behind a label the editor has no access to.
81
+ const HIDDEN_PAGE_X: string = "b0000000-0000-4000-8000-0000000000f1";
82
+ const HIDDEN_PAGE_Y: string = "b0000000-0000-4000-8000-0000000000f2";
83
+
84
+ type OnBeforeUpdate = (
85
+ updateBy: UpdateBy<Incident>,
86
+ ) => Promise<OnUpdate<Incident>>;
87
+ type OnBeforeCreate = (
88
+ createBy: CreateBy<Incident>,
89
+ ) => Promise<OnCreate<Incident>>;
90
+ type OnUpdateSuccess = (
91
+ onUpdate: OnUpdate<Incident>,
92
+ updatedItemIds: Array<ObjectID>,
93
+ ) => Promise<OnUpdate<Incident>>;
94
+
95
+ interface ScopeCarryForward {
96
+ addedStatusPageIds: Array<string>;
97
+ removedStatusPageIds: Array<string>;
98
+ isScoped: boolean;
99
+ notificationQueued: boolean;
100
+ notificationAlreadyQueued?: boolean | undefined;
101
+ }
102
+
103
+ function makeProps(
104
+ permissions: Array<Permission>,
105
+ ): DatabaseCommonInteractionProps {
106
+ const tenantPermission: UserTenantAccessPermission = {
107
+ projectId,
108
+ _type: "UserTenantAccessPermission",
109
+ permissions: permissions.map((permission: Permission) => {
110
+ return {
111
+ _type: "UserPermission",
112
+ permission: permission,
113
+ labelIds: [],
114
+ isBlockPermission: false,
115
+ };
116
+ }),
117
+ };
118
+
119
+ return {
120
+ userId,
121
+ tenantId: projectId,
122
+ userTenantAccessPermission: {
123
+ [projectId.toString()]: tenantPermission,
124
+ },
125
+ };
126
+ }
127
+
128
+ const MEMBER_PROPS: DatabaseCommonInteractionProps = makeProps([
129
+ Permission.IncidentMember,
130
+ Permission.StatusPageViewer,
131
+ ]);
132
+
133
+ function statusPage(id: string, name?: string): StatusPage {
134
+ const page: StatusPage = new StatusPage();
135
+ page._id = id;
136
+ if (name) {
137
+ page.name = name;
138
+ }
139
+ return page;
140
+ }
141
+
142
+ function storedIncident(
143
+ overrides: Partial<Incident> & { statusPageIds?: Array<string> } = {},
144
+ ): Incident {
145
+ const incident: Incident = new Incident();
146
+ incident._id = incidentId;
147
+ incident.projectId = projectId;
148
+ incident.isVisibleOnStatusPage = true;
149
+ incident.isPrivate = false;
150
+ incident.shouldStatusPageSubscribersBeNotifiedOnIncidentCreated = true;
151
+ incident.subscriberNotificationStatusOnIncidentCreated =
152
+ StatusPageSubscriberNotificationStatus.Success;
153
+ /*
154
+ * An incident whose send settled since the record was introduced: it has
155
+ * a record, empty unless a test says who was told. An incident from before
156
+ * the record (null) is its own case below.
157
+ */
158
+ incident.statusPagesNotifiedOnCreation = [];
159
+
160
+ const { statusPageIds, ...rest } = overrides;
161
+
162
+ incident.statusPages = (statusPageIds || []).map((id: string) => {
163
+ return statusPage(id);
164
+ });
165
+
166
+ Object.assign(incident, rest);
167
+ return incident;
168
+ }
169
+
170
+ // The ids QueryHelper.any was given (it builds a Raw IN operator).
171
+ function idsInQueryOperator(operator: unknown): Array<string> {
172
+ const raw: { objectLiteralParameters?: Dictionary<unknown> } = operator as {
173
+ objectLiteralParameters?: Dictionary<unknown>;
174
+ };
175
+
176
+ return (
177
+ Object.values(raw.objectLiteralParameters || {}) as Array<Array<string>>
178
+ )
179
+ .flat()
180
+ .map((id: string): string => {
181
+ return String(id).toLowerCase();
182
+ });
183
+ }
184
+
185
+ // What the database holds for the incidents an update matches.
186
+ let storedIncidents: Array<Incident> = [];
187
+ // The status pages the caller can read; null means no status page access.
188
+ let readableStatusPageIds: Array<string> | null = [];
189
+
190
+ let incidentFindBy: MockFunction;
191
+ let statusPageFindBy: MockFunction;
192
+
193
+ function savedStatusPageIds(data: Record<string, unknown>): Array<string> {
194
+ return IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
195
+ data["statusPages"],
196
+ );
197
+ }
198
+
199
+ function scopeUpdate(data: {
200
+ data: Record<string, unknown>;
201
+ props?: DatabaseCommonInteractionProps;
202
+ miscDataProps?: JSONObject | undefined;
203
+ query?: Record<string, unknown>;
204
+ }): UpdateBy<Incident> {
205
+ return {
206
+ query: (data.query || { _id: incidentId }) as UpdateBy<Incident>["query"],
207
+ data: data.data as UpdateBy<Incident>["data"],
208
+ props: data.props || MEMBER_PROPS,
209
+ miscDataProps: data.miscDataProps,
210
+ limit: 1,
211
+ skip: 0,
212
+ };
213
+ }
214
+
215
+ async function runBeforeUpdate(updateBy: UpdateBy<Incident>): Promise<{
216
+ data: Record<string, unknown>;
217
+ carryForward: Dictionary<{ statusPageScopeChange?: ScopeCarryForward }>;
218
+ }> {
219
+ const onUpdate: OnUpdate<Incident> = await (
220
+ IncidentService as unknown as { onBeforeUpdate: OnBeforeUpdate }
221
+ ).onBeforeUpdate(updateBy);
222
+
223
+ return {
224
+ data: onUpdate.updateBy.data as unknown as Record<string, unknown>,
225
+ carryForward: onUpdate.carryForward as Dictionary<{
226
+ statusPageScopeChange?: ScopeCarryForward;
227
+ }>,
228
+ };
229
+ }
230
+
231
+ function scopeChangeOf(
232
+ carryForward: Dictionary<{ statusPageScopeChange?: ScopeCarryForward }>,
233
+ id: string = incidentId,
234
+ ): ScopeCarryForward | undefined {
235
+ return carryForward[id]?.statusPageScopeChange;
236
+ }
237
+
238
+ beforeEach(() => {
239
+ storedIncidents = [storedIncident()];
240
+ readableStatusPageIds = [PAGE_A, PAGE_B, PAGE_C];
241
+
242
+ incidentFindBy = getJestMockFunction();
243
+ incidentFindBy.mockImplementation(() => {
244
+ return Promise.resolve(storedIncidents);
245
+ });
246
+ jest
247
+ .spyOn(IncidentService, "findBy")
248
+ .mockImplementation(incidentFindBy as never);
249
+
250
+ statusPageFindBy = getJestMockFunction();
251
+ statusPageFindBy.mockImplementation(
252
+ (findBy: { query: { _id: unknown } }): Promise<Array<StatusPage>> => {
253
+ if (readableStatusPageIds === null) {
254
+ return Promise.reject(
255
+ new NotAuthorizedException(
256
+ "You do not have permissions to read Status Page.",
257
+ ),
258
+ );
259
+ }
260
+
261
+ // The ids are asked for with QueryHelper.any (a Raw IN operator).
262
+ const operator: { objectLiteralParameters?: Dictionary<unknown> } = findBy
263
+ .query._id as { objectLiteralParameters?: Dictionary<unknown> };
264
+ const requested: Array<string> = (
265
+ Object.values(operator.objectLiteralParameters || {}) as Array<
266
+ Array<string>
267
+ >
268
+ )
269
+ .flat()
270
+ .map((id: string) => {
271
+ return id.toLowerCase();
272
+ });
273
+
274
+ return Promise.resolve(
275
+ requested
276
+ .filter((id: string) => {
277
+ return readableStatusPageIds!.includes(id);
278
+ })
279
+ .map((id: string) => {
280
+ return statusPage(id);
281
+ }),
282
+ );
283
+ },
284
+ );
285
+ jest
286
+ .spyOn(StatusPageService, "findBy")
287
+ .mockImplementation(statusPageFindBy as never);
288
+
289
+ jest
290
+ .spyOn(
291
+ IncidentService as unknown as {
292
+ validateProjectScopedReferences: () => Promise<void>;
293
+ },
294
+ "validateProjectScopedReferences",
295
+ )
296
+ .mockResolvedValue(undefined as never);
297
+ jest
298
+ .spyOn(CustomFieldMappingService, "applyMappingsToUpdate")
299
+ .mockResolvedValue(undefined as never);
300
+ jest
301
+ .spyOn(CustomFieldMappingService, "applyMappingsToCreate")
302
+ .mockResolvedValue(undefined as never);
303
+ });
304
+
305
+ afterEach(() => {
306
+ jest.restoreAllMocks();
307
+ });
308
+
309
+ describe("IncidentService.onBeforeUpdate: isScopedToStatusPages follows statusPages", () => {
310
+ test("scoping an incident sets the flag", async () => {
311
+ const { data } = await runBeforeUpdate(
312
+ scopeUpdate({ data: { statusPages: [PAGE_A, PAGE_B] } }),
313
+ );
314
+
315
+ expect(savedStatusPageIds(data)).toEqual([PAGE_A, PAGE_B]);
316
+ expect(data["isScopedToStatusPages"]).toBe(true);
317
+ });
318
+
319
+ test("clearing the scope clears the flag", async () => {
320
+ storedIncidents = [storedIncident({ statusPageIds: [PAGE_A] })];
321
+
322
+ const { data } = await runBeforeUpdate(
323
+ scopeUpdate({ data: { statusPages: [] } }),
324
+ );
325
+
326
+ expect(savedStatusPageIds(data)).toEqual([]);
327
+ expect(data["isScopedToStatusPages"]).toBe(false);
328
+ });
329
+
330
+ test("a null list clears the scope like an empty one", async () => {
331
+ storedIncidents = [storedIncident({ statusPageIds: [PAGE_A] })];
332
+
333
+ const { data } = await runBeforeUpdate(
334
+ scopeUpdate({ data: { statusPages: null } }),
335
+ );
336
+
337
+ expect(savedStatusPageIds(data)).toEqual([]);
338
+ expect(data["isScopedToStatusPages"]).toBe(false);
339
+ });
340
+
341
+ test("writes the list as status page stubs, deduplicated, whatever shape it came in", async () => {
342
+ const { data } = await runBeforeUpdate(
343
+ scopeUpdate({
344
+ data: {
345
+ statusPages: [
346
+ statusPage(PAGE_A, "Site A"),
347
+ { _id: PAGE_B },
348
+ PAGE_A.toUpperCase(),
349
+ new ObjectID(PAGE_C),
350
+ ],
351
+ },
352
+ }),
353
+ );
354
+
355
+ const written: Array<unknown> = data["statusPages"] as Array<unknown>;
356
+
357
+ expect(written).toHaveLength(3);
358
+ for (const entry of written) {
359
+ expect(entry).toBeInstanceOf(StatusPage);
360
+ }
361
+ expect(savedStatusPageIds(data)).toEqual([PAGE_A, PAGE_B, PAGE_C]);
362
+ });
363
+
364
+ test.each([
365
+ ["a non-root editor", MEMBER_PROPS],
366
+ ["a root caller", { isRoot: true } as DatabaseCommonInteractionProps],
367
+ ])(
368
+ "ignores the flag when %s sends it without a list",
369
+ async (_label: string, props: DatabaseCommonInteractionProps) => {
370
+ for (const value of [true, false]) {
371
+ const { data } = await runBeforeUpdate(
372
+ scopeUpdate({
373
+ data: { isScopedToStatusPages: value, title: "renamed" },
374
+ props,
375
+ }),
376
+ );
377
+
378
+ expect(data).not.toHaveProperty("isScopedToStatusPages");
379
+ expect(data["title"]).toBe("renamed");
380
+ }
381
+
382
+ // Nothing about the scope was read.
383
+ expect(incidentFindBy).not.toHaveBeenCalled();
384
+ expect(statusPageFindBy).not.toHaveBeenCalled();
385
+ },
386
+ );
387
+
388
+ test("overrides a flag sent alongside the list", async () => {
389
+ const scoped: Record<string, unknown> = (
390
+ await runBeforeUpdate(
391
+ scopeUpdate({
392
+ data: { statusPages: [PAGE_A], isScopedToStatusPages: false },
393
+ }),
394
+ )
395
+ ).data;
396
+
397
+ expect(scoped["isScopedToStatusPages"]).toBe(true);
398
+
399
+ const unscoped: Record<string, unknown> = (
400
+ await runBeforeUpdate(
401
+ scopeUpdate({
402
+ data: { statusPages: [], isScopedToStatusPages: true },
403
+ }),
404
+ )
405
+ ).data;
406
+
407
+ expect(unscoped["isScopedToStatusPages"]).toBe(false);
408
+ });
409
+
410
+ test("an update that does not write the list leaves the flag alone", async () => {
411
+ /*
412
+ * The flag is never recomputed from what the join table holds: an
413
+ * incident whose only page was deleted keeps its flag through every
414
+ * later edit, and stays hidden rather than being widened.
415
+ */
416
+ storedIncidents = [
417
+ storedIncident({ isScopedToStatusPages: true, statusPageIds: [] }),
418
+ ];
419
+
420
+ const { data } = await runBeforeUpdate(
421
+ scopeUpdate({ data: { title: "Still investigating" } }),
422
+ );
423
+
424
+ expect(data).not.toHaveProperty("isScopedToStatusPages");
425
+ expect(data).not.toHaveProperty("statusPages");
426
+ expect(incidentFindBy).not.toHaveBeenCalled();
427
+ });
428
+
429
+ test("a client cannot write the record of notified pages", async () => {
430
+ const { data } = await runBeforeUpdate(
431
+ scopeUpdate({
432
+ data: { statusPagesNotifiedOnCreation: [PAGE_A], title: "renamed" },
433
+ }),
434
+ );
435
+
436
+ expect(data).not.toHaveProperty("statusPagesNotifiedOnCreation");
437
+ });
438
+
439
+ test("a root caller (the notification job) may write the record of notified pages", async () => {
440
+ const { data } = await runBeforeUpdate(
441
+ scopeUpdate({
442
+ data: { statusPagesNotifiedOnCreation: [PAGE_A] },
443
+ props: { isRoot: true },
444
+ }),
445
+ );
446
+
447
+ expect(data["statusPagesNotifiedOnCreation"]).toEqual([PAGE_A]);
448
+ });
449
+
450
+ test("reads the matched incidents as root, with the update's own query", async () => {
451
+ await runBeforeUpdate(scopeUpdate({ data: { statusPages: [PAGE_A] } }));
452
+
453
+ expect(incidentFindBy).toHaveBeenCalledTimes(1);
454
+
455
+ const findBy: {
456
+ query: Record<string, unknown>;
457
+ select: Record<string, unknown>;
458
+ props: DatabaseCommonInteractionProps;
459
+ } = incidentFindBy.mock.calls[0]![0];
460
+
461
+ expect(findBy.props).toEqual({ isRoot: true });
462
+ expect(findBy.query["_id"]).toBe(incidentId);
463
+ expect(findBy.select["statusPages"]).toEqual({ _id: true });
464
+ });
465
+
466
+ test("a non-root caller's read is limited to their own project", async () => {
467
+ /*
468
+ * The update's tenant filter is only added after this hook, so without
469
+ * it another project's incident would be read, and could refuse the
470
+ * edit on its own state instead of the usual "nothing updated".
471
+ */
472
+ await runBeforeUpdate(scopeUpdate({ data: { statusPages: [PAGE_A] } }));
473
+
474
+ expect(incidentFindBy.mock.calls[0]![0].query["projectId"]).toBe(projectId);
475
+ });
476
+
477
+ test("a root caller's read uses the update's own query", async () => {
478
+ await runBeforeUpdate(
479
+ scopeUpdate({ data: { statusPages: [PAGE_A] }, props: { isRoot: true } }),
480
+ );
481
+
482
+ expect(incidentFindBy.mock.calls[0]![0].query).toEqual({
483
+ _id: incidentId,
484
+ });
485
+ });
486
+ });
487
+
488
+ describe("IncidentService.onBeforeUpdate: scoped pages the editor cannot read survive the edit", () => {
489
+ test("a hidden page is kept when the editor saves the pages they can see", async () => {
490
+ readableStatusPageIds = [PAGE_A, PAGE_B];
491
+ storedIncidents = [
492
+ storedIncident({ statusPageIds: [PAGE_A, HIDDEN_PAGE_X] }),
493
+ ];
494
+
495
+ // The dashboard sends back only what the editor sees, plus their change.
496
+ const { data, carryForward } = await runBeforeUpdate(
497
+ scopeUpdate({ data: { statusPages: [PAGE_A, PAGE_B] } }),
498
+ );
499
+
500
+ expect(savedStatusPageIds(data)).toEqual([PAGE_A, PAGE_B, HIDDEN_PAGE_X]);
501
+ expect(data["isScopedToStatusPages"]).toBe(true);
502
+
503
+ // The hidden page is neither added nor removed.
504
+ expect(scopeChangeOf(carryForward)).toEqual({
505
+ addedStatusPageIds: [PAGE_B],
506
+ removedStatusPageIds: [],
507
+ isScoped: true,
508
+ notificationQueued: false,
509
+ notificationAlreadyQueued: false,
510
+ });
511
+ });
512
+
513
+ test("clearing the pages the editor can see keeps the incident scoped to the hidden ones", async () => {
514
+ readableStatusPageIds = [PAGE_A];
515
+ storedIncidents = [
516
+ storedIncident({ statusPageIds: [PAGE_A, HIDDEN_PAGE_X] }),
517
+ ];
518
+
519
+ const { data, carryForward } = await runBeforeUpdate(
520
+ scopeUpdate({ data: { statusPages: [] } }),
521
+ );
522
+
523
+ expect(savedStatusPageIds(data)).toEqual([HIDDEN_PAGE_X]);
524
+ expect(data["isScopedToStatusPages"]).toBe(true);
525
+ expect(scopeChangeOf(carryForward)?.removedStatusPageIds).toEqual([PAGE_A]);
526
+ });
527
+
528
+ test("asks the status page service which pages are readable with the editor's own props", async () => {
529
+ storedIncidents = [
530
+ storedIncident({ statusPageIds: [PAGE_A, HIDDEN_PAGE_X] }),
531
+ ];
532
+
533
+ await runBeforeUpdate(scopeUpdate({ data: { statusPages: [PAGE_A] } }));
534
+
535
+ // Only the hidden-page question: nothing is added.
536
+ expect(statusPageFindBy).toHaveBeenCalledTimes(1);
537
+ expect(statusPageFindBy.mock.calls[0]![0].props).toBe(MEMBER_PROPS);
538
+ });
539
+
540
+ test("an editor without any status page access keeps every scoped page", async () => {
541
+ readableStatusPageIds = null;
542
+ storedIncidents = [
543
+ storedIncident({ statusPageIds: [HIDDEN_PAGE_X, HIDDEN_PAGE_Y] }),
544
+ ];
545
+
546
+ const { data } = await runBeforeUpdate(
547
+ scopeUpdate({
548
+ data: { statusPages: [] },
549
+ props: makeProps([Permission.IncidentMember]),
550
+ }),
551
+ );
552
+
553
+ expect(savedStatusPageIds(data).sort()).toEqual(
554
+ [HIDDEN_PAGE_X, HIDDEN_PAGE_Y].sort(),
555
+ );
556
+ expect(data["isScopedToStatusPages"]).toBe(true);
557
+ });
558
+
559
+ test("a page the editor sends that is also hidden is not added twice", async () => {
560
+ readableStatusPageIds = [];
561
+ storedIncidents = [storedIncident({ statusPageIds: [HIDDEN_PAGE_X] })];
562
+
563
+ const { data } = await runBeforeUpdate(
564
+ scopeUpdate({ data: { statusPages: [HIDDEN_PAGE_X.toUpperCase()] } }),
565
+ );
566
+
567
+ expect(savedStatusPageIds(data)).toEqual([HIDDEN_PAGE_X]);
568
+ });
569
+
570
+ test("a root caller replaces the list exactly, and nothing is checked for it", async () => {
571
+ storedIncidents = [
572
+ storedIncident({ statusPageIds: [PAGE_A, HIDDEN_PAGE_X] }),
573
+ ];
574
+
575
+ const { data } = await runBeforeUpdate(
576
+ scopeUpdate({ data: { statusPages: [PAGE_B] }, props: { isRoot: true } }),
577
+ );
578
+
579
+ expect(savedStatusPageIds(data)).toEqual([PAGE_B]);
580
+ expect(statusPageFindBy).not.toHaveBeenCalled();
581
+ });
582
+
583
+ test("an incident with no scope has no hidden pages to keep: only the added pages are checked", async () => {
584
+ storedIncidents = [storedIncident({ statusPageIds: [] })];
585
+
586
+ await runBeforeUpdate(scopeUpdate({ data: { statusPages: [PAGE_A] } }));
587
+
588
+ expect(statusPageFindBy).toHaveBeenCalledTimes(1);
589
+ expect(
590
+ idsInQueryOperator(statusPageFindBy.mock.calls[0]![0].query["_id"]),
591
+ ).toEqual([PAGE_A]);
592
+ });
593
+
594
+ test("a bulk edit keeps hidden pages every matched incident holds", async () => {
595
+ readableStatusPageIds = [PAGE_A, PAGE_B];
596
+ storedIncidents = [
597
+ storedIncident({ statusPageIds: [PAGE_A, HIDDEN_PAGE_X] }),
598
+ storedIncident({
599
+ _id: secondIncidentId,
600
+ statusPageIds: [HIDDEN_PAGE_X],
601
+ } as Partial<Incident>),
602
+ ];
603
+
604
+ const { data } = await runBeforeUpdate(
605
+ scopeUpdate({
606
+ data: { statusPages: [PAGE_B] },
607
+ query: { projectId: projectId },
608
+ }),
609
+ );
610
+
611
+ expect(savedStatusPageIds(data)).toEqual([PAGE_B, HIDDEN_PAGE_X]);
612
+ });
613
+
614
+ test("a bulk edit is refused when the matched incidents hide different pages", async () => {
615
+ /*
616
+ * The same list is written onto every matched incident, so keeping X
617
+ * for the first would scope the second to a page it never had.
618
+ */
619
+ readableStatusPageIds = [PAGE_A];
620
+ storedIncidents = [
621
+ storedIncident({ statusPageIds: [HIDDEN_PAGE_X] }),
622
+ storedIncident({
623
+ _id: secondIncidentId,
624
+ statusPageIds: [HIDDEN_PAGE_Y],
625
+ } as Partial<Incident>),
626
+ ];
627
+
628
+ await expect(
629
+ runBeforeUpdate(
630
+ scopeUpdate({
631
+ data: { statusPages: [PAGE_A] },
632
+ query: { projectId: projectId },
633
+ }),
634
+ ),
635
+ ).rejects.toThrow(
636
+ "limited to different status pages that you do not have access to",
637
+ );
638
+ });
639
+
640
+ test("an unexpected error reading status pages is not swallowed", async () => {
641
+ storedIncidents = [storedIncident({ statusPageIds: [PAGE_A] })];
642
+ statusPageFindBy.mockImplementation(() => {
643
+ return Promise.reject(new Error("connection lost"));
644
+ });
645
+
646
+ await expect(
647
+ runBeforeUpdate(scopeUpdate({ data: { statusPages: [PAGE_B] } })),
648
+ ).rejects.toThrow("connection lost");
649
+ });
650
+ });
651
+
652
+ describe("IncidentService.onBeforeUpdate: 'Send the incident-created notification to newly added pages'", () => {
653
+ function addPages(
654
+ data: {
655
+ box?: boolean;
656
+ extra?: Record<string, unknown>;
657
+ } = {},
658
+ ): UpdateBy<Incident> {
659
+ return scopeUpdate({
660
+ data: { statusPages: [PAGE_A, PAGE_B], ...(data.extra || {}) },
661
+ miscDataProps:
662
+ data.box === false
663
+ ? undefined
664
+ : IncidentScopeAddedPagesNotification.getMiscDataProps(),
665
+ });
666
+ }
667
+
668
+ function withStatus(
669
+ status: StatusPageSubscriberNotificationStatus,
670
+ overrides: Partial<Incident> & { statusPageIds?: Array<string> } = {},
671
+ ): Incident {
672
+ return storedIncident({
673
+ statusPageIds: [PAGE_A],
674
+ subscriberNotificationStatusOnIncidentCreated: status,
675
+ ...overrides,
676
+ });
677
+ }
678
+
679
+ describe.each([
680
+ StatusPageSubscriberNotificationStatus.Success,
681
+ StatusPageSubscriberNotificationStatus.Skipped,
682
+ StatusPageSubscriberNotificationStatus.Failed,
683
+ ])(
684
+ "while the notification is %s, with a record of the pages told",
685
+ (status: StatusPageSubscriberNotificationStatus) => {
686
+ test("adding a page with the box ticked queues it again", async () => {
687
+ storedIncidents = [
688
+ withStatus(status, { statusPagesNotifiedOnCreation: [PAGE_A] }),
689
+ ];
690
+
691
+ const { data, carryForward } = await runBeforeUpdate(addPages());
692
+
693
+ expect(data["subscriberNotificationStatusOnIncidentCreated"]).toBe(
694
+ StatusPageSubscriberNotificationStatus.Pending,
695
+ );
696
+ expect(data["subscriberNotificationStatusMessage"]).toBe(
697
+ IncidentScopeAddedPagesNotification.queuedMessage,
698
+ );
699
+ expect(scopeChangeOf(carryForward)).toEqual({
700
+ addedStatusPageIds: [PAGE_B],
701
+ removedStatusPageIds: [],
702
+ isScoped: true,
703
+ notificationQueued: true,
704
+ notificationAlreadyQueued: false,
705
+ });
706
+ // The record of notified pages is the job's, and is left alone.
707
+ expect(data).not.toHaveProperty("statusPagesNotifiedOnCreation");
708
+ });
709
+
710
+ test("without the box, the scope changes and nothing is queued", async () => {
711
+ storedIncidents = [
712
+ withStatus(status, { statusPagesNotifiedOnCreation: [PAGE_A] }),
713
+ ];
714
+
715
+ const { data, carryForward } = await runBeforeUpdate(
716
+ addPages({ box: false }),
717
+ );
718
+
719
+ expect(savedStatusPageIds(data)).toEqual([PAGE_A, PAGE_B]);
720
+ expect(data).not.toHaveProperty(
721
+ "subscriberNotificationStatusOnIncidentCreated",
722
+ );
723
+ expect(data).not.toHaveProperty("statusPagesNotifiedOnCreation");
724
+ expect(scopeChangeOf(carryForward)?.notificationQueued).toBe(false);
725
+ });
726
+ },
727
+ );
728
+
729
+ test.each([
730
+ StatusPageSubscriberNotificationStatus.Pending,
731
+ StatusPageSubscriberNotificationStatus.InProgress,
732
+ ])(
733
+ "while the notification is %s, the edit goes through and the send picks the pages up",
734
+ async (status: StatusPageSubscriberNotificationStatus) => {
735
+ /*
736
+ * A queued send reads the scope when it goes out; a running one reads
737
+ * it again when it finishes. So nothing is refused or rewritten, and
738
+ * onUpdateSuccess is told to check the send did not slip past.
739
+ */
740
+ storedIncidents = [withStatus(status)];
741
+
742
+ const { data, carryForward } = await runBeforeUpdate(addPages());
743
+
744
+ expect(savedStatusPageIds(data)).toEqual([PAGE_A, PAGE_B]);
745
+ expect(data).not.toHaveProperty(
746
+ "subscriberNotificationStatusOnIncidentCreated",
747
+ );
748
+ expect(data).not.toHaveProperty("subscriberNotificationStatusMessage");
749
+ expect(data).not.toHaveProperty("statusPagesNotifiedOnCreation");
750
+ expect(scopeChangeOf(carryForward)?.notificationQueued).toBe(false);
751
+ expect(scopeChangeOf(carryForward)?.notificationAlreadyQueued).toBe(true);
752
+ },
753
+ );
754
+
755
+ test("while the notification is being sent, the edit goes through without the box", async () => {
756
+ storedIncidents = [
757
+ withStatus(StatusPageSubscriberNotificationStatus.InProgress),
758
+ ];
759
+
760
+ const { data } = await runBeforeUpdate(addPages({ box: false }));
761
+
762
+ expect(savedStatusPageIds(data)).toEqual([PAGE_A, PAGE_B]);
763
+ expect(data).not.toHaveProperty(
764
+ "subscriberNotificationStatusOnIncidentCreated",
765
+ );
766
+ });
767
+
768
+ test("scoping an unscoped incident counts every selected page as added", async () => {
769
+ // The job skips the pages it has a record of telling.
770
+ storedIncidents = [
771
+ storedIncident({
772
+ statusPageIds: [],
773
+ subscriberNotificationStatusOnIncidentCreated:
774
+ StatusPageSubscriberNotificationStatus.Success,
775
+ }),
776
+ ];
777
+
778
+ const { data, carryForward } = await runBeforeUpdate(addPages());
779
+
780
+ expect(data["subscriberNotificationStatusOnIncidentCreated"]).toBe(
781
+ StatusPageSubscriberNotificationStatus.Pending,
782
+ );
783
+ expect(scopeChangeOf(carryForward)?.addedStatusPageIds).toEqual([
784
+ PAGE_A,
785
+ PAGE_B,
786
+ ]);
787
+ });
788
+
789
+ describe("queues nothing", () => {
790
+ async function expectNothingQueued(
791
+ updateBy: UpdateBy<Incident>,
792
+ ): Promise<void> {
793
+ const { data } = await runBeforeUpdate(updateBy);
794
+
795
+ expect(data).not.toHaveProperty(
796
+ "subscriberNotificationStatusOnIncidentCreated",
797
+ );
798
+ expect(data).not.toHaveProperty("subscriberNotificationStatusMessage");
799
+ }
800
+
801
+ test("when no page is added: the same pages saved again", async () => {
802
+ storedIncidents = [storedIncident({ statusPageIds: [PAGE_A, PAGE_B] })];
803
+
804
+ await expectNothingQueued(addPages());
805
+ });
806
+
807
+ test("when pages are only removed, even while the notification is being sent", async () => {
808
+ storedIncidents = [
809
+ storedIncident({
810
+ statusPageIds: [PAGE_A, PAGE_B, PAGE_C],
811
+ subscriberNotificationStatusOnIncidentCreated:
812
+ StatusPageSubscriberNotificationStatus.InProgress,
813
+ }),
814
+ ];
815
+
816
+ await expectNothingQueued(addPages());
817
+ });
818
+
819
+ test("when the scope is cleared", async () => {
820
+ storedIncidents = [storedIncident({ statusPageIds: [PAGE_A] })];
821
+
822
+ await expectNothingQueued(
823
+ scopeUpdate({
824
+ data: { statusPages: [] },
825
+ miscDataProps: IncidentScopeAddedPagesNotification.getMiscDataProps(),
826
+ }),
827
+ );
828
+ });
829
+
830
+ test("when notifying subscribers on creation is off for the incident", async () => {
831
+ storedIncidents = [
832
+ withStatus(StatusPageSubscriberNotificationStatus.Skipped, {
833
+ shouldStatusPageSubscribersBeNotifiedOnIncidentCreated: false,
834
+ }),
835
+ ];
836
+
837
+ await expectNothingQueued(addPages());
838
+ });
839
+
840
+ test("when the incident is hidden from status pages", async () => {
841
+ storedIncidents = [
842
+ withStatus(StatusPageSubscriberNotificationStatus.Skipped, {
843
+ isVisibleOnStatusPage: false,
844
+ subscriberNotificationStatusMessage:
845
+ IncidentCreatedRenotify.hiddenFromStatusPagesMessage,
846
+ }),
847
+ ];
848
+
849
+ await expectNothingQueued(addPages());
850
+ });
851
+
852
+ test("when the same edit hides the incident", async () => {
853
+ storedIncidents = [
854
+ withStatus(StatusPageSubscriberNotificationStatus.Success),
855
+ ];
856
+
857
+ await expectNothingQueued(
858
+ addPages({ extra: { isVisibleOnStatusPage: false } }),
859
+ );
860
+ });
861
+
862
+ test("when the same edit makes the incident private", async () => {
863
+ storedIncidents = [
864
+ withStatus(StatusPageSubscriberNotificationStatus.Success),
865
+ ];
866
+
867
+ await expectNothingQueued(addPages({ extra: { isPrivate: true } }));
868
+ });
869
+
870
+ test("when the incident is private", async () => {
871
+ storedIncidents = [
872
+ withStatus(StatusPageSubscriberNotificationStatus.Skipped, {
873
+ isPrivate: true,
874
+ isVisibleOnStatusPage: false,
875
+ }),
876
+ ];
877
+
878
+ await expectNothingQueued(addPages());
879
+ });
880
+
881
+ test("when the box is sent as anything but a real yes", async () => {
882
+ storedIncidents = [
883
+ withStatus(StatusPageSubscriberNotificationStatus.Success),
884
+ ];
885
+
886
+ await expectNothingQueued(
887
+ scopeUpdate({
888
+ data: { statusPages: [PAGE_A, PAGE_B] },
889
+ miscDataProps: {
890
+ [IncidentScopeAddedPagesNotification.miscDataKey]: "false",
891
+ },
892
+ }),
893
+ );
894
+ });
895
+
896
+ test("when the update matches several incidents and one of them gains no page", async () => {
897
+ storedIncidents = [
898
+ withStatus(StatusPageSubscriberNotificationStatus.Success),
899
+ storedIncident({
900
+ _id: secondIncidentId,
901
+ statusPageIds: [PAGE_A, PAGE_B],
902
+ } as Partial<Incident>),
903
+ ];
904
+
905
+ await expectNothingQueued(addPages());
906
+ });
907
+ });
908
+
909
+ test("the same edit publishing a hidden incident queues it too", async () => {
910
+ storedIncidents = [
911
+ withStatus(StatusPageSubscriberNotificationStatus.Skipped, {
912
+ isVisibleOnStatusPage: false,
913
+ }),
914
+ ];
915
+
916
+ const { data } = await runBeforeUpdate(
917
+ addPages({ extra: { isVisibleOnStatusPage: true } }),
918
+ );
919
+
920
+ expect(data["subscriberNotificationStatusOnIncidentCreated"]).toBe(
921
+ StatusPageSubscriberNotificationStatus.Pending,
922
+ );
923
+ });
924
+
925
+ test("publishing with its own box as well: the publish decides the status and message", async () => {
926
+ storedIncidents = [
927
+ withStatus(StatusPageSubscriberNotificationStatus.Skipped, {
928
+ isVisibleOnStatusPage: false,
929
+ }),
930
+ ];
931
+
932
+ const { data } = await runBeforeUpdate(
933
+ scopeUpdate({
934
+ data: {
935
+ statusPages: [PAGE_A, PAGE_B],
936
+ isVisibleOnStatusPage: true,
937
+ isPrivate: false,
938
+ },
939
+ miscDataProps: {
940
+ ...IncidentScopeAddedPagesNotification.getMiscDataProps(),
941
+ ...IncidentCreatedRenotify.getMiscDataProps(),
942
+ },
943
+ }),
944
+ );
945
+
946
+ expect(data["subscriberNotificationStatusOnIncidentCreated"]).toBe(
947
+ StatusPageSubscriberNotificationStatus.Pending,
948
+ );
949
+ expect(data["subscriberNotificationStatusMessage"]).toBe(
950
+ IncidentCreatedRenotify.queuedMessage,
951
+ );
952
+ });
953
+
954
+ test("leaves an explicitly written status alone, even while a send is running", async () => {
955
+ storedIncidents = [
956
+ withStatus(StatusPageSubscriberNotificationStatus.InProgress),
957
+ ];
958
+
959
+ const { data } = await runBeforeUpdate(
960
+ scopeUpdate({
961
+ data: {
962
+ statusPages: [PAGE_A, PAGE_B],
963
+ subscriberNotificationStatusOnIncidentCreated:
964
+ StatusPageSubscriberNotificationStatus.Failed,
965
+ },
966
+ props: { isRoot: true },
967
+ miscDataProps: IncidentScopeAddedPagesNotification.getMiscDataProps(),
968
+ }),
969
+ );
970
+
971
+ expect(data["subscriberNotificationStatusOnIncidentCreated"]).toBe(
972
+ StatusPageSubscriberNotificationStatus.Failed,
973
+ );
974
+ });
975
+
976
+ test("a bulk edit queues when every matched incident gains a page and may be announced", async () => {
977
+ storedIncidents = [
978
+ withStatus(StatusPageSubscriberNotificationStatus.Success),
979
+ storedIncident({
980
+ _id: secondIncidentId,
981
+ statusPageIds: [],
982
+ subscriberNotificationStatusOnIncidentCreated:
983
+ StatusPageSubscriberNotificationStatus.Pending,
984
+ } as Partial<Incident>),
985
+ ];
986
+
987
+ const { data, carryForward } = await runBeforeUpdate(addPages());
988
+
989
+ expect(data["subscriberNotificationStatusOnIncidentCreated"]).toBe(
990
+ StatusPageSubscriberNotificationStatus.Pending,
991
+ );
992
+ expect(scopeChangeOf(carryForward)?.notificationQueued).toBe(true);
993
+ // The Pending one was already queued; this edit did not queue it.
994
+ expect(
995
+ scopeChangeOf(carryForward, secondIncidentId)?.notificationQueued,
996
+ ).toBe(false);
997
+ });
998
+
999
+ test("a bulk edit queues when one matched incident is being sent: that send picks the pages up", async () => {
1000
+ storedIncidents = [
1001
+ withStatus(StatusPageSubscriberNotificationStatus.Success),
1002
+ storedIncident({
1003
+ _id: secondIncidentId,
1004
+ statusPageIds: [],
1005
+ subscriberNotificationStatusOnIncidentCreated:
1006
+ StatusPageSubscriberNotificationStatus.InProgress,
1007
+ } as Partial<Incident>),
1008
+ ];
1009
+
1010
+ const { data, carryForward } = await runBeforeUpdate(addPages());
1011
+
1012
+ expect(data["subscriberNotificationStatusOnIncidentCreated"]).toBe(
1013
+ StatusPageSubscriberNotificationStatus.Pending,
1014
+ );
1015
+ expect(
1016
+ scopeChangeOf(carryForward, secondIncidentId)?.notificationQueued,
1017
+ ).toBe(false);
1018
+ });
1019
+ });
1020
+
1021
+ describe("IncidentService.onBeforeUpdate: carries the scope change forward for the feed", () => {
1022
+ test("records the pages added and removed, next to a monitor change", async () => {
1023
+ storedIncidents = [
1024
+ storedIncident({ statusPageIds: [PAGE_A, PAGE_B], monitors: [] }),
1025
+ ];
1026
+
1027
+ const { carryForward } = await runBeforeUpdate(
1028
+ scopeUpdate({
1029
+ data: { statusPages: [PAGE_B, PAGE_C], monitors: [] },
1030
+ props: { isRoot: true },
1031
+ }),
1032
+ );
1033
+
1034
+ expect(scopeChangeOf(carryForward)).toEqual({
1035
+ addedStatusPageIds: [PAGE_C],
1036
+ removedStatusPageIds: [PAGE_A],
1037
+ isScoped: true,
1038
+ notificationQueued: false,
1039
+ notificationAlreadyQueued: false,
1040
+ });
1041
+ // The monitor bookkeeping for the same incident is kept.
1042
+ expect(carryForward[incidentId]).toMatchObject({
1043
+ monitorsRemoved: [],
1044
+ monitorsAdded: [],
1045
+ });
1046
+ });
1047
+
1048
+ test("records clearing the scope", async () => {
1049
+ storedIncidents = [storedIncident({ statusPageIds: [PAGE_A] })];
1050
+
1051
+ const { carryForward } = await runBeforeUpdate(
1052
+ scopeUpdate({ data: { statusPages: [] } }),
1053
+ );
1054
+
1055
+ expect(scopeChangeOf(carryForward)).toEqual({
1056
+ addedStatusPageIds: [],
1057
+ removedStatusPageIds: [PAGE_A],
1058
+ isScoped: false,
1059
+ notificationQueued: false,
1060
+ notificationAlreadyQueued: false,
1061
+ });
1062
+ });
1063
+ });
1064
+
1065
+ /*
1066
+ * The job's record of told pages decides who a re-queued 'created'
1067
+ * notification goes to. Two kinds of incident have no record (null), and
1068
+ * neither may let an added-pages request tell pages that were not added.
1069
+ */
1070
+ describe("IncidentService.onBeforeUpdate: added pages and incidents without a record of told pages", () => {
1071
+ function addPage(
1072
+ statusPages: Array<string>,
1073
+ box: boolean = true,
1074
+ ): UpdateBy<Incident> {
1075
+ return scopeUpdate({
1076
+ data: { statusPages: statusPages },
1077
+ miscDataProps: box
1078
+ ? IncidentScopeAddedPagesNotification.getMiscDataProps()
1079
+ : undefined,
1080
+ });
1081
+ }
1082
+
1083
+ async function expectNothingQueued(
1084
+ updateBy: UpdateBy<Incident>,
1085
+ ): Promise<Record<string, unknown>> {
1086
+ const { data, carryForward } = await runBeforeUpdate(updateBy);
1087
+
1088
+ expect(data).not.toHaveProperty(
1089
+ "subscriberNotificationStatusOnIncidentCreated",
1090
+ );
1091
+ expect(data).not.toHaveProperty("subscriberNotificationStatusMessage");
1092
+ expect(data).not.toHaveProperty("statusPagesNotifiedOnCreation");
1093
+ expect(scopeChangeOf(carryForward)?.notificationQueued).toBe(false);
1094
+
1095
+ return data;
1096
+ }
1097
+
1098
+ test.each([
1099
+ StatusPageSubscriberNotificationStatus.Success,
1100
+ StatusPageSubscriberNotificationStatus.Failed,
1101
+ ])(
1102
+ "an incident told before the record existed (%s, no record) is not told again when it is scoped",
1103
+ async (status: StatusPageSubscriberNotificationStatus) => {
1104
+ /*
1105
+ * Created last release on a monitor shared by every site: all of them
1106
+ * were emailed. Narrowing it to Site A and B must not email them again.
1107
+ */
1108
+ storedIncidents = [
1109
+ storedIncident({
1110
+ statusPageIds: [],
1111
+ subscriberNotificationStatusOnIncidentCreated: status,
1112
+ statusPagesNotifiedOnCreation: null as unknown as Array<string>,
1113
+ }),
1114
+ ];
1115
+
1116
+ const data: Record<string, unknown> = await expectNothingQueued(
1117
+ addPage([PAGE_A, PAGE_B]),
1118
+ );
1119
+
1120
+ // The scope itself still changes.
1121
+ expect(savedStatusPageIds(data)).toEqual([PAGE_A, PAGE_B]);
1122
+ expect(data["isScopedToStatusPages"]).toBe(true);
1123
+ },
1124
+ );
1125
+
1126
+ test("an incident published without announcing it tells only the page added later, not the pages it was limited to", async () => {
1127
+ /*
1128
+ * Hidden and Skipped, then published with 'Notify subscribers' unticked:
1129
+ * still Skipped, no record. Adding Site C with the box ticked must not
1130
+ * announce it on A and B, which the editor declined.
1131
+ */
1132
+ storedIncidents = [
1133
+ storedIncident({
1134
+ statusPageIds: [PAGE_A, PAGE_B],
1135
+ subscriberNotificationStatusOnIncidentCreated:
1136
+ StatusPageSubscriberNotificationStatus.Skipped,
1137
+ subscriberNotificationStatusMessage:
1138
+ IncidentCreatedRenotify.hiddenFromStatusPagesMessage,
1139
+ statusPagesNotifiedOnCreation: null as unknown as Array<string>,
1140
+ }),
1141
+ ];
1142
+
1143
+ const { data, carryForward } = await runBeforeUpdate(
1144
+ addPage([PAGE_A, PAGE_B, PAGE_C]),
1145
+ );
1146
+
1147
+ expect(data["subscriberNotificationStatusOnIncidentCreated"]).toBe(
1148
+ StatusPageSubscriberNotificationStatus.Pending,
1149
+ );
1150
+ // Written in the same update: the job then tells C only.
1151
+ expect(data["statusPagesNotifiedOnCreation"]).toEqual([PAGE_A, PAGE_B]);
1152
+ expect(scopeChangeOf(carryForward)?.notificationQueued).toBe(true);
1153
+ });
1154
+
1155
+ test("a skipped incident that was not limited before: every page it is limited to now is added", async () => {
1156
+ storedIncidents = [
1157
+ storedIncident({
1158
+ statusPageIds: [],
1159
+ subscriberNotificationStatusOnIncidentCreated:
1160
+ StatusPageSubscriberNotificationStatus.Skipped,
1161
+ statusPagesNotifiedOnCreation: null as unknown as Array<string>,
1162
+ }),
1163
+ ];
1164
+
1165
+ const { data } = await runBeforeUpdate(addPage([PAGE_A, PAGE_B]));
1166
+
1167
+ expect(data["subscriberNotificationStatusOnIncidentCreated"]).toBe(
1168
+ StatusPageSubscriberNotificationStatus.Pending,
1169
+ );
1170
+ expect(data["statusPagesNotifiedOnCreation"]).toEqual([]);
1171
+ });
1172
+
1173
+ test("a skipped incident with a record keeps it", async () => {
1174
+ storedIncidents = [
1175
+ storedIncident({
1176
+ statusPageIds: [PAGE_A],
1177
+ subscriberNotificationStatusOnIncidentCreated:
1178
+ StatusPageSubscriberNotificationStatus.Skipped,
1179
+ statusPagesNotifiedOnCreation: [PAGE_A],
1180
+ }),
1181
+ ];
1182
+
1183
+ const { data } = await runBeforeUpdate(addPage([PAGE_A, PAGE_B]));
1184
+
1185
+ expect(data["subscriberNotificationStatusOnIncidentCreated"]).toBe(
1186
+ StatusPageSubscriberNotificationStatus.Pending,
1187
+ );
1188
+ expect(data).not.toHaveProperty("statusPagesNotifiedOnCreation");
1189
+ });
1190
+
1191
+ test("without the box, a skipped incident's record is not written either", async () => {
1192
+ storedIncidents = [
1193
+ storedIncident({
1194
+ statusPageIds: [PAGE_A],
1195
+ subscriberNotificationStatusOnIncidentCreated:
1196
+ StatusPageSubscriberNotificationStatus.Skipped,
1197
+ statusPagesNotifiedOnCreation: null as unknown as Array<string>,
1198
+ }),
1199
+ ];
1200
+
1201
+ await expectNothingQueued(addPage([PAGE_A, PAGE_B], false));
1202
+ });
1203
+
1204
+ test("an unscoped incident narrowed to pages it already told queues nothing", async () => {
1205
+ // It reached every site; the record lists them.
1206
+ storedIncidents = [
1207
+ storedIncident({
1208
+ statusPageIds: [],
1209
+ statusPagesNotifiedOnCreation: [PAGE_A, PAGE_B, PAGE_C],
1210
+ }),
1211
+ ];
1212
+
1213
+ await expectNothingQueued(addPage([PAGE_A, PAGE_B]));
1214
+ });
1215
+
1216
+ test("a page removed earlier and added back is not told twice", async () => {
1217
+ storedIncidents = [
1218
+ storedIncident({
1219
+ statusPageIds: [PAGE_A],
1220
+ statusPagesNotifiedOnCreation: [PAGE_A, PAGE_B],
1221
+ }),
1222
+ ];
1223
+
1224
+ await expectNothingQueued(addPage([PAGE_A, PAGE_B]));
1225
+ });
1226
+
1227
+ test("a bulk edit writes one record for incidents that were limited to the same pages", async () => {
1228
+ storedIncidents = [
1229
+ storedIncident({
1230
+ statusPageIds: [PAGE_A],
1231
+ subscriberNotificationStatusOnIncidentCreated:
1232
+ StatusPageSubscriberNotificationStatus.Skipped,
1233
+ statusPagesNotifiedOnCreation: null as unknown as Array<string>,
1234
+ }),
1235
+ storedIncident({
1236
+ _id: secondIncidentId,
1237
+ statusPageIds: [PAGE_A],
1238
+ subscriberNotificationStatusOnIncidentCreated:
1239
+ StatusPageSubscriberNotificationStatus.Skipped,
1240
+ statusPagesNotifiedOnCreation: null as unknown as Array<string>,
1241
+ } as Partial<Incident>),
1242
+ ];
1243
+
1244
+ const { data } = await runBeforeUpdate(
1245
+ scopeUpdate({
1246
+ data: { statusPages: [PAGE_A, PAGE_B] },
1247
+ query: { projectId: projectId },
1248
+ miscDataProps: IncidentScopeAddedPagesNotification.getMiscDataProps(),
1249
+ }),
1250
+ );
1251
+
1252
+ expect(data["statusPagesNotifiedOnCreation"]).toEqual([PAGE_A]);
1253
+ });
1254
+
1255
+ test.each([
1256
+ [
1257
+ "they were limited to different pages",
1258
+ {
1259
+ statusPageIds: [PAGE_B],
1260
+ subscriberNotificationStatusOnIncidentCreated:
1261
+ StatusPageSubscriberNotificationStatus.Skipped,
1262
+ statusPagesNotifiedOnCreation: null as unknown as Array<string>,
1263
+ },
1264
+ ],
1265
+ [
1266
+ "the other one has a record the write would overwrite",
1267
+ {
1268
+ statusPageIds: [PAGE_A],
1269
+ statusPagesNotifiedOnCreation: [PAGE_A],
1270
+ },
1271
+ ],
1272
+ [
1273
+ "the other one is already on its way",
1274
+ {
1275
+ statusPageIds: [PAGE_A],
1276
+ subscriberNotificationStatusOnIncidentCreated:
1277
+ StatusPageSubscriberNotificationStatus.Pending,
1278
+ statusPagesNotifiedOnCreation: null as unknown as Array<string>,
1279
+ },
1280
+ ],
1281
+ ])(
1282
+ "a bulk edit that would have to write a record is refused when %s",
1283
+ async (_label: string, other: Record<string, unknown>) => {
1284
+ storedIncidents = [
1285
+ storedIncident({
1286
+ statusPageIds: [PAGE_A],
1287
+ subscriberNotificationStatusOnIncidentCreated:
1288
+ StatusPageSubscriberNotificationStatus.Skipped,
1289
+ statusPagesNotifiedOnCreation: null as unknown as Array<string>,
1290
+ }),
1291
+ storedIncident({
1292
+ _id: secondIncidentId,
1293
+ ...other,
1294
+ } as Partial<Incident>),
1295
+ ];
1296
+
1297
+ await expect(
1298
+ runBeforeUpdate(
1299
+ scopeUpdate({
1300
+ data: { statusPages: [PAGE_A, PAGE_B, PAGE_C] },
1301
+ query: { projectId: projectId },
1302
+ miscDataProps:
1303
+ IncidentScopeAddedPagesNotification.getMiscDataProps(),
1304
+ }),
1305
+ ),
1306
+ ).rejects.toThrow("Change them one at a time.");
1307
+ },
1308
+ );
1309
+ });
1310
+
1311
+ /*
1312
+ * Picking a status page decides who is told about an incident, so it needs
1313
+ * read access to that page - through the API as well as the picker.
1314
+ */
1315
+ describe("IncidentService.onBeforeUpdate: the pages an editor adds must be pages they can read", () => {
1316
+ test("an editor without status page access cannot add a page", async () => {
1317
+ readableStatusPageIds = null;
1318
+ storedIncidents = [storedIncident({ statusPageIds: [] })];
1319
+
1320
+ await expect(
1321
+ runBeforeUpdate(
1322
+ scopeUpdate({
1323
+ data: { statusPages: [PAGE_A] },
1324
+ props: makeProps([Permission.IncidentMember]),
1325
+ }),
1326
+ ),
1327
+ ).rejects.toThrow(StatusPageReadAccess.getRefusalMessage("incident"));
1328
+ });
1329
+
1330
+ test("a label-restricted editor cannot add a page outside their labels", async () => {
1331
+ readableStatusPageIds = [PAGE_A];
1332
+ storedIncidents = [storedIncident({ statusPageIds: [PAGE_A] })];
1333
+
1334
+ await expect(
1335
+ runBeforeUpdate(scopeUpdate({ data: { statusPages: [PAGE_A, PAGE_C] } })),
1336
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
1337
+ });
1338
+
1339
+ test("pages the incident already holds are not added, so they are not checked", async () => {
1340
+ // The dashboard sends the loaded list back, hidden pages included.
1341
+ readableStatusPageIds = [PAGE_A];
1342
+ storedIncidents = [
1343
+ storedIncident({ statusPageIds: [PAGE_A, HIDDEN_PAGE_X] }),
1344
+ ];
1345
+
1346
+ const { data } = await runBeforeUpdate(
1347
+ scopeUpdate({ data: { statusPages: [HIDDEN_PAGE_X] } }),
1348
+ );
1349
+
1350
+ expect(savedStatusPageIds(data)).toEqual([HIDDEN_PAGE_X]);
1351
+ });
1352
+
1353
+ test("removing pages needs no read access", async () => {
1354
+ readableStatusPageIds = null;
1355
+ storedIncidents = [storedIncident({ statusPageIds: [PAGE_A, PAGE_B] })];
1356
+
1357
+ const { data } = await runBeforeUpdate(
1358
+ scopeUpdate({
1359
+ data: { statusPages: [PAGE_A] },
1360
+ props: makeProps([Permission.IncidentMember]),
1361
+ }),
1362
+ );
1363
+
1364
+ // A was not readable, so it is kept as a hidden page too.
1365
+ expect(savedStatusPageIds(data).sort()).toEqual([PAGE_A, PAGE_B].sort());
1366
+ });
1367
+
1368
+ test("a bulk edit checks every page it adds to any matched incident", async () => {
1369
+ readableStatusPageIds = [PAGE_A, PAGE_C];
1370
+ storedIncidents = [
1371
+ storedIncident({ statusPageIds: [PAGE_A, PAGE_C] }),
1372
+ storedIncident({
1373
+ _id: secondIncidentId,
1374
+ statusPageIds: [PAGE_A],
1375
+ } as Partial<Incident>),
1376
+ ];
1377
+
1378
+ // C is added to the second one only, and readable: fine.
1379
+ const { data } = await runBeforeUpdate(
1380
+ scopeUpdate({
1381
+ data: { statusPages: [PAGE_A, PAGE_C] },
1382
+ query: { projectId: projectId },
1383
+ }),
1384
+ );
1385
+
1386
+ expect(savedStatusPageIds(data)).toEqual([PAGE_A, PAGE_C]);
1387
+ expect(
1388
+ idsInQueryOperator(
1389
+ statusPageFindBy.mock.calls[statusPageFindBy.mock.calls.length - 1]![0]
1390
+ .query["_id"],
1391
+ ),
1392
+ ).toEqual([PAGE_C]);
1393
+
1394
+ // B is added to both, and not readable.
1395
+ await expect(
1396
+ runBeforeUpdate(
1397
+ scopeUpdate({
1398
+ data: { statusPages: [PAGE_A, PAGE_B, PAGE_C] },
1399
+ query: { projectId: projectId },
1400
+ }),
1401
+ ),
1402
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
1403
+ });
1404
+
1405
+ test("a root caller adds any page of the project", async () => {
1406
+ readableStatusPageIds = null;
1407
+ storedIncidents = [storedIncident({ statusPageIds: [] })];
1408
+
1409
+ const { data } = await runBeforeUpdate(
1410
+ scopeUpdate({
1411
+ data: { statusPages: [PAGE_A] },
1412
+ props: { isRoot: true },
1413
+ }),
1414
+ );
1415
+
1416
+ expect(savedStatusPageIds(data)).toEqual([PAGE_A]);
1417
+ expect(statusPageFindBy).not.toHaveBeenCalled();
1418
+ });
1419
+ });
1420
+
1421
+ /*
1422
+ * The API route of resending the 'created' notification - setting its status
1423
+ * back to Pending - goes to every page, as it always did. The job skips the
1424
+ * pages in its record, so the record is emptied in the same write.
1425
+ */
1426
+ describe("IncidentService.onBeforeUpdate: a resend of the 'created' notification empties the record", () => {
1427
+ function resend(
1428
+ props: DatabaseCommonInteractionProps = MEMBER_PROPS,
1429
+ extra: Record<string, unknown> = {},
1430
+ ): UpdateBy<Incident> {
1431
+ return scopeUpdate({
1432
+ data: {
1433
+ subscriberNotificationStatusOnIncidentCreated:
1434
+ StatusPageSubscriberNotificationStatus.Pending,
1435
+ ...extra,
1436
+ },
1437
+ props: props,
1438
+ });
1439
+ }
1440
+
1441
+ test.each([
1442
+ StatusPageSubscriberNotificationStatus.Success,
1443
+ StatusPageSubscriberNotificationStatus.Skipped,
1444
+ ])(
1445
+ "resending a notification that is %s reaches every page again",
1446
+ async (status: StatusPageSubscriberNotificationStatus) => {
1447
+ storedIncidents = [
1448
+ storedIncident({
1449
+ subscriberNotificationStatusOnIncidentCreated: status,
1450
+ statusPagesNotifiedOnCreation: [PAGE_A, PAGE_B],
1451
+ }),
1452
+ ];
1453
+
1454
+ const { data } = await runBeforeUpdate(resend());
1455
+
1456
+ expect(data["subscriberNotificationStatusOnIncidentCreated"]).toBe(
1457
+ StatusPageSubscriberNotificationStatus.Pending,
1458
+ );
1459
+ expect(data["statusPagesNotifiedOnCreation"]).toEqual([]);
1460
+ },
1461
+ );
1462
+
1463
+ test("Retry after a failure resumes: the pages the failed send told keep their record", async () => {
1464
+ storedIncidents = [
1465
+ storedIncident({
1466
+ subscriberNotificationStatusOnIncidentCreated:
1467
+ StatusPageSubscriberNotificationStatus.Failed,
1468
+ statusPagesNotifiedOnCreation: [PAGE_A],
1469
+ }),
1470
+ ];
1471
+
1472
+ const { data } = await runBeforeUpdate(resend());
1473
+
1474
+ expect(data).not.toHaveProperty("statusPagesNotifiedOnCreation");
1475
+ });
1476
+
1477
+ test("a notification that is Pending keeps its record", async () => {
1478
+ storedIncidents = [
1479
+ storedIncident({
1480
+ subscriberNotificationStatusOnIncidentCreated:
1481
+ StatusPageSubscriberNotificationStatus.Pending,
1482
+ statusPagesNotifiedOnCreation: [PAGE_A],
1483
+ }),
1484
+ ];
1485
+
1486
+ const { data } = await runBeforeUpdate(resend());
1487
+
1488
+ expect(data).not.toHaveProperty("statusPagesNotifiedOnCreation");
1489
+ });
1490
+
1491
+ /*
1492
+ * A user's resend of a notification being sent is refused outright
1493
+ * (SubscriberNotificationInFlightRequeue.test.ts), so its record is never
1494
+ * touched; a root caller's keeps it.
1495
+ */
1496
+ test("a notification that is InProgress is not sent again by a user, and a root caller keeps its record", async () => {
1497
+ storedIncidents = [
1498
+ storedIncident({
1499
+ subscriberNotificationStatusOnIncidentCreated:
1500
+ StatusPageSubscriberNotificationStatus.InProgress,
1501
+ statusPagesNotifiedOnCreation: [PAGE_A],
1502
+ }),
1503
+ ];
1504
+
1505
+ await expect(runBeforeUpdate(resend())).rejects.toThrow(
1506
+ SubscriberNotificationResend.beingSentMessage,
1507
+ );
1508
+
1509
+ const { data } = await runBeforeUpdate(resend({ isRoot: true }));
1510
+
1511
+ expect(data).not.toHaveProperty("statusPagesNotifiedOnCreation");
1512
+ });
1513
+
1514
+ test("turning notifying on creation on (root) resends to every page", async () => {
1515
+ storedIncidents = [
1516
+ storedIncident({
1517
+ statusPagesNotifiedOnCreation: [PAGE_A],
1518
+ }),
1519
+ ];
1520
+
1521
+ const { data } = await runBeforeUpdate(
1522
+ scopeUpdate({
1523
+ data: {
1524
+ shouldStatusPageSubscribersBeNotifiedOnIncidentCreated: true,
1525
+ },
1526
+ props: { isRoot: true },
1527
+ }),
1528
+ );
1529
+
1530
+ expect(data["subscriberNotificationStatusOnIncidentCreated"]).toBe(
1531
+ StatusPageSubscriberNotificationStatus.Pending,
1532
+ );
1533
+ expect(data["statusPagesNotifiedOnCreation"]).toEqual([]);
1534
+ });
1535
+
1536
+ test("a bulk resend empties the record when any matched incident would reach nobody", async () => {
1537
+ storedIncidents = [
1538
+ storedIncident({
1539
+ subscriberNotificationStatusOnIncidentCreated:
1540
+ StatusPageSubscriberNotificationStatus.Failed,
1541
+ statusPagesNotifiedOnCreation: [PAGE_A],
1542
+ }),
1543
+ storedIncident({
1544
+ _id: secondIncidentId,
1545
+ statusPagesNotifiedOnCreation: [PAGE_A],
1546
+ } as Partial<Incident>),
1547
+ ];
1548
+
1549
+ const { data } = await runBeforeUpdate(resend());
1550
+
1551
+ expect(data["statusPagesNotifiedOnCreation"]).toEqual([]);
1552
+ });
1553
+
1554
+ test("a client's own record never survives, even on a resend", async () => {
1555
+ storedIncidents = [
1556
+ storedIncident({
1557
+ subscriberNotificationStatusOnIncidentCreated:
1558
+ StatusPageSubscriberNotificationStatus.Failed,
1559
+ }),
1560
+ ];
1561
+
1562
+ const { data } = await runBeforeUpdate(
1563
+ resend(MEMBER_PROPS, { statusPagesNotifiedOnCreation: [PAGE_C] }),
1564
+ );
1565
+
1566
+ expect(data).not.toHaveProperty("statusPagesNotifiedOnCreation");
1567
+ });
1568
+
1569
+ test("a root caller that writes the record itself keeps it", async () => {
1570
+ storedIncidents = [storedIncident()];
1571
+
1572
+ const { data } = await runBeforeUpdate(
1573
+ resend({ isRoot: true }, { statusPagesNotifiedOnCreation: [PAGE_C] }),
1574
+ );
1575
+
1576
+ expect(data["statusPagesNotifiedOnCreation"]).toEqual([PAGE_C]);
1577
+ });
1578
+
1579
+ test("an update that does not resend reads nothing for it", async () => {
1580
+ await runBeforeUpdate(scopeUpdate({ data: { title: "Renamed" } }));
1581
+
1582
+ expect(incidentFindBy).not.toHaveBeenCalled();
1583
+ });
1584
+
1585
+ test("the resend's read is limited to the caller's project", async () => {
1586
+ storedIncidents = [storedIncident()];
1587
+
1588
+ await runBeforeUpdate(resend());
1589
+
1590
+ // The hook's own read, as root; the in-flight check reads as the caller.
1591
+ const rootReads: Array<{ query: JSONObject; props: JSONObject }> =
1592
+ incidentFindBy.mock.calls
1593
+ .map((call: Array<unknown>) => {
1594
+ return call[0] as { query: JSONObject; props: JSONObject };
1595
+ })
1596
+ .filter((read: { props: JSONObject }) => {
1597
+ return read.props["isRoot"] === true;
1598
+ });
1599
+
1600
+ expect(rootReads).toHaveLength(1);
1601
+ expect(rootReads[0]!.query["projectId"]).toBe(projectId);
1602
+ expect(rootReads[0]!.props).toEqual({ isRoot: true });
1603
+ });
1604
+ });
1605
+
1606
+ /*
1607
+ * A page added to an incident's scope while it is hidden cannot be told then,
1608
+ * and is no longer "added" by any later edit. Publishing offers to tell it.
1609
+ */
1610
+ describe("IncidentService.onBeforeUpdate: publishing tells the pages added while the incident was hidden", () => {
1611
+ function publish(extra: Record<string, unknown> = {}): UpdateBy<Incident> {
1612
+ return scopeUpdate({
1613
+ data: { isVisibleOnStatusPage: true, ...extra },
1614
+ miscDataProps: IncidentCreatedRenotify.getMiscDataProps(),
1615
+ });
1616
+ }
1617
+
1618
+ function toldThenHidden(
1619
+ overrides: Partial<Incident> & { statusPageIds?: Array<string> } = {},
1620
+ ): Incident {
1621
+ // Visible and told on A, hidden, then B added to its scope.
1622
+ return storedIncident({
1623
+ statusPageIds: [PAGE_A, PAGE_B],
1624
+ isVisibleOnStatusPage: false,
1625
+ subscriberNotificationStatusOnIncidentCreated:
1626
+ StatusPageSubscriberNotificationStatus.Success,
1627
+ statusPagesNotifiedOnCreation: [PAGE_A],
1628
+ ...overrides,
1629
+ });
1630
+ }
1631
+
1632
+ test("queues the notification for the pages that were never told", async () => {
1633
+ storedIncidents = [toldThenHidden()];
1634
+
1635
+ const { data } = await runBeforeUpdate(publish());
1636
+
1637
+ expect(data["subscriberNotificationStatusOnIncidentCreated"]).toBe(
1638
+ StatusPageSubscriberNotificationStatus.Pending,
1639
+ );
1640
+ expect(data["subscriberNotificationStatusMessage"]).toBe(
1641
+ IncidentCreatedRenotify.queuedMessage,
1642
+ );
1643
+ // The record stays: the job tells B, not A again.
1644
+ expect(data).not.toHaveProperty("statusPagesNotifiedOnCreation");
1645
+ });
1646
+
1647
+ test("queues nothing when every page of its scope was told", async () => {
1648
+ storedIncidents = [
1649
+ toldThenHidden({ statusPagesNotifiedOnCreation: [PAGE_A, PAGE_B] }),
1650
+ ];
1651
+
1652
+ const { data } = await runBeforeUpdate(publish());
1653
+
1654
+ expect(data).not.toHaveProperty(
1655
+ "subscriberNotificationStatusOnIncidentCreated",
1656
+ );
1657
+ });
1658
+
1659
+ test("queues nothing for an incident told before the record existed", async () => {
1660
+ storedIncidents = [
1661
+ toldThenHidden({
1662
+ statusPagesNotifiedOnCreation: null as unknown as Array<string>,
1663
+ }),
1664
+ ];
1665
+
1666
+ const { data } = await runBeforeUpdate(publish());
1667
+
1668
+ expect(data).not.toHaveProperty(
1669
+ "subscriberNotificationStatusOnIncidentCreated",
1670
+ );
1671
+ });
1672
+
1673
+ test("without the box, nothing is queued", async () => {
1674
+ storedIncidents = [toldThenHidden()];
1675
+
1676
+ const { data } = await runBeforeUpdate(
1677
+ scopeUpdate({ data: { isVisibleOnStatusPage: true } }),
1678
+ );
1679
+
1680
+ expect(data).not.toHaveProperty(
1681
+ "subscriberNotificationStatusOnIncidentCreated",
1682
+ );
1683
+ });
1684
+
1685
+ test("the same edit's list of pages is the one that counts", async () => {
1686
+ storedIncidents = [
1687
+ toldThenHidden({ statusPagesNotifiedOnCreation: [PAGE_A, PAGE_B] }),
1688
+ ];
1689
+
1690
+ const { data } = await runBeforeUpdate(
1691
+ publish({ statusPages: [PAGE_A, PAGE_B, PAGE_C] }),
1692
+ );
1693
+
1694
+ expect(data["subscriberNotificationStatusOnIncidentCreated"]).toBe(
1695
+ StatusPageSubscriberNotificationStatus.Pending,
1696
+ );
1697
+ });
1698
+
1699
+ test("reads the scope and the record, within the caller's project", async () => {
1700
+ storedIncidents = [toldThenHidden()];
1701
+
1702
+ await runBeforeUpdate(publish());
1703
+
1704
+ const findBy: {
1705
+ query: Record<string, unknown>;
1706
+ select: Record<string, unknown>;
1707
+ } = incidentFindBy.mock.calls[0]![0];
1708
+
1709
+ expect(findBy.select["statusPages"]).toEqual({ _id: true });
1710
+ expect(findBy.select["statusPagesNotifiedOnCreation"]).toBe(true);
1711
+ expect(findBy.query["projectId"]).toBe(projectId);
1712
+ });
1713
+ });
1714
+
1715
+ describe("IncidentService.onBeforeCreate: the scope of a new incident", () => {
1716
+ let template: IncidentTemplate | null = null;
1717
+
1718
+ beforeEach(() => {
1719
+ template = null;
1720
+
1721
+ const createdState: IncidentState = new IncidentState();
1722
+ createdState._id = "c0000000-0000-4000-8000-000000000001";
1723
+ jest
1724
+ .spyOn(IncidentStateService, "findOneBy")
1725
+ .mockResolvedValue(createdState as never);
1726
+ jest
1727
+ .spyOn(ProjectService, "incrementAndGetIncidentCounter")
1728
+ .mockResolvedValue({ counter: 7, prefix: undefined } as never);
1729
+ jest
1730
+ .spyOn(
1731
+ ProjectScopedReferenceValidator,
1732
+ "validateReferencesBelongToProject",
1733
+ )
1734
+ .mockResolvedValue(undefined as never);
1735
+ // The root cause names the user who declared the incident.
1736
+ jest
1737
+ .spyOn(UserService, "getUserMarkdownString")
1738
+ .mockResolvedValue("Test User" as never);
1739
+ jest.spyOn(IncidentTemplateService, "findOneBy").mockImplementation((() => {
1740
+ return Promise.resolve(template);
1741
+ }) as never);
1742
+ });
1743
+
1744
+ function newIncident(overrides: Partial<Incident> = {}): Incident {
1745
+ const incident: Incident = new Incident();
1746
+ incident.title = "Site outage";
1747
+ incident.incidentSeverityId = new ObjectID(
1748
+ "d0000000-0000-4000-8000-000000000001",
1749
+ );
1750
+ Object.assign(incident, overrides);
1751
+ return incident;
1752
+ }
1753
+
1754
+ async function runBeforeCreate(
1755
+ data: Incident,
1756
+ props: DatabaseCommonInteractionProps = { tenantId: projectId },
1757
+ ): Promise<Incident> {
1758
+ const onCreate: OnCreate<Incident> = await (
1759
+ IncidentService as unknown as { onBeforeCreate: OnBeforeCreate }
1760
+ ).onBeforeCreate({ data, props });
1761
+
1762
+ return onCreate.createBy.data;
1763
+ }
1764
+
1765
+ test("an incident created with pages is scoped", async () => {
1766
+ const created: Incident = await runBeforeCreate(
1767
+ newIncident({ statusPages: [statusPage(PAGE_A), statusPage(PAGE_B)] }),
1768
+ );
1769
+
1770
+ expect(
1771
+ IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
1772
+ created.statusPages,
1773
+ ),
1774
+ ).toEqual([PAGE_A, PAGE_B]);
1775
+ expect(created.isScopedToStatusPages).toBe(true);
1776
+ });
1777
+
1778
+ test("an incident created without pages is unscoped", async () => {
1779
+ const created: Incident = await runBeforeCreate(newIncident());
1780
+
1781
+ expect(created.isScopedToStatusPages).toBe(false);
1782
+ expect(created.statusPages).toBeUndefined();
1783
+ });
1784
+
1785
+ test("an empty list is unscoped", async () => {
1786
+ const created: Incident = await runBeforeCreate(
1787
+ newIncident({ statusPages: [] }),
1788
+ );
1789
+
1790
+ expect(created.isScopedToStatusPages).toBe(false);
1791
+ });
1792
+
1793
+ test("ignores a flag the client sends: scoped with no pages would hide the incident everywhere", async () => {
1794
+ const created: Incident = await runBeforeCreate(
1795
+ newIncident({ isScopedToStatusPages: true }),
1796
+ );
1797
+
1798
+ expect(created.isScopedToStatusPages).toBe(false);
1799
+
1800
+ const scoped: Incident = await runBeforeCreate(
1801
+ newIncident({
1802
+ isScopedToStatusPages: false,
1803
+ statusPages: [statusPage(PAGE_A)],
1804
+ }),
1805
+ );
1806
+
1807
+ expect(scoped.isScopedToStatusPages).toBe(true);
1808
+ });
1809
+
1810
+ test("deduplicates the pages and writes them as stubs", async () => {
1811
+ const created: Incident = await runBeforeCreate(
1812
+ newIncident({
1813
+ statusPages: [
1814
+ statusPage(PAGE_A, "Site A"),
1815
+ PAGE_A.toUpperCase() as unknown as StatusPage,
1816
+ { _id: PAGE_B } as unknown as StatusPage,
1817
+ ],
1818
+ }),
1819
+ );
1820
+
1821
+ expect(created.statusPages).toHaveLength(2);
1822
+ for (const page of created.statusPages || []) {
1823
+ expect(page).toBeInstanceOf(StatusPage);
1824
+ }
1825
+ });
1826
+
1827
+ test("a client cannot seed the record of notified pages", async () => {
1828
+ const created: Incident = await runBeforeCreate(
1829
+ newIncident({ statusPagesNotifiedOnCreation: [PAGE_A] }),
1830
+ MEMBER_PROPS,
1831
+ );
1832
+
1833
+ expect(created.statusPagesNotifiedOnCreation).toBeUndefined();
1834
+ });
1835
+
1836
+ test("a root caller keeps the record it sends", async () => {
1837
+ const incident: Incident = newIncident({
1838
+ statusPagesNotifiedOnCreation: [PAGE_A],
1839
+ });
1840
+ incident.projectId = projectId;
1841
+
1842
+ const created: Incident = await runBeforeCreate(incident, {
1843
+ isRoot: true,
1844
+ });
1845
+
1846
+ expect(created.statusPagesNotifiedOnCreation).toEqual([PAGE_A]);
1847
+ });
1848
+
1849
+ describe("the pages a caller picks must be pages they can read", () => {
1850
+ let callerTemplates: Array<IncidentTemplate> | Error = [];
1851
+ let templatesWithPages: Array<IncidentTemplate> = [];
1852
+
1853
+ beforeEach(() => {
1854
+ callerTemplates = [];
1855
+ templatesWithPages = [];
1856
+
1857
+ jest.spyOn(IncidentTemplateService, "findBy").mockImplementation(((data: {
1858
+ props: DatabaseCommonInteractionProps;
1859
+ }) => {
1860
+ if (!data.props.isRoot) {
1861
+ return callerTemplates instanceof Error
1862
+ ? Promise.reject(callerTemplates)
1863
+ : Promise.resolve(callerTemplates);
1864
+ }
1865
+
1866
+ return Promise.resolve(templatesWithPages);
1867
+ }) as never);
1868
+ });
1869
+
1870
+ function template(
1871
+ id: string,
1872
+ statusPageIds: Array<string>,
1873
+ ): IncidentTemplate {
1874
+ const incidentTemplate: IncidentTemplate = new IncidentTemplate();
1875
+ incidentTemplate._id = id;
1876
+ incidentTemplate.statusPages = statusPageIds.map((pageId: string) => {
1877
+ return statusPage(pageId);
1878
+ });
1879
+ return incidentTemplate;
1880
+ }
1881
+
1882
+ test("a page the caller cannot read is refused", async () => {
1883
+ readableStatusPageIds = [PAGE_A];
1884
+
1885
+ await expect(
1886
+ runBeforeCreate(
1887
+ newIncident({
1888
+ statusPages: [statusPage(PAGE_A), statusPage(PAGE_C)],
1889
+ }),
1890
+ MEMBER_PROPS,
1891
+ ),
1892
+ ).rejects.toThrow(StatusPageReadAccess.getRefusalMessage("incident"));
1893
+ });
1894
+
1895
+ test("a caller with no status page access at all cannot pick one", async () => {
1896
+ readableStatusPageIds = null;
1897
+
1898
+ await expect(
1899
+ runBeforeCreate(
1900
+ newIncident({ statusPages: [statusPage(PAGE_A)] }),
1901
+ makeProps([Permission.IncidentMember]),
1902
+ ),
1903
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
1904
+ });
1905
+
1906
+ test("a page of a template the caller can read may be picked without reading it", async () => {
1907
+ // The dashboard fills a template's pages in for whoever declares from it.
1908
+ readableStatusPageIds = null;
1909
+ callerTemplates = [template(templateId, [])];
1910
+ templatesWithPages = [template(templateId, [PAGE_C])];
1911
+
1912
+ const created: Incident = await runBeforeCreate(
1913
+ newIncident({ statusPages: [statusPage(PAGE_C)] }),
1914
+ makeProps([Permission.IncidentMember]),
1915
+ );
1916
+
1917
+ expect(
1918
+ IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
1919
+ created.statusPages,
1920
+ ),
1921
+ ).toEqual([PAGE_C]);
1922
+ expect(created.isScopedToStatusPages).toBe(true);
1923
+
1924
+ // The templates are read with the caller's own permissions first.
1925
+ const callerRead: {
1926
+ query: Record<string, unknown>;
1927
+ props: DatabaseCommonInteractionProps;
1928
+ } = (IncidentTemplateService.findBy as unknown as MockFunction).mock
1929
+ .calls[0]![0];
1930
+
1931
+ expect(callerRead.props.isRoot).toBeFalsy();
1932
+ expect(callerRead.query["projectId"]).toBe(projectId);
1933
+ });
1934
+
1935
+ test("a template page does not cover another page the caller cannot read", async () => {
1936
+ readableStatusPageIds = null;
1937
+ callerTemplates = [template(templateId, [])];
1938
+ templatesWithPages = [template(templateId, [PAGE_C])];
1939
+
1940
+ await expect(
1941
+ runBeforeCreate(
1942
+ newIncident({
1943
+ statusPages: [statusPage(PAGE_C), statusPage(PAGE_B)],
1944
+ }),
1945
+ makeProps([Permission.IncidentMember]),
1946
+ ),
1947
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
1948
+ });
1949
+
1950
+ test("a caller who cannot read templates gets no template's pages", async () => {
1951
+ readableStatusPageIds = null;
1952
+ callerTemplates = new NotAuthorizedException(
1953
+ "You do not have permissions to read Incident Template.",
1954
+ );
1955
+ templatesWithPages = [template(templateId, [PAGE_C])];
1956
+
1957
+ await expect(
1958
+ runBeforeCreate(
1959
+ newIncident({ statusPages: [statusPage(PAGE_C)] }),
1960
+ makeProps([Permission.IncidentMember]),
1961
+ ),
1962
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
1963
+ });
1964
+
1965
+ test("a root caller picks any page, and nothing is read for it", async () => {
1966
+ readableStatusPageIds = null;
1967
+
1968
+ const incident: Incident = newIncident({
1969
+ statusPages: [statusPage(PAGE_C)],
1970
+ });
1971
+ incident.projectId = projectId;
1972
+
1973
+ const created: Incident = await runBeforeCreate(incident, {
1974
+ isRoot: true,
1975
+ });
1976
+
1977
+ expect(created.isScopedToStatusPages).toBe(true);
1978
+ expect(statusPageFindBy).not.toHaveBeenCalled();
1979
+ });
1980
+ });
1981
+
1982
+ describe("declared from a template", () => {
1983
+ function templateWithPages(ids: Array<string>): IncidentTemplate {
1984
+ const incidentTemplate: IncidentTemplate = new IncidentTemplate();
1985
+ incidentTemplate._id = templateId;
1986
+ incidentTemplate.statusPages = ids.map((id: string) => {
1987
+ return statusPage(id);
1988
+ });
1989
+ return incidentTemplate;
1990
+ }
1991
+
1992
+ test("copies the template's pages and scopes the incident", async () => {
1993
+ template = templateWithPages([PAGE_A, PAGE_C]);
1994
+
1995
+ const created: Incident = await runBeforeCreate(
1996
+ newIncident({ createdIncidentTemplateId: templateId }),
1997
+ );
1998
+
1999
+ expect(
2000
+ IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
2001
+ created.statusPages,
2002
+ ),
2003
+ ).toEqual([PAGE_A, PAGE_C]);
2004
+ expect(created.isScopedToStatusPages).toBe(true);
2005
+ });
2006
+
2007
+ test("reads the template's pages, as root and within the project", async () => {
2008
+ template = templateWithPages([PAGE_A]);
2009
+
2010
+ await runBeforeCreate(
2011
+ newIncident({ createdIncidentTemplateId: templateId }),
2012
+ );
2013
+
2014
+ const findOneBy: {
2015
+ query: Record<string, unknown>;
2016
+ select: Record<string, unknown>;
2017
+ props: DatabaseCommonInteractionProps;
2018
+ } = (IncidentTemplateService.findOneBy as unknown as MockFunction).mock
2019
+ .calls[0]![0];
2020
+
2021
+ expect(findOneBy.select["statusPages"]).toEqual({ _id: true });
2022
+ expect(findOneBy.query["projectId"]).toBe(projectId);
2023
+ expect(findOneBy.props).toEqual({ isRoot: true });
2024
+ });
2025
+
2026
+ test("the pages the caller picked win over the template's", async () => {
2027
+ template = templateWithPages([PAGE_A, PAGE_C]);
2028
+
2029
+ const created: Incident = await runBeforeCreate(
2030
+ newIncident({
2031
+ createdIncidentTemplateId: templateId,
2032
+ statusPages: [statusPage(PAGE_B)],
2033
+ }),
2034
+ );
2035
+
2036
+ expect(
2037
+ IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
2038
+ created.statusPages,
2039
+ ),
2040
+ ).toEqual([PAGE_B]);
2041
+ });
2042
+
2043
+ test("an explicitly empty list is an intentional override: the incident stays unscoped", async () => {
2044
+ template = templateWithPages([PAGE_A]);
2045
+
2046
+ const created: Incident = await runBeforeCreate(
2047
+ newIncident({ createdIncidentTemplateId: templateId, statusPages: [] }),
2048
+ );
2049
+
2050
+ expect(created.statusPages).toEqual([]);
2051
+ expect(created.isScopedToStatusPages).toBe(false);
2052
+ });
2053
+
2054
+ test("a template without pages leaves the incident unscoped", async () => {
2055
+ template = templateWithPages([]);
2056
+
2057
+ const created: Incident = await runBeforeCreate(
2058
+ newIncident({ createdIncidentTemplateId: templateId }),
2059
+ );
2060
+
2061
+ expect(created.statusPages).toBeUndefined();
2062
+ expect(created.isScopedToStatusPages).toBe(false);
2063
+ });
2064
+
2065
+ test("pages copied from the template are not the caller's pick, so they are not checked", async () => {
2066
+ readableStatusPageIds = null;
2067
+ template = templateWithPages([PAGE_C]);
2068
+
2069
+ const created: Incident = await runBeforeCreate(
2070
+ newIncident({ createdIncidentTemplateId: templateId }),
2071
+ makeProps([Permission.IncidentMember]),
2072
+ );
2073
+
2074
+ expect(
2075
+ IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
2076
+ created.statusPages,
2077
+ ),
2078
+ ).toEqual([PAGE_C]);
2079
+ expect(statusPageFindBy).not.toHaveBeenCalled();
2080
+ });
2081
+
2082
+ describe("a template whose status pages were all deleted", () => {
2083
+ function scopedTemplateWithoutPages(): IncidentTemplate {
2084
+ const incidentTemplate: IncidentTemplate = templateWithPages([]);
2085
+ incidentTemplate.isScopedToStatusPages = true;
2086
+ return incidentTemplate;
2087
+ }
2088
+
2089
+ test("declares the incident scoped to nothing: hidden, rather than on every page", async () => {
2090
+ template = scopedTemplateWithoutPages();
2091
+
2092
+ const created: Incident = await runBeforeCreate(
2093
+ newIncident({ createdIncidentTemplateId: templateId }),
2094
+ );
2095
+
2096
+ expect(created.isScopedToStatusPages).toBe(true);
2097
+ expect(
2098
+ IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
2099
+ created.statusPages,
2100
+ ),
2101
+ ).toEqual([]);
2102
+ });
2103
+
2104
+ test("reads the template's flag", async () => {
2105
+ template = scopedTemplateWithoutPages();
2106
+
2107
+ await runBeforeCreate(
2108
+ newIncident({ createdIncidentTemplateId: templateId }),
2109
+ );
2110
+
2111
+ const findOneBy: { select: Record<string, unknown> } = (
2112
+ IncidentTemplateService.findOneBy as unknown as MockFunction
2113
+ ).mock.calls[0]![0];
2114
+
2115
+ expect(findOneBy.select["isScopedToStatusPages"]).toBe(true);
2116
+ });
2117
+
2118
+ test("the pages the caller picked still win", async () => {
2119
+ template = scopedTemplateWithoutPages();
2120
+
2121
+ const created: Incident = await runBeforeCreate(
2122
+ newIncident({
2123
+ createdIncidentTemplateId: templateId,
2124
+ statusPages: [statusPage(PAGE_B)],
2125
+ }),
2126
+ );
2127
+
2128
+ expect(
2129
+ IncidentScopeAddedPagesNotification.normalizeStatusPageIds(
2130
+ created.statusPages,
2131
+ ),
2132
+ ).toEqual([PAGE_B]);
2133
+ expect(created.isScopedToStatusPages).toBe(true);
2134
+ });
2135
+
2136
+ test("an explicitly empty list is still an intentional override", async () => {
2137
+ template = scopedTemplateWithoutPages();
2138
+
2139
+ const created: Incident = await runBeforeCreate(
2140
+ newIncident({
2141
+ createdIncidentTemplateId: templateId,
2142
+ statusPages: [],
2143
+ }),
2144
+ );
2145
+
2146
+ expect(created.isScopedToStatusPages).toBe(false);
2147
+ });
2148
+
2149
+ test("a template that was never scoped still declares unscoped incidents", async () => {
2150
+ template = templateWithPages([]);
2151
+ template.isScopedToStatusPages = false;
2152
+
2153
+ const created: Incident = await runBeforeCreate(
2154
+ newIncident({ createdIncidentTemplateId: templateId }),
2155
+ );
2156
+
2157
+ expect(created.isScopedToStatusPages).toBe(false);
2158
+ });
2159
+ });
2160
+
2161
+ test("the copied pages are validated against the project with the rest", async () => {
2162
+ template = templateWithPages([PAGE_A]);
2163
+
2164
+ await runBeforeCreate(
2165
+ newIncident({ createdIncidentTemplateId: templateId }),
2166
+ );
2167
+
2168
+ const validation: {
2169
+ references: Array<{ modelName: string; id: unknown }>;
2170
+ } = (
2171
+ ProjectScopedReferenceValidator.validateReferencesBelongToProject as unknown as MockFunction
2172
+ ).mock.calls[0]![0];
2173
+
2174
+ expect(
2175
+ validation.references
2176
+ .filter((reference: { modelName: string }) => {
2177
+ return reference.modelName === "Status Page";
2178
+ })
2179
+ .map((reference: { id: unknown }) => {
2180
+ return String(reference.id).toLowerCase();
2181
+ }),
2182
+ ).toEqual([PAGE_A]);
2183
+ });
2184
+ });
2185
+ });
2186
+
2187
+ describe("IncidentService.onUpdateSuccess: the feed records scope changes", () => {
2188
+ let createFeedItem: MockFunction;
2189
+
2190
+ beforeEach(() => {
2191
+ createFeedItem = getJestMockFunction();
2192
+ createFeedItem.mockResolvedValue(undefined);
2193
+ jest
2194
+ .spyOn(IncidentFeedService, "createIncidentFeedItem")
2195
+ .mockImplementation(createFeedItem as never);
2196
+
2197
+ jest
2198
+ .spyOn(CustomFieldMappingService, "restampAfterMultiRowUpdate")
2199
+ .mockReturnValue(undefined as never);
2200
+
2201
+ jest.spyOn(IncidentService, "findOneById").mockImplementation((() => {
2202
+ const incident: Incident = new Incident();
2203
+ incident._id = incidentId;
2204
+ incident.projectId = projectId;
2205
+ incident.incidentNumber = 42;
2206
+ incident.incidentNumberWithPrefix = "INC-42";
2207
+ return Promise.resolve(incident);
2208
+ }) as never);
2209
+
2210
+ jest
2211
+ .spyOn(IncidentService, "getIncidentLinkInDashboard")
2212
+ .mockResolvedValue(
2213
+ URL.fromString("https://oneuptime.example/incidents/42"),
2214
+ );
2215
+ jest
2216
+ .spyOn(StatusPageService, "getStatusPageLinkInDashboard")
2217
+ .mockImplementation(((_projectId: ObjectID, statusPageId: ObjectID) => {
2218
+ return Promise.resolve(
2219
+ URL.fromString(
2220
+ `https://oneuptime.example/status-pages/${statusPageId.toString()}`,
2221
+ ),
2222
+ );
2223
+ }) as never);
2224
+
2225
+ statusPageFindBy.mockImplementation(() => {
2226
+ return Promise.resolve([
2227
+ statusPage(PAGE_A, "Site 03"),
2228
+ statusPage(PAGE_B, "Site 07"),
2229
+ statusPage(PAGE_C, "Site 05"),
2230
+ ]);
2231
+ });
2232
+ });
2233
+
2234
+ async function runUpdateSuccess(
2235
+ scopeChange: ScopeCarryForward | undefined,
2236
+ ): Promise<void> {
2237
+ await (
2238
+ IncidentService as unknown as { onUpdateSuccess: OnUpdateSuccess }
2239
+ ).onUpdateSuccess(
2240
+ {
2241
+ updateBy: {
2242
+ query: { _id: incidentId },
2243
+ data: { statusPages: [] } as UpdateBy<Incident>["data"],
2244
+ props: { tenantId: projectId, userId },
2245
+ limit: 1,
2246
+ skip: 0,
2247
+ },
2248
+ carryForward: {
2249
+ [incidentId]: {
2250
+ monitorsRemoved: [],
2251
+ monitorsAdded: [],
2252
+ oldChangeMonitorStatusIdTo: undefined,
2253
+ newMonitorChangeStatusIdTo: undefined,
2254
+ statusPageScopeChange: scopeChange,
2255
+ },
2256
+ },
2257
+ },
2258
+ [new ObjectID(incidentId)],
2259
+ );
2260
+ }
2261
+
2262
+ function feedMarkdown(): string {
2263
+ expect(createFeedItem).toHaveBeenCalledTimes(1);
2264
+ return createFeedItem.mock.calls[0]![0].feedInfoInMarkdown as string;
2265
+ }
2266
+
2267
+ test("writes an Incident Updated item naming the pages added and removed", async () => {
2268
+ await runUpdateSuccess({
2269
+ addedStatusPageIds: [PAGE_A, PAGE_B],
2270
+ removedStatusPageIds: [PAGE_C],
2271
+ isScoped: true,
2272
+ notificationQueued: false,
2273
+ });
2274
+
2275
+ const item: {
2276
+ incidentFeedEventType: IncidentFeedEventType;
2277
+ projectId: ObjectID;
2278
+ userId: ObjectID;
2279
+ } = createFeedItem.mock.calls[0]![0];
2280
+
2281
+ expect(item.incidentFeedEventType).toBe(
2282
+ IncidentFeedEventType.IncidentUpdated,
2283
+ );
2284
+ expect(item.projectId).toBe(projectId);
2285
+ expect(item.userId).toBe(userId);
2286
+
2287
+ const markdown: string = feedMarkdown();
2288
+
2289
+ expect(markdown).toContain("INC-42");
2290
+ expect(markdown).toContain(
2291
+ `**📣 Status Pages Added**:\n- [Site 03](https://oneuptime.example/status-pages/${PAGE_A})\n- [Site 07](https://oneuptime.example/status-pages/${PAGE_B})\n`,
2292
+ );
2293
+ expect(markdown).toContain(
2294
+ `**🔕 Status Pages Removed**:\n- [Site 05](https://oneuptime.example/status-pages/${PAGE_C})\n`,
2295
+ );
2296
+ expect(markdown).toContain("only its selected status pages");
2297
+ expect(markdown).not.toContain("will be sent the notification");
2298
+ });
2299
+
2300
+ test("says when the added pages will be sent the incident-created notification", async () => {
2301
+ await runUpdateSuccess({
2302
+ addedStatusPageIds: [PAGE_B],
2303
+ removedStatusPageIds: [],
2304
+ isScoped: true,
2305
+ notificationQueued: true,
2306
+ });
2307
+
2308
+ expect(feedMarkdown()).toContain(
2309
+ "Subscribers of the added status pages will be sent the notification that this incident was created.",
2310
+ );
2311
+ });
2312
+
2313
+ test("says when the scope is cleared", async () => {
2314
+ await runUpdateSuccess({
2315
+ addedStatusPageIds: [],
2316
+ removedStatusPageIds: [PAGE_A],
2317
+ isScoped: false,
2318
+ notificationQueued: false,
2319
+ });
2320
+
2321
+ const markdown: string = feedMarkdown();
2322
+
2323
+ expect(markdown).not.toContain("Status Pages Added");
2324
+ expect(markdown).toContain("no longer limited to specific status pages");
2325
+ });
2326
+
2327
+ test("reads page names as root, within the incident's project", async () => {
2328
+ await runUpdateSuccess({
2329
+ addedStatusPageIds: [PAGE_A],
2330
+ removedStatusPageIds: [PAGE_C],
2331
+ isScoped: true,
2332
+ notificationQueued: false,
2333
+ });
2334
+
2335
+ const findBy: {
2336
+ query: Record<string, unknown>;
2337
+ props: DatabaseCommonInteractionProps;
2338
+ } = statusPageFindBy.mock.calls[0]![0];
2339
+
2340
+ expect(findBy.props).toEqual({ isRoot: true });
2341
+ expect(findBy.query["projectId"]).toBe(projectId);
2342
+ });
2343
+
2344
+ test("a page deleted since is still listed, without its name", async () => {
2345
+ await runUpdateSuccess({
2346
+ addedStatusPageIds: ["b0000000-0000-4000-8000-0000000000dd"],
2347
+ removedStatusPageIds: [],
2348
+ isScoped: true,
2349
+ notificationQueued: false,
2350
+ });
2351
+
2352
+ expect(feedMarkdown()).toContain("- A deleted status page\n");
2353
+ });
2354
+
2355
+ test("writes no item when the scope did not change", async () => {
2356
+ await runUpdateSuccess({
2357
+ addedStatusPageIds: [],
2358
+ removedStatusPageIds: [],
2359
+ isScoped: true,
2360
+ notificationQueued: false,
2361
+ });
2362
+
2363
+ expect(createFeedItem).not.toHaveBeenCalled();
2364
+ expect(statusPageFindBy).not.toHaveBeenCalled();
2365
+ });
2366
+
2367
+ test("writes no item when the update did not touch the scope", async () => {
2368
+ await runUpdateSuccess(undefined);
2369
+
2370
+ expect(createFeedItem).not.toHaveBeenCalled();
2371
+ });
2372
+
2373
+ test("a failure naming the pages does not fail the update that was already written", async () => {
2374
+ statusPageFindBy.mockImplementation(() => {
2375
+ return Promise.reject(new Error("connection lost"));
2376
+ });
2377
+
2378
+ await expect(
2379
+ runUpdateSuccess({
2380
+ addedStatusPageIds: [PAGE_A],
2381
+ removedStatusPageIds: [],
2382
+ isScoped: true,
2383
+ notificationQueued: false,
2384
+ }),
2385
+ ).resolves.toBeUndefined();
2386
+
2387
+ expect(createFeedItem).not.toHaveBeenCalled();
2388
+ });
2389
+
2390
+ /*
2391
+ * Pages added while the 'created' notification was Pending or being sent
2392
+ * leave the status alone. If the whole send ran between this update
2393
+ * reading the status and writing the pages, it missed them: once the pages
2394
+ * are written, the notification is queued again for them.
2395
+ */
2396
+ describe("a send that finished without the pages the update added", () => {
2397
+ let stored: Incident | null = null;
2398
+ let updateOneById: MockFunction;
2399
+
2400
+ beforeEach(() => {
2401
+ stored = null;
2402
+
2403
+ jest.spyOn(IncidentService, "findOneById").mockImplementation(((data: {
2404
+ select: Record<string, unknown>;
2405
+ }) => {
2406
+ if (data.select["subscriberNotificationStatusOnIncidentCreated"]) {
2407
+ return Promise.resolve(stored);
2408
+ }
2409
+
2410
+ const incident: Incident = new Incident();
2411
+ incident._id = incidentId;
2412
+ incident.projectId = projectId;
2413
+ incident.incidentNumber = 42;
2414
+ incident.incidentNumberWithPrefix = "INC-42";
2415
+ return Promise.resolve(incident);
2416
+ }) as never);
2417
+
2418
+ updateOneById = getJestMockFunction();
2419
+ updateOneById.mockResolvedValue(1);
2420
+ jest
2421
+ .spyOn(IncidentService, "updateOneById")
2422
+ .mockImplementation(updateOneById as never);
2423
+ });
2424
+
2425
+ function storedNotification(
2426
+ status: StatusPageSubscriberNotificationStatus,
2427
+ record: Array<string> | null,
2428
+ ): Incident {
2429
+ const incident: Incident = new Incident();
2430
+ incident._id = incidentId;
2431
+ incident.subscriberNotificationStatusOnIncidentCreated = status;
2432
+ incident.statusPagesNotifiedOnCreation = record as Array<string>;
2433
+ return incident;
2434
+ }
2435
+
2436
+ const alreadyQueuedChange: ScopeCarryForward = {
2437
+ addedStatusPageIds: [PAGE_B],
2438
+ removedStatusPageIds: [],
2439
+ isScoped: true,
2440
+ notificationQueued: false,
2441
+ notificationAlreadyQueued: true,
2442
+ };
2443
+
2444
+ test("queues it again when the send settled without an added page", async () => {
2445
+ stored = storedNotification(
2446
+ StatusPageSubscriberNotificationStatus.Success,
2447
+ [PAGE_A],
2448
+ );
2449
+
2450
+ await runUpdateSuccess(alreadyQueuedChange);
2451
+
2452
+ expect(updateOneById).toHaveBeenCalledTimes(1);
2453
+ expect(updateOneById.mock.calls[0]![0]).toEqual({
2454
+ id: new ObjectID(incidentId),
2455
+ data: {
2456
+ subscriberNotificationStatusOnIncidentCreated:
2457
+ StatusPageSubscriberNotificationStatus.Pending,
2458
+ subscriberNotificationStatusMessage:
2459
+ IncidentScopeAddedPagesNotification.queuedMessage,
2460
+ },
2461
+ props: {
2462
+ isRoot: true,
2463
+ ignoreHooks: true,
2464
+ },
2465
+ });
2466
+ });
2467
+
2468
+ test.each([
2469
+ StatusPageSubscriberNotificationStatus.Pending,
2470
+ StatusPageSubscriberNotificationStatus.InProgress,
2471
+ ])(
2472
+ "leaves a notification that is still %s alone: it reads the new pages itself",
2473
+ async (status: StatusPageSubscriberNotificationStatus) => {
2474
+ stored = storedNotification(status, [PAGE_A]);
2475
+
2476
+ await runUpdateSuccess(alreadyQueuedChange);
2477
+
2478
+ expect(updateOneById).not.toHaveBeenCalled();
2479
+ },
2480
+ );
2481
+
2482
+ test("leaves it alone when the send did reach the added page", async () => {
2483
+ stored = storedNotification(
2484
+ StatusPageSubscriberNotificationStatus.Success,
2485
+ [PAGE_A, PAGE_B],
2486
+ );
2487
+
2488
+ await runUpdateSuccess(alreadyQueuedChange);
2489
+
2490
+ expect(updateOneById).not.toHaveBeenCalled();
2491
+ });
2492
+
2493
+ test("leaves a failed send to its Retry", async () => {
2494
+ stored = storedNotification(
2495
+ StatusPageSubscriberNotificationStatus.Failed,
2496
+ [PAGE_A],
2497
+ );
2498
+
2499
+ await runUpdateSuccess(alreadyQueuedChange);
2500
+
2501
+ expect(updateOneById).not.toHaveBeenCalled();
2502
+ });
2503
+
2504
+ test("checks nothing for an update that queued the notification itself, or found it settled", async () => {
2505
+ stored = storedNotification(
2506
+ StatusPageSubscriberNotificationStatus.Success,
2507
+ [PAGE_A],
2508
+ );
2509
+
2510
+ await runUpdateSuccess({
2511
+ ...alreadyQueuedChange,
2512
+ notificationQueued: true,
2513
+ notificationAlreadyQueued: false,
2514
+ });
2515
+
2516
+ expect(updateOneById).not.toHaveBeenCalled();
2517
+ });
2518
+
2519
+ test("a failure checking does not fail the update that was already written", async () => {
2520
+ jest.spyOn(IncidentService, "findOneById").mockImplementation(((data: {
2521
+ select: Record<string, unknown>;
2522
+ }) => {
2523
+ if (data.select["subscriberNotificationStatusOnIncidentCreated"]) {
2524
+ return Promise.reject(new Error("connection lost"));
2525
+ }
2526
+
2527
+ const incident: Incident = new Incident();
2528
+ incident._id = incidentId;
2529
+ incident.projectId = projectId;
2530
+ incident.incidentNumber = 42;
2531
+ return Promise.resolve(incident);
2532
+ }) as never);
2533
+
2534
+ await expect(
2535
+ runUpdateSuccess(alreadyQueuedChange),
2536
+ ).resolves.toBeUndefined();
2537
+
2538
+ expect(updateOneById).not.toHaveBeenCalled();
2539
+ });
2540
+ });
2541
+ });
2542
+
2543
+ /*
2544
+ * isScopedToStatusPages and the notification columns are computed, and the
2545
+ * hook writes them into the caller's own update. The column check runs after
2546
+ * the hook and exempts computed columns only on create, so their update
2547
+ * access control has to let every role that may edit statusPages through, or
2548
+ * a non-root scope edit would fail with "not allowed to update".
2549
+ */
2550
+ describe("Incident column permissions for scope edits", () => {
2551
+ const HOOK_WRITTEN_COLUMNS: Array<string> = [
2552
+ "isScopedToStatusPages",
2553
+ "statusPagesNotifiedOnCreation",
2554
+ "subscriberNotificationStatusOnIncidentCreated",
2555
+ "subscriberNotificationStatusMessage",
2556
+ ];
2557
+
2558
+ function access(column: string): {
2559
+ create: Array<Permission>;
2560
+ read: Array<Permission>;
2561
+ update: Array<Permission>;
2562
+ } {
2563
+ const columnAccess: {
2564
+ create?: Array<Permission>;
2565
+ read?: Array<Permission>;
2566
+ update?: Array<Permission>;
2567
+ } = new Incident().getColumnAccessControlFor(column) || {};
2568
+
2569
+ return {
2570
+ create: (columnAccess.create || []).slice(),
2571
+ read: (columnAccess.read || []).slice(),
2572
+ update: (columnAccess.update || []).slice(),
2573
+ };
2574
+ }
2575
+
2576
+ test("statusPages has the access control of the monitors list", () => {
2577
+ expect(access("statusPages")).toEqual(access("monitors"));
2578
+ });
2579
+
2580
+ test("isScopedToStatusPages has the create and update access control of statusPages", () => {
2581
+ expect(access("isScopedToStatusPages").create).toEqual(
2582
+ access("statusPages").create,
2583
+ );
2584
+ expect(access("isScopedToStatusPages").update).toEqual(
2585
+ access("statusPages").update,
2586
+ );
2587
+ });
2588
+
2589
+ test.each(HOOK_WRITTEN_COLUMNS)(
2590
+ "every role that may edit statusPages may also write %s",
2591
+ (column: string) => {
2592
+ for (const role of access("statusPages").update) {
2593
+ expect(access(column).update).toContain(role);
2594
+ }
2595
+ },
2596
+ );
2597
+
2598
+ test("statusPagesNotifiedOnCreation follows the update access control of the created status", () => {
2599
+ expect(access("statusPagesNotifiedOnCreation").update).toEqual(
2600
+ access("subscriberNotificationStatusOnIncidentCreated").update,
2601
+ );
2602
+ });
2603
+
2604
+ test("the scope and the record are readable by every incident reader", () => {
2605
+ for (const column of [
2606
+ "statusPages",
2607
+ "isScopedToStatusPages",
2608
+ "statusPagesNotifiedOnCreation",
2609
+ ]) {
2610
+ expect(access(column).read.sort()).toEqual(access("title").read.sort());
2611
+ }
2612
+ });
2613
+
2614
+ test("the derived columns are computed and hidden from the API docs", () => {
2615
+ const incident: Incident = new Incident();
2616
+
2617
+ for (const column of [
2618
+ "isScopedToStatusPages",
2619
+ "statusPagesNotifiedOnCreation",
2620
+ ]) {
2621
+ expect(incident.getTableColumnMetadata(column).computed).toBe(true);
2622
+ expect(
2623
+ incident.getTableColumnMetadata(column).hideColumnInDocumentation,
2624
+ ).toBe(true);
2625
+ }
2626
+ });
2627
+
2628
+ test.each([
2629
+ Permission.IncidentMember,
2630
+ Permission.IncidentAdmin,
2631
+ Permission.ProjectMember,
2632
+ Permission.EditProjectIncident,
2633
+ ])(
2634
+ "setting, re-queueing and clearing the scope passes the column check for %s",
2635
+ async (role: Permission) => {
2636
+ const props: DatabaseCommonInteractionProps = makeProps([role]);
2637
+
2638
+ storedIncidents = [storedIncident({ statusPageIds: [PAGE_A] })];
2639
+
2640
+ const set: Record<string, unknown> = (
2641
+ await runBeforeUpdate(
2642
+ scopeUpdate({
2643
+ data: { statusPages: [PAGE_A, PAGE_B] },
2644
+ props,
2645
+ miscDataProps:
2646
+ IncidentScopeAddedPagesNotification.getMiscDataProps(),
2647
+ }),
2648
+ )
2649
+ ).data;
2650
+
2651
+ // Harness guard: the hook really injected the computed columns.
2652
+ expect(set["isScopedToStatusPages"]).toBe(true);
2653
+ expect(set["subscriberNotificationStatusOnIncidentCreated"]).toBe(
2654
+ StatusPageSubscriberNotificationStatus.Pending,
2655
+ );
2656
+
2657
+ const cleared: Record<string, unknown> = (
2658
+ await runBeforeUpdate(scopeUpdate({ data: { statusPages: [] }, props }))
2659
+ ).data;
2660
+
2661
+ expect(cleared["isScopedToStatusPages"]).toBe(false);
2662
+
2663
+ for (const data of [set, cleared]) {
2664
+ expect(() => {
2665
+ ColumnPermissions.checkDataColumnPermissions(
2666
+ Incident,
2667
+ data as unknown as Incident,
2668
+ props,
2669
+ DatabaseRequestType.Update,
2670
+ );
2671
+ }).not.toThrow();
2672
+ }
2673
+ },
2674
+ );
2675
+
2676
+ test("harness guard: a viewer's scope edit is refused by the same check", () => {
2677
+ expect(() => {
2678
+ ColumnPermissions.checkDataColumnPermissions(
2679
+ Incident,
2680
+ {
2681
+ statusPages: [statusPage(PAGE_A)],
2682
+ isScopedToStatusPages: true,
2683
+ } as unknown as Incident,
2684
+ makeProps([Permission.IncidentViewer]),
2685
+ DatabaseRequestType.Update,
2686
+ );
2687
+ }).toThrow();
2688
+ });
2689
+
2690
+ test("a create carrying the computed columns passes the check for an incident member", () => {
2691
+ const incident: Incident = new Incident();
2692
+ incident.title = "Site outage";
2693
+ incident.statusPages = [statusPage(PAGE_A)];
2694
+ incident.isScopedToStatusPages = true;
2695
+
2696
+ expect(() => {
2697
+ ColumnPermissions.checkDataColumnPermissions(
2698
+ Incident,
2699
+ incident,
2700
+ makeProps([Permission.IncidentMember]),
2701
+ DatabaseRequestType.Create,
2702
+ );
2703
+ }).not.toThrow();
2704
+ });
2705
+ });
2706
+
2707
+ /*
2708
+ * End to end through DatabaseService.updateOneById for a non-root incident
2709
+ * member: the real hook, then the real column check, then the write. Only the
2710
+ * database, the label/access-control query rewrite, the success hooks and the
2711
+ * workflow, realtime and audit side effects are stubbed.
2712
+ */
2713
+ describe("IncidentService.updateOneById: an incident member sets and clears the scope", () => {
2714
+ let saveMock: MockFunction;
2715
+ let updateMock: MockFunction;
2716
+
2717
+ beforeEach(() => {
2718
+ saveMock = getJestMockFunction();
2719
+ saveMock.mockImplementation((item: unknown) => {
2720
+ return Promise.resolve(item);
2721
+ });
2722
+ updateMock = getJestMockFunction();
2723
+ updateMock.mockResolvedValue({ affected: 1 });
2724
+
2725
+ jest
2726
+ .spyOn(
2727
+ IncidentService as unknown as {
2728
+ _findBy: (...args: Array<unknown>) => Promise<unknown>;
2729
+ },
2730
+ "_findBy",
2731
+ )
2732
+ .mockImplementation((() => {
2733
+ const row: Incident = new Incident();
2734
+ row._id = incidentId;
2735
+ row.projectId = projectId;
2736
+ return Promise.resolve([row]);
2737
+ }) as never);
2738
+
2739
+ jest
2740
+ .spyOn(
2741
+ IncidentService as unknown as {
2742
+ onUpdateSuccess: (...args: Array<unknown>) => Promise<unknown>;
2743
+ },
2744
+ "onUpdateSuccess",
2745
+ )
2746
+ .mockImplementation(((onUpdate: unknown): Promise<unknown> => {
2747
+ return Promise.resolve(onUpdate);
2748
+ }) as never);
2749
+
2750
+ jest
2751
+ .spyOn(
2752
+ IncidentService as unknown as { getRepository: () => unknown },
2753
+ "getRepository",
2754
+ )
2755
+ .mockReturnValue({
2756
+ update: updateMock,
2757
+ save: saveMock,
2758
+ } as never);
2759
+
2760
+ jest.spyOn(BasePermission, "checkPermissions").mockImplementation(((
2761
+ _modelType: unknown,
2762
+ query: unknown,
2763
+ ): Promise<unknown> => {
2764
+ return Promise.resolve({ query });
2765
+ }) as never);
2766
+
2767
+ jest
2768
+ .spyOn(IncidentService, "onTriggerWorkflow")
2769
+ .mockResolvedValue(undefined as never);
2770
+ jest
2771
+ .spyOn(IncidentService, "onTriggerRealtime")
2772
+ .mockResolvedValue(undefined as never);
2773
+ jest
2774
+ .spyOn(AuditLogService, "recordUpdate")
2775
+ .mockResolvedValue(undefined as never);
2776
+ });
2777
+
2778
+ /*
2779
+ * An update writing these status pages. Built apart from the call, so the
2780
+ * compiler does not expand the deep partial-entity type of the literal.
2781
+ */
2782
+ function scopeData(ids: Array<string>): UpdateBy<Incident>["data"] {
2783
+ return {
2784
+ statusPages: ids.map((id: string): StatusPage => {
2785
+ return statusPage(id);
2786
+ }),
2787
+ } as unknown as UpdateBy<Incident>["data"];
2788
+ }
2789
+
2790
+ function saved(): Record<string, unknown> {
2791
+ expect(saveMock).toHaveBeenCalledTimes(1);
2792
+ return saveMock.mock.calls[0]![0] as Record<string, unknown>;
2793
+ }
2794
+
2795
+ test("sets the scope: the list and the derived flag are written together", async () => {
2796
+ await IncidentService.updateOneById({
2797
+ id: new ObjectID(incidentId),
2798
+ data: scopeData([PAGE_A, PAGE_B]),
2799
+ props: makeProps([Permission.IncidentMember]),
2800
+ });
2801
+
2802
+ const written: Record<string, unknown> = saved();
2803
+
2804
+ expect(savedStatusPageIds(written)).toEqual([PAGE_A, PAGE_B]);
2805
+ expect(written["isScopedToStatusPages"]).toBe(true);
2806
+ expect(written["_id"]).toBe(incidentId);
2807
+ });
2808
+
2809
+ test("clears the scope", async () => {
2810
+ storedIncidents = [storedIncident({ statusPageIds: [PAGE_A] })];
2811
+
2812
+ await IncidentService.updateOneById({
2813
+ id: new ObjectID(incidentId),
2814
+ data: { statusPages: [] },
2815
+ props: makeProps([Permission.IncidentMember]),
2816
+ });
2817
+
2818
+ const written: Record<string, unknown> = saved();
2819
+
2820
+ expect(written["statusPages"]).toEqual([]);
2821
+ expect(written["isScopedToStatusPages"]).toBe(false);
2822
+ });
2823
+
2824
+ test("adds pages and re-queues the notification in the same write", async () => {
2825
+ storedIncidents = [storedIncident({ statusPageIds: [PAGE_A] })];
2826
+
2827
+ await IncidentService.updateOneById({
2828
+ id: new ObjectID(incidentId),
2829
+ data: scopeData([PAGE_A, PAGE_B]),
2830
+ miscDataProps: IncidentScopeAddedPagesNotification.getMiscDataProps(),
2831
+ props: makeProps([Permission.IncidentMember]),
2832
+ });
2833
+
2834
+ const written: Record<string, unknown> = saved();
2835
+
2836
+ expect(written["subscriberNotificationStatusOnIncidentCreated"]).toBe(
2837
+ StatusPageSubscriberNotificationStatus.Pending,
2838
+ );
2839
+ expect(written["subscriberNotificationStatusMessage"]).toBe(
2840
+ IncidentScopeAddedPagesNotification.queuedMessage,
2841
+ );
2842
+ });
2843
+
2844
+ test("a flag sent on its own writes nothing", async () => {
2845
+ await IncidentService.updateOneById({
2846
+ id: new ObjectID(incidentId),
2847
+ data: { isScopedToStatusPages: true },
2848
+ props: makeProps([Permission.IncidentMember]),
2849
+ });
2850
+
2851
+ expect(saveMock).not.toHaveBeenCalled();
2852
+ expect(updateMock).not.toHaveBeenCalled();
2853
+ });
2854
+
2855
+ test("an incident viewer cannot change the scope", async () => {
2856
+ await expect(
2857
+ IncidentService.updateOneById({
2858
+ id: new ObjectID(incidentId),
2859
+ data: scopeData([PAGE_A]),
2860
+ props: makeProps([Permission.IncidentViewer]),
2861
+ }),
2862
+ ).rejects.toThrow();
2863
+
2864
+ expect(saveMock).not.toHaveBeenCalled();
2865
+ });
2866
+ });
2867
+
2868
+ /*
2869
+ * The created feed item of a scoped incident names the pages it is limited
2870
+ * to, so the feed's history starts with the scope the incident was declared
2871
+ * with. The pages are read on their own, not in onCreateSuccess's select.
2872
+ */
2873
+ describe("IncidentService created feed item: the scope the incident was declared with", () => {
2874
+ let createFeedItem: MockFunction;
2875
+ let findOneById: MockFunction;
2876
+ let storedStatusPages: Array<StatusPage> = [];
2877
+
2878
+ beforeEach(() => {
2879
+ storedStatusPages = [];
2880
+
2881
+ createFeedItem = getJestMockFunction();
2882
+ createFeedItem.mockResolvedValue(undefined);
2883
+ jest
2884
+ .spyOn(IncidentFeedService, "createIncidentFeedItem")
2885
+ .mockImplementation(createFeedItem as never);
2886
+
2887
+ jest
2888
+ .spyOn(IncidentWorkspaceMessages, "getIncidentCreateMessageBlocks")
2889
+ .mockResolvedValue([] as never);
2890
+
2891
+ jest
2892
+ .spyOn(StatusPageService, "getStatusPageLinkInDashboard")
2893
+ .mockImplementation(((_projectId: ObjectID, statusPageId: ObjectID) => {
2894
+ return Promise.resolve(
2895
+ URL.fromString(
2896
+ `https://oneuptime.example/status-pages/${statusPageId.toString()}`,
2897
+ ),
2898
+ );
2899
+ }) as never);
2900
+
2901
+ findOneById = getJestMockFunction();
2902
+ findOneById.mockImplementation(() => {
2903
+ const incident: Incident = new Incident();
2904
+ incident._id = incidentId;
2905
+ incident.statusPages = storedStatusPages;
2906
+ return Promise.resolve(incident);
2907
+ });
2908
+ jest
2909
+ .spyOn(IncidentService, "findOneById")
2910
+ .mockImplementation(findOneById as never);
2911
+ });
2912
+
2913
+ function createdIncident(isScoped: boolean): Incident {
2914
+ const incident: Incident = new Incident();
2915
+ incident._id = incidentId;
2916
+ incident.projectId = projectId;
2917
+ incident.incidentNumber = 42;
2918
+ incident.incidentNumberWithPrefix = "INC-42";
2919
+ incident.title = "Shared uplink down";
2920
+ incident.isScopedToStatusPages = isScoped;
2921
+ return incident;
2922
+ }
2923
+
2924
+ async function createdFeedMarkdown(incident: Incident): Promise<string> {
2925
+ await (
2926
+ IncidentService as unknown as {
2927
+ createIncidentFeedAsync: (incident: Incident) => Promise<void>;
2928
+ }
2929
+ ).createIncidentFeedAsync(incident);
2930
+
2931
+ expect(createFeedItem).toHaveBeenCalledTimes(1);
2932
+
2933
+ return createFeedItem.mock.calls[0]![0].feedInfoInMarkdown as string;
2934
+ }
2935
+
2936
+ test("lists the pages a scoped incident is limited to", async () => {
2937
+ storedStatusPages = [
2938
+ statusPage(PAGE_A, "Site 03"),
2939
+ statusPage(PAGE_B, "Site 07"),
2940
+ ];
2941
+
2942
+ const markdown: string = await createdFeedMarkdown(createdIncident(true));
2943
+
2944
+ expect(markdown).toContain(
2945
+ `📣 **Limited to Status Pages**:\n- [Site 03](https://oneuptime.example/status-pages/${PAGE_A})\n- [Site 07](https://oneuptime.example/status-pages/${PAGE_B})\n`,
2946
+ );
2947
+
2948
+ const read: {
2949
+ select: Record<string, unknown>;
2950
+ props: DatabaseCommonInteractionProps;
2951
+ } = findOneById.mock.calls[0]![0];
2952
+
2953
+ expect(read.select).toEqual({ statusPages: { _id: true, name: true } });
2954
+ expect(read.props).toEqual({ isRoot: true });
2955
+ });
2956
+
2957
+ test("says so when every page it was limited to has been deleted since", async () => {
2958
+ storedStatusPages = [];
2959
+
2960
+ expect(await createdFeedMarkdown(createdIncident(true))).toContain(
2961
+ "not shown on any status page",
2962
+ );
2963
+ });
2964
+
2965
+ test("an unscoped incident gets no scope section, and nothing is read for it", async () => {
2966
+ const markdown: string = await createdFeedMarkdown(createdIncident(false));
2967
+
2968
+ expect(markdown).not.toContain("Limited to Status Pages");
2969
+ expect(findOneById).not.toHaveBeenCalled();
2970
+ });
2971
+ });