@oneuptime/common 14.0.8 → 14.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/AutoRemediationSuggestion.ts +60 -0
- package/Models/DatabaseModels/CephCluster.ts +213 -0
- package/Models/DatabaseModels/DatabaseServer.ts +213 -0
- package/Models/DatabaseModels/DockerHost.ts +213 -0
- package/Models/DatabaseModels/DockerSwarmCluster.ts +213 -0
- package/Models/DatabaseModels/GlobalConfig.ts +1 -1
- package/Models/DatabaseModels/Host.ts +213 -0
- package/Models/DatabaseModels/Index.ts +4 -0
- package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
- package/Models/DatabaseModels/PodmanHost.ts +213 -0
- package/Models/DatabaseModels/Project.ts +55 -5
- package/Models/DatabaseModels/ProxmoxCluster.ts +213 -0
- package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
- package/Models/DatabaseModels/ResourceAiAgent.ts +419 -0
- package/Models/DatabaseModels/RunnerJob.ts +219 -0
- package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
- package/Models/DatabaseModels/VMwareVCenter.ts +213 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
- package/Server/API/AIChatAPI.ts +6 -3
- package/Server/API/AIInvestigationAPI.ts +17 -10
- package/Server/API/AutoRemediationAPI.ts +598 -25
- package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
- package/Server/API/MicrosoftTeamsAPI.ts +93 -14
- package/Server/API/ProxmoxResourceAPI.ts +244 -14
- package/Server/API/ResourceAiAccessAPI.ts +1669 -0
- package/Server/Infrastructure/GlobalCache.ts +32 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.ts +423 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Infrastructure/Semaphore.ts +22 -0
- package/Server/Middleware/TelemetryIngest.ts +51 -18
- package/Server/Services/AIService.ts +70 -1
- package/Server/Services/AlertService.ts +352 -11
- package/Server/Services/AlertSeverityService.ts +65 -15
- package/Server/Services/AnalyticsDatabaseService.ts +45 -1
- package/Server/Services/AutoRemediationRuleEngineService.ts +940 -112
- package/Server/Services/AutoRemediationRuleService.ts +1 -1
- package/Server/Services/CephClusterService.ts +109 -1
- package/Server/Services/DatabaseServerService.ts +116 -1
- package/Server/Services/DockerHostService.ts +109 -1
- package/Server/Services/DockerSwarmClusterService.ts +113 -1
- package/Server/Services/HostService.ts +126 -4
- package/Server/Services/IncidentService.ts +280 -104
- package/Server/Services/IncidentSeverityService.ts +66 -15
- package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
- package/Server/Services/KubernetesAiAgentService.ts +1634 -0
- package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
- package/Server/Services/KubernetesClusterService.ts +129 -21
- package/Server/Services/MetricRecordingRuleService.ts +47 -0
- package/Server/Services/PodmanHostService.ts +109 -1
- package/Server/Services/ProjectService.ts +32 -0
- package/Server/Services/ProxmoxClusterService.ts +110 -1
- package/Server/Services/ProxmoxResourceService.ts +322 -5
- package/Server/Services/ResourceAiAccessService.ts +1439 -0
- package/Server/Services/ResourceAiAgentJobService.ts +202 -0
- package/Server/Services/ResourceAiAgentService.ts +2432 -0
- package/Server/Services/RunnerJobService.ts +852 -108
- package/Server/Services/RunnerService.ts +1 -1
- package/Server/Services/ScheduledMaintenanceService.ts +737 -1
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
- package/Server/Services/TeamComplianceSettingService.ts +1125 -9
- package/Server/Services/TelemetryUsageBillingService.ts +28 -0
- package/Server/Services/TraceRecordingRuleService.ts +33 -0
- package/Server/Services/VMwareVCenterService.ts +110 -1
- package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
- package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
- package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
- package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +1475 -51
- package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +1000 -43
- package/Server/Utils/AI/Remediation/RemediationPlanRunner.ts +25 -1
- package/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.ts +577 -0
- package/Server/Utils/AI/ResourceAccess/ResourceAccessContext.ts +271 -0
- package/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.ts +45 -0
- package/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.ts +964 -0
- package/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.ts +333 -0
- package/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.ts +873 -0
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +120 -7
- package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +72 -5
- package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +72 -5
- package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
- package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
- package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
- package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +402 -14
- package/Server/Utils/AutoRemediation/RemediationVerifier.ts +35 -2
- package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +9 -1
- package/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.ts +920 -0
- package/Server/Utils/SessionReplay/SessionReplayUsage.ts +84 -6
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
- package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
- package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
- package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
- package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +35 -15
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +33 -15
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +21 -1
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +298 -224
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +35 -15
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +389 -175
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +1073 -213
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.ts +163 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.ts +396 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.ts +418 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.ts +152 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.ts +251 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.ts +214 -0
- package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
- package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
- package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
- package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
- package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
- package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
- package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
- package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
- package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
- package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
- package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
- package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
- package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
- package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
- package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
- package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
- package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
- package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +184 -19
- package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
- package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
- package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
- package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
- package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
- package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
- package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
- package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
- package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
- package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
- package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
- package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
- package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
- package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
- package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +18 -6
- package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
- package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
- package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
- package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
- package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
- package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
- package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
- package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
- package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
- package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
- package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
- package/Tests/App/Dashboard/EventNotes.test.tsx +2 -2
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +587 -13
- package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
- package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
- package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
- package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
- package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
- package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
- package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
- package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
- package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
- package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
- package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
- package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
- package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
- package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
- package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
- package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
- package/Tests/App/Dashboard/InvestigationInfrastructureTools.test.tsx +506 -0
- package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
- package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
- package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
- package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
- package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
- package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
- package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
- package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
- package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
- package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
- package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
- package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
- package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
- package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
- package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
- package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
- package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
- package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
- package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
- package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
- package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
- package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
- package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
- package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
- package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
- package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
- package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
- package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
- package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
- package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
- package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
- package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
- package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
- package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
- package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
- package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
- package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
- package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
- package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
- package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
- package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
- package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
- package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
- package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
- package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
- package/Tests/App/Dashboard/RemediationSuggestionCardDescription.test.tsx +209 -0
- package/Tests/App/Dashboard/RemediationSuggestionCardResource.test.tsx +317 -0
- package/Tests/App/Dashboard/ResourceAiAccessSettingsUtil.test.ts +921 -0
- package/Tests/App/Dashboard/ResourceAiAgentInstall.test.ts +860 -0
- package/Tests/App/Dashboard/ResourceAiAgentPage.test.tsx +1721 -0
- package/Tests/App/Dashboard/ResourceAiAgentStatus.test.ts +1002 -0
- package/Tests/App/Dashboard/ResourceAiInsightsPage.test.tsx +931 -0
- package/Tests/App/Dashboard/ResourceAiNavigation.test.tsx +575 -0
- package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
- package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
- package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +38 -7
- package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
- package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
- package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
- package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
- package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
- package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
- package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
- package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
- package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
- package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
- package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
- package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
- package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
- package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
- package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
- package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
- package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
- package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
- package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
- package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
- package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
- package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
- package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
- package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
- package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
- package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
- package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
- package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
- package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
- package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
- package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
- package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
- package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +33 -1
- package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
- package/Tests/Models/DatabaseModels/ResourceAiAccessColumns.test.ts +738 -0
- package/Tests/Models/DatabaseModels/ResourceAiAgentModel.test.ts +842 -0
- package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
- package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
- package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
- package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
- package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
- package/Tests/Server/API/AutoRemediationApproveResourceRound.test.ts +835 -0
- package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
- package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
- package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
- package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
- package/Tests/Server/API/ResourceAiAccessAPI.test.ts +2730 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
- package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +60 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
- package/Tests/Server/Infrastructure/Postgres/AddResourceAiAgentsMigration.test.ts +659 -0
- package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
- package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +31 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +119 -4
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +196 -2
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +123 -2
- package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
- package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
- package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
- package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
- package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
- package/Tests/Server/Services/AutoRemediationResourceRuleEngine.test.ts +1279 -0
- package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
- package/Tests/Server/Services/DatabaseServerService.test.ts +55 -0
- package/Tests/Server/Services/GroupTelemetryUsageExcludeNames.test.ts +299 -0
- package/Tests/Server/Services/HostServiceFindOrCreateMemo.test.ts +44 -0
- package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
- package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
- package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
- package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
- package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
- package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
- package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
- package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
- package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
- package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +45 -6
- package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
- package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
- package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
- package/Tests/Server/Services/RecordingRuleReservedMetricName.test.ts +171 -0
- package/Tests/Server/Services/ResourceAiAccessChangeAuthorization.test.ts +256 -0
- package/Tests/Server/Services/ResourceAiAccessService.test.ts +1373 -0
- package/Tests/Server/Services/ResourceAiAgentJobService.test.ts +375 -0
- package/Tests/Server/Services/ResourceAiAgentServiceHelpers.test.ts +1114 -0
- package/Tests/Server/Services/ResourceAiAgentServiceLifecycle.test.ts +1050 -0
- package/Tests/Server/Services/ResourceAiAgentServiceRegister.test.ts +2251 -0
- package/Tests/Server/Services/ResourceAiSettingsCreate.test.ts +373 -0
- package/Tests/Server/Services/ResourceAiSettingsPermission.test.ts +1042 -0
- package/Tests/Server/Services/ResourceServiceDeleteCleansUpAi.test.ts +319 -0
- package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +153 -15
- package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
- package/Tests/Server/Services/RunnerJobEnqueueResourceCommand.test.ts +986 -0
- package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
- package/Tests/Server/Services/RunnerJobResourceCommandLane.test.ts +211 -0
- package/Tests/Server/Services/RunnerJobResourceCommandTimeoutAndRedaction.test.ts +352 -0
- package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
- package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
- package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
- package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
- package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
- package/Tests/Server/Services/TelemetryUsageBillingSloExclusion.test.ts +218 -0
- package/Tests/Server/TestingUtils/Services/FakeRunnerJobCount.ts +145 -0
- package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
- package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
- package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
- package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
- package/Tests/Server/Utils/AI/InvestigationInfrastructureAccessWiring.test.ts +453 -0
- package/Tests/Server/Utils/AI/InvestigationInfrastructureReport.test.ts +721 -0
- package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
- package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
- package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +94 -1
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsResource.test.ts +1530 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
- package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerResourceMode.test.ts +1249 -0
- package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationPlanRunner.test.ts +117 -0
- package/Tests/Server/Utils/AI/RemediationResourceCopyParity.test.ts +463 -0
- package/Tests/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.test.ts +722 -0
- package/Tests/Server/Utils/AI/ResourceAccess/ResourceAccessContext.test.ts +266 -0
- package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.test.ts +1220 -0
- package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.test.ts +569 -0
- package/Tests/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.test.ts +830 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorResource.test.ts +717 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/RemediationVerifierResourceFollowUp.test.ts +338 -0
- package/Tests/Server/Utils/Monitor/Criteria/SessionReplayBudgetTemplateCriteria.test.ts +422 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.test.ts +1647 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +143 -1
- package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
- package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
- package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
- package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +75 -3
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsAccountNotLinked.test.ts +1093 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.test.ts +1049 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.test.ts +1676 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCards.test.ts +2884 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.test.ts +2490 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmit.test.ts +2293 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmitServerTimezone.test.ts +386 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.test.ts +1477 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.test.ts +1967 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsStripHtmlTags.test.ts +92 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsSubmittedFormRemoval.test.ts +1819 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.test.ts +1033 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezoneServerZone.test.ts +610 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsBotMessageHandling.test.ts +2610 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsCreateCommandsEndToEnd.test.ts +4672 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
- package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +190 -65
- package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +10 -4
- package/Tests/Types/AutoRemediation/AiRemediationCommandPlanResourceCommand.test.ts +305 -0
- package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
- package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +277 -13
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +113 -0
- package/Tests/Types/Monitor/RumAlertTemplates.test.ts +773 -2
- package/Tests/Types/ResourceAiAgent/AiResourceType.test.ts +419 -0
- package/Tests/Types/ResourceAiAgent/ResourceAiAccess.test.ts +517 -0
- package/Tests/Types/Runbook/RunbookStepType.test.ts +147 -8
- package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
- package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
- package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
- package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
- package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
- package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
- package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
- package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
- package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
- package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
- package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
- package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
- package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
- package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
- package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
- package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
- package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
- package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
- package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
- package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
- package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
- package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
- package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
- package/Tests/UI/Components/Input.test.tsx +80 -1
- package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
- package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
- package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
- package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
- package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
- package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
- package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
- package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
- package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
- package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
- package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
- package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
- package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
- package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
- package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
- package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
- package/Tests/UI/Components/Toggle.test.tsx +66 -0
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
- package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
- package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +769 -1
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
- package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
- package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
- package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
- package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
- package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
- package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
- package/Tests/Utils/AiRemediation/Resource/CephCommandPolicy.test.ts +1653 -0
- package/Tests/Utils/AiRemediation/Resource/CephOutputRedaction.test.ts +204 -0
- package/Tests/Utils/AiRemediation/Resource/DatabaseCommandPolicy.test.ts +1077 -0
- package/Tests/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.test.ts +558 -0
- package/Tests/Utils/AiRemediation/Resource/DatabaseQueryRedactor.test.ts +834 -0
- package/Tests/Utils/AiRemediation/Resource/DockerCliGrammar.test.ts +747 -0
- package/Tests/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.test.ts +1259 -0
- package/Tests/Utils/AiRemediation/Resource/DockerOutputRedaction.test.ts +386 -0
- package/Tests/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.test.ts +782 -0
- package/Tests/Utils/AiRemediation/Resource/GovcCommandPolicy.test.ts +2259 -0
- package/Tests/Utils/AiRemediation/Resource/HostCommandPolicy.test.ts +2019 -0
- package/Tests/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.test.ts +2497 -0
- package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicy.test.ts +1305 -0
- package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.test.ts +585 -0
- package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactor.test.ts +334 -0
- package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactorCopyParity.test.ts +98 -0
- package/Tests/Utils/AiRemediation/Resource/ResourcePolicyImportClosure.test.ts +391 -0
- package/Tests/Utils/AiRemediation/ResourceAiAgentPolicyCopyParity.test.ts +497 -0
- package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
- package/Tests/Utils/SessionReplay/SessionReplayBudgetMetricType.test.ts +383 -0
- package/Types/AI/AIRunType.ts +9 -4
- package/Types/AI/InvestigationNotStartedReason.ts +6 -0
- package/Types/AI/ResourceAiAccessApi.ts +140 -0
- package/Types/AI/ResourceAiAccessPermissions.ts +126 -0
- package/Types/AutoRemediation/AiRemediationCommandPlan.ts +197 -18
- package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
- package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
- package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
- package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
- package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
- package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
- package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +55 -22
- package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +35 -3
- package/Types/Monitor/RumAlertTemplates.ts +351 -4
- package/Types/ResourceAiAgent/AiResourceType.ts +310 -0
- package/Types/ResourceAiAgent/ResourceAiAccess.ts +574 -0
- package/Types/Rum/SessionReplayBudgetMetricType.ts +47 -0
- package/Types/Runbook/RunbookStepType.ts +29 -0
- package/Types/Team/ComplianceNotificationChannel.ts +22 -0
- package/Types/Team/ComplianceRule.ts +434 -0
- package/Types/Team/ComplianceRuleType.ts +21 -0
- package/Types/Team/TeamComplianceStatus.ts +81 -0
- package/Types/Telemetry/TelemetryIngestSurface.ts +40 -0
- package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
- package/UI/Components/ActionButton/RowActions.tsx +238 -0
- package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
- package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
- package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
- package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
- package/UI/Components/Charts/Line/LineChart.tsx +32 -2
- package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
- package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
- package/UI/Components/Charts/Utils/XAxis.ts +75 -0
- package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
- package/UI/Components/Forms/Fields/FormField.tsx +10 -0
- package/UI/Components/Input/Input.tsx +90 -57
- package/UI/Components/List/ListRow.tsx +8 -74
- package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +12 -1
- package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
- package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
- package/UI/Components/OrderedStatesList/Item.tsx +7 -72
- package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
- package/UI/Components/Table/TableBody.tsx +8 -2
- package/UI/Components/Table/TableRow.tsx +20 -140
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
- package/UI/Components/TelemetryViewer/types.ts +2 -0
- package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
- package/UI/Components/Toggle/Toggle.tsx +26 -0
- package/UI/Components/Tooltip/Tooltip.tsx +8 -1
- package/UI/Styles/Theme.css +14 -2
- package/Utils/AI/InvestigationReport.ts +121 -15
- package/Utils/AiRemediation/CommandPolicy.ts +1 -1
- package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
- package/Utils/AiRemediation/Resource/CephCommandPolicy.ts +1936 -0
- package/Utils/AiRemediation/Resource/DatabaseCommandPolicy.ts +166 -0
- package/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.ts +1532 -0
- package/Utils/AiRemediation/Resource/DatabaseQueryRedactor.ts +1394 -0
- package/Utils/AiRemediation/Resource/DockerCliGrammar.ts +1839 -0
- package/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.ts +490 -0
- package/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.ts +687 -0
- package/Utils/AiRemediation/Resource/GovcCommandPolicy.ts +2096 -0
- package/Utils/AiRemediation/Resource/HostCommandPolicy.ts +2982 -0
- package/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.ts +1679 -0
- package/Utils/AiRemediation/Resource/ResourceCommandPolicy.ts +851 -0
- package/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.ts +593 -0
- package/Utils/AiRemediation/Resource/ResourceOutputRedactor.ts +2812 -0
- package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
- package/Utils/SessionReplay/SessionReplayBudgetMetricType.ts +184 -0
- package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
- package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CephCluster.js +219 -0
- package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DatabaseServer.js +219 -0
- package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DockerHost.js +219 -0
- package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +219 -0
- package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Host.js +219 -0
- package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
- package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/PodmanHost.js +219 -0
- package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +56 -5
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +219 -0
- package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
- package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ResourceAiAgent.js +439 -0
- package/build/dist/Models/DatabaseModels/ResourceAiAgent.js.map +1 -0
- package/build/dist/Models/DatabaseModels/RunnerJob.js +219 -0
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
- package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/VMwareVCenter.js +219 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +6 -1
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/AutoRemediationAPI.js +374 -24
- package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
- package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
- package/build/dist/Server/API/ResourceAiAccessAPI.js +1119 -0
- package/build/dist/Server/API/ResourceAiAccessAPI.js.map +1 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js +168 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Semaphore.js +6 -0
- package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +41 -19
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/AIService.js +59 -1
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +248 -13
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/AlertSeverityService.js +51 -15
- package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +26 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +601 -3
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +104 -0
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseServerService.js +99 -0
- package/build/dist/Server/Services/DatabaseServerService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +104 -0
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +104 -0
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +114 -2
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +199 -72
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
- package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
- package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
- package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
- package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/MetricRecordingRuleService.js +46 -0
- package/build/dist/Server/Services/MetricRecordingRuleService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +104 -0
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +22 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +104 -0
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/ResourceAiAccessService.js +1028 -0
- package/build/dist/Server/Services/ResourceAiAccessService.js.map +1 -0
- package/build/dist/Server/Services/ResourceAiAgentJobService.js +173 -0
- package/build/dist/Server/Services/ResourceAiAgentJobService.js.map +1 -0
- package/build/dist/Server/Services/ResourceAiAgentService.js +1687 -0
- package/build/dist/Server/Services/ResourceAiAgentService.js.map +1 -0
- package/build/dist/Server/Services/RunnerJobService.js +522 -97
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Services/RunnerService.js +1 -1
- package/build/dist/Server/Services/RunnerService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +535 -3
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
- package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryUsageBillingService.js +26 -0
- package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
- package/build/dist/Server/Services/TraceRecordingRuleService.js +37 -0
- package/build/dist/Server/Services/TraceRecordingRuleService.js.map +1 -1
- package/build/dist/Server/Services/VMwareVCenterService.js +104 -0
- package/build/dist/Server/Services/VMwareVCenterService.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +1056 -43
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +683 -42
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js +21 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js +330 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js +160 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js +33 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js +640 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js +226 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js +600 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +89 -9
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +55 -4
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +55 -4
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
- package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
- package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +294 -15
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
- package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +29 -1
- package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +9 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js +627 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js +60 -5
- package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +19 -15
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +19 -15
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +17 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +183 -209
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +19 -15
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +233 -167
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +768 -152
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js +129 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js +266 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js +258 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js +104 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js +183 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js +123 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js.map +1 -0
- package/build/dist/Types/AI/AIRunType.js +9 -4
- package/build/dist/Types/AI/AIRunType.js.map +1 -1
- package/build/dist/Types/AI/ResourceAiAccessApi.js +30 -0
- package/build/dist/Types/AI/ResourceAiAccessApi.js.map +1 -0
- package/build/dist/Types/AI/ResourceAiAccessPermissions.js +98 -0
- package/build/dist/Types/AI/ResourceAiAccessPermissions.js.map +1 -0
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +126 -45
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +47 -21
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +5 -0
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
- package/build/dist/Types/Monitor/RumAlertTemplates.js +199 -3
- package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
- package/build/dist/Types/ResourceAiAgent/AiResourceType.js +242 -0
- package/build/dist/Types/ResourceAiAgent/AiResourceType.js.map +1 -0
- package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js +275 -0
- package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js +45 -0
- package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js.map +1 -0
- package/build/dist/Types/Runbook/RunbookStepType.js +27 -0
- package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
- package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
- package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
- package/build/dist/Types/Team/ComplianceRule.js +304 -0
- package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
- package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
- package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
- package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
- package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +36 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
- package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
- package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
- package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
- package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
- package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
- package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
- package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +69 -54
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/List/ListRow.js +4 -39
- package/build/dist/UI/Components/List/ListRow.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +10 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
- package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
- package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
- package/build/dist/UI/Components/Table/TableBody.js +6 -2
- package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +12 -75
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
- package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
- package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
- package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
- package/build/dist/Utils/AI/InvestigationReport.js +71 -11
- package/build/dist/Utils/AI/InvestigationReport.js.map +1 -1
- package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
- package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
- package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js +1462 -0
- package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js +122 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js +1139 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js +1060 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js +1301 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js +370 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js +498 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js +1565 -0
- package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js +2206 -0
- package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js +1129 -0
- package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js +565 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js +408 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js +1910 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js.map +1 -0
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
- package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js +160 -0
- package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2237 @@
|
|
|
1
|
+
import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
|
|
2
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
3
|
+
import Entities from "../../../Models/DatabaseModels/Index";
|
|
4
|
+
import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
|
|
5
|
+
import TeamComplianceSetting from "../../../Models/DatabaseModels/TeamComplianceSetting";
|
|
6
|
+
import EnterpriseEdition from "../../../Server/Enterprise/EnterpriseEdition";
|
|
7
|
+
import PostgresAppInstance from "../../../Server/Infrastructure/PostgresDatabase";
|
|
8
|
+
import AlertSeverityService from "../../../Server/Services/AlertSeverityService";
|
|
9
|
+
import IncidentSeverityService from "../../../Server/Services/IncidentSeverityService";
|
|
10
|
+
import TeamComplianceSettingService, {
|
|
11
|
+
DeletedSeverity,
|
|
12
|
+
DUPLICATE_COMPLIANCE_RULE_MESSAGE,
|
|
13
|
+
SEVERITIES_DELETED_ENABLE_MESSAGE,
|
|
14
|
+
} from "../../../Server/Services/TeamComplianceSettingService";
|
|
15
|
+
import UpdateBy from "../../../Server/Types/Database/UpdateBy";
|
|
16
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
17
|
+
import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
|
|
18
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
19
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
20
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
21
|
+
import Permission, {
|
|
22
|
+
UserTenantAccessPermission,
|
|
23
|
+
} from "../../../Types/Permission";
|
|
24
|
+
import ComplianceNotificationChannel from "../../../Types/Team/ComplianceNotificationChannel";
|
|
25
|
+
import { ComplianceSeverityKind } from "../../../Types/Team/ComplianceRule";
|
|
26
|
+
import ComplianceRuleType from "../../../Types/Team/ComplianceRuleType";
|
|
27
|
+
import UserType from "../../../Types/UserType";
|
|
28
|
+
import { DataSource, Logger } from "typeorm";
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* Team compliance rules scoped by severity and channel
|
|
32
|
+
* (TeamComplianceSetting.notificationChannel / incidentSeverities /
|
|
33
|
+
* alertSeverities) against a migrated Postgres.
|
|
34
|
+
*
|
|
35
|
+
* Opt in with RUN_POSTGRES_TEAM_COMPLIANCE_TESTS=true against a database the
|
|
36
|
+
* registered migrations (1796000000000-AddTeamComplianceRuleScope included)
|
|
37
|
+
* have been applied to, e.g. from packages/Common:
|
|
38
|
+
*
|
|
39
|
+
* RUN_POSTGRES_TEAM_COMPLIANCE_TESTS=true \
|
|
40
|
+
* TEAM_COMPLIANCE_TEST_DATABASE_HOST=127.0.0.1 \
|
|
41
|
+
* TEAM_COMPLIANCE_TEST_DATABASE_PORT=5400 \
|
|
42
|
+
* DATABASE_PASSWORD=... node node_modules/.bin/jest --runInBand \
|
|
43
|
+
* Tests/Server/Services/TeamComplianceSettingPostgres.test.ts --forceExit
|
|
44
|
+
*
|
|
45
|
+
* Without the flag the suite is skipped, so the Common test job - whose
|
|
46
|
+
* Postgres is not migrated - never runs it.
|
|
47
|
+
*
|
|
48
|
+
* Why a real Postgres: the unit tests of TeamComplianceSettingService mock
|
|
49
|
+
* every read, so they cannot show that the two ManyToMany join tables the
|
|
50
|
+
* migration created are the ones the model writes, that severity ids in each
|
|
51
|
+
* shape a caller sends (id strings, `{_id}` JSON, models) land as join rows,
|
|
52
|
+
* that the service's validation reads - an IN over uuid ids, a relation
|
|
53
|
+
* select - are SQL Postgres accepts, that an update replaces (or clears) the
|
|
54
|
+
* join rows, or that the ON DELETE CASCADE of both foreign keys holds.
|
|
55
|
+
*
|
|
56
|
+
* Two halves, as in IncidentStatusPageScopePostgres.test.ts:
|
|
57
|
+
*
|
|
58
|
+
* - the migrated public tables are inspected as they are: the new column, the
|
|
59
|
+
* foreign keys of the two join tables, and the dropped unique index;
|
|
60
|
+
* - behaviour runs in a uniquely named schema holding structure-only clones
|
|
61
|
+
* (LIKE ... INCLUDING ALL) of the tables involved. The join tables' foreign
|
|
62
|
+
* keys are copied from the migrated tables' own definitions, so a cascade
|
|
63
|
+
* observed here is the cascade the migration declared. No row is ever
|
|
64
|
+
* written outside that schema, and the schema is dropped afterwards.
|
|
65
|
+
*
|
|
66
|
+
* The production TeamComplianceSettingService runs unchanged: its hooks, the
|
|
67
|
+
* permission layer (as root, and as signed-in users), and the relation save -
|
|
68
|
+
* and so do IncidentSeverityService and AlertSeverityService, whose deletes
|
|
69
|
+
* pause a rule left with no severity instead of leaving it to widen to every
|
|
70
|
+
* severity - and mark it, so it cannot be turned back on as it is or be
|
|
71
|
+
* mistaken for a duplicate. Only realtime and workflow triggers are stubbed,
|
|
72
|
+
* and - for the signed-in half - the enterprise licence check, which reads a
|
|
73
|
+
* licence no test database has. Every statement Postgres rejects is recorded through the
|
|
74
|
+
* DataSource's logger and fails the test, even when a caller catches it.
|
|
75
|
+
*/
|
|
76
|
+
const describePostgres: typeof describe =
|
|
77
|
+
process.env["RUN_POSTGRES_TEAM_COMPLIANCE_TESTS"] === "true"
|
|
78
|
+
? describe
|
|
79
|
+
: describe.skip;
|
|
80
|
+
|
|
81
|
+
const TABLES: Array<string> = [
|
|
82
|
+
"Project",
|
|
83
|
+
"Team",
|
|
84
|
+
"IncidentSeverity",
|
|
85
|
+
"AlertSeverity",
|
|
86
|
+
"TeamComplianceSetting",
|
|
87
|
+
"TeamComplianceSettingIncidentSeverity",
|
|
88
|
+
"TeamComplianceSettingAlertSeverity",
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const JOIN_TABLES: Array<string> = [
|
|
92
|
+
"TeamComplianceSettingIncidentSeverity",
|
|
93
|
+
"TeamComplianceSettingAlertSeverity",
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const INCIDENT_JOIN_TABLE: string = "TeamComplianceSettingIncidentSeverity";
|
|
97
|
+
const ALERT_JOIN_TABLE: string = "TeamComplianceSettingAlertSeverity";
|
|
98
|
+
|
|
99
|
+
const FOREIGN_SEVERITY_MESSAGE: string =
|
|
100
|
+
"One or more of the selected incident severities do not exist in this project.";
|
|
101
|
+
const FOREIGN_ALERT_SEVERITY_MESSAGE: string =
|
|
102
|
+
"One or more of the selected alert severities do not exist in this project.";
|
|
103
|
+
|
|
104
|
+
// What a severity delete writes into the options of a rule it emptied.
|
|
105
|
+
const MARKED: JSONObject = { severitiesDeleted: true };
|
|
106
|
+
|
|
107
|
+
interface ForeignKeyRow {
|
|
108
|
+
table: string;
|
|
109
|
+
name: string;
|
|
110
|
+
column: string;
|
|
111
|
+
referencedTable: string;
|
|
112
|
+
onDelete: string;
|
|
113
|
+
definition: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
type SqlRow = Record<string, unknown>;
|
|
117
|
+
|
|
118
|
+
// A severity list in any shape a caller sends it.
|
|
119
|
+
type SeverityList = Array<
|
|
120
|
+
string | JSONObject | IncidentSeverity | AlertSeverity
|
|
121
|
+
>;
|
|
122
|
+
|
|
123
|
+
interface RuleInput {
|
|
124
|
+
ruleType: ComplianceRuleType;
|
|
125
|
+
notificationChannel?: ComplianceNotificationChannel | null | undefined;
|
|
126
|
+
incidentSeverities?: SeverityList | undefined;
|
|
127
|
+
alertSeverities?: SeverityList | undefined;
|
|
128
|
+
enabled?: boolean | undefined;
|
|
129
|
+
teamId?: ObjectID | undefined;
|
|
130
|
+
projectId?: ObjectID | undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// A rule row as Postgres stores it, with its join rows.
|
|
134
|
+
interface StoredRule {
|
|
135
|
+
ruleType: string;
|
|
136
|
+
notificationChannel: string | null;
|
|
137
|
+
enabled: boolean;
|
|
138
|
+
incidentSeverityIds: Array<string>;
|
|
139
|
+
alertSeverityIds: Array<string>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface ReadSeverity {
|
|
143
|
+
id: string;
|
|
144
|
+
name: string | undefined;
|
|
145
|
+
color: string | undefined;
|
|
146
|
+
order: number | undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/*
|
|
150
|
+
* Records every statement Postgres rejected. PostgresQueryRunner hands each
|
|
151
|
+
* failure to the logger before it throws, whatever the caller then does with
|
|
152
|
+
* the error.
|
|
153
|
+
*/
|
|
154
|
+
class FailedQueryRecorder implements Logger {
|
|
155
|
+
public failures: Array<string> = [];
|
|
156
|
+
|
|
157
|
+
public logQuery(): void {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public logQueryError(error: string | Error, query: string): void {
|
|
162
|
+
this.failures.push(
|
|
163
|
+
`${error instanceof Error ? error.message : error} in: ${query}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public logQuerySlow(): void {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public logSchemaBuild(): void {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public logMigration(): void {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public log(): void {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const ROOT: DatabaseCommonInteractionProps = { isRoot: true };
|
|
185
|
+
|
|
186
|
+
describePostgres(
|
|
187
|
+
"Team compliance rule scope (severities + channel) against a migrated Postgres",
|
|
188
|
+
() => {
|
|
189
|
+
const schema: string = `team_compliance_${ObjectID.generate()
|
|
190
|
+
.toString()
|
|
191
|
+
.replace(/-/g, "")}`;
|
|
192
|
+
|
|
193
|
+
const failedQueries: FailedQueryRecorder = new FailedQueryRecorder();
|
|
194
|
+
|
|
195
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
196
|
+
const otherProjectId: ObjectID = ObjectID.generate();
|
|
197
|
+
const teamId: ObjectID = ObjectID.generate();
|
|
198
|
+
const otherTeamId: ObjectID = ObjectID.generate();
|
|
199
|
+
const otherProjectTeamId: ObjectID = ObjectID.generate();
|
|
200
|
+
|
|
201
|
+
// Incident severities of the project, most severe first.
|
|
202
|
+
const critical: ObjectID = ObjectID.generate();
|
|
203
|
+
const major: ObjectID = ObjectID.generate();
|
|
204
|
+
const minor: ObjectID = ObjectID.generate();
|
|
205
|
+
|
|
206
|
+
const criticalAlert: ObjectID = ObjectID.generate();
|
|
207
|
+
const warningAlert: ObjectID = ObjectID.generate();
|
|
208
|
+
|
|
209
|
+
// Severities of ANOTHER project.
|
|
210
|
+
const otherProjectCritical: ObjectID = ObjectID.generate();
|
|
211
|
+
const otherProjectCriticalAlert: ObjectID = ObjectID.generate();
|
|
212
|
+
|
|
213
|
+
let database: DataSource;
|
|
214
|
+
let migratedForeignKeys: Array<ForeignKeyRow> = [];
|
|
215
|
+
|
|
216
|
+
beforeAll(async () => {
|
|
217
|
+
database = new DataSource({
|
|
218
|
+
type: "postgres",
|
|
219
|
+
host: process.env["TEAM_COMPLIANCE_TEST_DATABASE_HOST"] || "localhost",
|
|
220
|
+
port: Number(
|
|
221
|
+
process.env["TEAM_COMPLIANCE_TEST_DATABASE_PORT"] || "5400",
|
|
222
|
+
),
|
|
223
|
+
username: process.env["DATABASE_USERNAME"] || "postgres",
|
|
224
|
+
password: process.env["DATABASE_PASSWORD"] || "password",
|
|
225
|
+
database:
|
|
226
|
+
process.env["TEAM_COMPLIANCE_TEST_DATABASE_NAME"] ||
|
|
227
|
+
process.env["DATABASE_NAME"] ||
|
|
228
|
+
"oneuptimedb",
|
|
229
|
+
entities: Entities,
|
|
230
|
+
schema,
|
|
231
|
+
synchronize: false,
|
|
232
|
+
logging: ["error"],
|
|
233
|
+
logger: failedQueries,
|
|
234
|
+
extra: { options: `-c search_path=${schema},public` },
|
|
235
|
+
});
|
|
236
|
+
await database.initialize();
|
|
237
|
+
|
|
238
|
+
/*
|
|
239
|
+
* Read the migrated foreign keys before the clones exist, so their
|
|
240
|
+
* definitions name the referenced tables without a schema.
|
|
241
|
+
*/
|
|
242
|
+
migratedForeignKeys = await database.query(
|
|
243
|
+
`SELECT t.relname AS "table",
|
|
244
|
+
c.conname AS "name",
|
|
245
|
+
a.attname AS "column",
|
|
246
|
+
r.relname AS "referencedTable",
|
|
247
|
+
c.confdeltype AS "onDelete",
|
|
248
|
+
pg_get_constraintdef(c.oid) AS "definition"
|
|
249
|
+
FROM pg_constraint c
|
|
250
|
+
JOIN pg_class t ON t.oid = c.conrelid
|
|
251
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
252
|
+
JOIN pg_class r ON r.oid = c.confrelid
|
|
253
|
+
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1]
|
|
254
|
+
WHERE n.nspname = 'public' AND t.relname = ANY($1) AND c.contype = 'f'
|
|
255
|
+
ORDER BY t.relname, a.attname`,
|
|
256
|
+
[JOIN_TABLES],
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
await database.query(`CREATE SCHEMA "${schema}"`);
|
|
260
|
+
|
|
261
|
+
for (const table of TABLES) {
|
|
262
|
+
await database.query(
|
|
263
|
+
`CREATE TABLE "${schema}"."${table}" (LIKE public."${table}" INCLUDING ALL)`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const foreignKey of migratedForeignKeys) {
|
|
268
|
+
const definition: string = foreignKey.definition.replace(
|
|
269
|
+
/REFERENCES public\./g,
|
|
270
|
+
"REFERENCES ",
|
|
271
|
+
);
|
|
272
|
+
await database.query(
|
|
273
|
+
`ALTER TABLE "${schema}"."${foreignKey.table}" ADD CONSTRAINT "${foreignKey.name}" ${definition}`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const currentSchema: Array<{ current_schema: string }> =
|
|
278
|
+
await database.query("SELECT current_schema()");
|
|
279
|
+
expect(currentSchema[0]?.current_schema).toBe(schema);
|
|
280
|
+
|
|
281
|
+
jest.spyOn(PostgresAppInstance, "isConnected").mockReturnValue(true);
|
|
282
|
+
jest
|
|
283
|
+
.spyOn(PostgresAppInstance, "getDataSource")
|
|
284
|
+
.mockReturnValue(database);
|
|
285
|
+
|
|
286
|
+
/*
|
|
287
|
+
* The only side effects of these paths that leave Postgres: realtime
|
|
288
|
+
* events go to the socket server, workflow triggers to the workflow
|
|
289
|
+
* service.
|
|
290
|
+
*/
|
|
291
|
+
jest
|
|
292
|
+
.spyOn(TeamComplianceSettingService, "onTriggerRealtime")
|
|
293
|
+
.mockResolvedValue(undefined);
|
|
294
|
+
jest
|
|
295
|
+
.spyOn(TeamComplianceSettingService, "onTriggerWorkflow")
|
|
296
|
+
.mockResolvedValue(undefined);
|
|
297
|
+
|
|
298
|
+
for (const severityService of [
|
|
299
|
+
IncidentSeverityService,
|
|
300
|
+
AlertSeverityService,
|
|
301
|
+
]) {
|
|
302
|
+
jest
|
|
303
|
+
.spyOn(severityService, "onTriggerRealtime")
|
|
304
|
+
.mockResolvedValue(undefined);
|
|
305
|
+
jest
|
|
306
|
+
.spyOn(severityService, "onTriggerWorkflow")
|
|
307
|
+
.mockResolvedValue(undefined);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
beforeEach(async () => {
|
|
312
|
+
failedQueries.failures = [];
|
|
313
|
+
|
|
314
|
+
await database.query(
|
|
315
|
+
TABLES.map((table: string): string => {
|
|
316
|
+
return `DELETE FROM "${schema}"."${table}"`;
|
|
317
|
+
})
|
|
318
|
+
.reverse()
|
|
319
|
+
.join("; "),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
for (const [id, name] of [
|
|
323
|
+
[projectId, "Compliance project"],
|
|
324
|
+
[otherProjectId, "Another project"],
|
|
325
|
+
] as Array<[ObjectID, string]>) {
|
|
326
|
+
await insert("Project", {
|
|
327
|
+
_id: id.toString(),
|
|
328
|
+
name: name,
|
|
329
|
+
slug: `project-${id.toString()}`,
|
|
330
|
+
version: 1,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
for (const [id, project, name] of [
|
|
335
|
+
[teamId, projectId, "Platform on-call"],
|
|
336
|
+
[otherTeamId, projectId, "Database on-call"],
|
|
337
|
+
[otherProjectTeamId, otherProjectId, "Someone else's team"],
|
|
338
|
+
] as Array<[ObjectID, ObjectID, string]>) {
|
|
339
|
+
await insert("Team", {
|
|
340
|
+
_id: id.toString(),
|
|
341
|
+
projectId: project.toString(),
|
|
342
|
+
name: name,
|
|
343
|
+
slug: `team-${id.toString()}`,
|
|
344
|
+
version: 1,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for (const [id, project, name, color, order] of [
|
|
349
|
+
[critical, projectId, "Critical Incident", "#FF0000", 1],
|
|
350
|
+
[major, projectId, "Major Incident", "#FFA500", 2],
|
|
351
|
+
[minor, projectId, "Minor Incident", "#FFFF00", 3],
|
|
352
|
+
[otherProjectCritical, otherProjectId, "Their Critical", "#000000", 1],
|
|
353
|
+
] as Array<[ObjectID, ObjectID, string, string, number]>) {
|
|
354
|
+
await insertSeverity("IncidentSeverity", {
|
|
355
|
+
id,
|
|
356
|
+
project,
|
|
357
|
+
name,
|
|
358
|
+
color,
|
|
359
|
+
order,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const [id, project, name, color, order] of [
|
|
364
|
+
[criticalAlert, projectId, "Critical Alert", "#DC2626", 1],
|
|
365
|
+
[warningAlert, projectId, "Warning", "#F59E0B", 2],
|
|
366
|
+
[
|
|
367
|
+
otherProjectCriticalAlert,
|
|
368
|
+
otherProjectId,
|
|
369
|
+
"Their Alert",
|
|
370
|
+
"#000000",
|
|
371
|
+
1,
|
|
372
|
+
],
|
|
373
|
+
] as Array<[ObjectID, ObjectID, string, string, number]>) {
|
|
374
|
+
await insertSeverity("AlertSeverity", {
|
|
375
|
+
id,
|
|
376
|
+
project,
|
|
377
|
+
name,
|
|
378
|
+
color,
|
|
379
|
+
order,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
afterEach(() => {
|
|
385
|
+
// A failed statement fails the test, even if a caller swallowed it.
|
|
386
|
+
expect(failedQueries.failures).toEqual([]);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
afterAll(async () => {
|
|
390
|
+
jest.restoreAllMocks();
|
|
391
|
+
if (database?.isInitialized) {
|
|
392
|
+
await database.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
|
|
393
|
+
await database.destroy();
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
async function insert(table: string, row: SqlRow): Promise<void> {
|
|
398
|
+
const columns: Array<string> = Object.keys(row);
|
|
399
|
+
await database.query(
|
|
400
|
+
`INSERT INTO "${schema}"."${table}" (${columns
|
|
401
|
+
.map((column: string): string => {
|
|
402
|
+
return `"${column}"`;
|
|
403
|
+
})
|
|
404
|
+
.join(", ")}) VALUES (${columns
|
|
405
|
+
.map((_column: string, index: number): string => {
|
|
406
|
+
return `$${index + 1}`;
|
|
407
|
+
})
|
|
408
|
+
.join(", ")})`,
|
|
409
|
+
columns.map((column: string): unknown => {
|
|
410
|
+
return row[column];
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function insertSeverity(
|
|
416
|
+
table: "IncidentSeverity" | "AlertSeverity",
|
|
417
|
+
data: {
|
|
418
|
+
id: ObjectID;
|
|
419
|
+
project: ObjectID;
|
|
420
|
+
name: string;
|
|
421
|
+
color: string;
|
|
422
|
+
order: number;
|
|
423
|
+
},
|
|
424
|
+
): Promise<void> {
|
|
425
|
+
await insert(table, {
|
|
426
|
+
_id: data.id.toString(),
|
|
427
|
+
projectId: data.project.toString(),
|
|
428
|
+
name: data.name,
|
|
429
|
+
slug: `severity-${data.id.toString()}`,
|
|
430
|
+
color: data.color,
|
|
431
|
+
order: data.order,
|
|
432
|
+
version: 1,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function ids(list: Array<ObjectID>): Array<string> {
|
|
437
|
+
return list
|
|
438
|
+
.map((id: ObjectID): string => {
|
|
439
|
+
return id.toString();
|
|
440
|
+
})
|
|
441
|
+
.sort();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function upper(id: ObjectID): string {
|
|
445
|
+
return id.toString().toUpperCase();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// `{_id}` JSON, as BaseAPI.updateItem hands an EntityArray to the hooks.
|
|
449
|
+
function asJson(list: Array<ObjectID>): Array<JSONObject> {
|
|
450
|
+
return list.map((id: ObjectID): JSONObject => {
|
|
451
|
+
return { _id: id.toString() };
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/*
|
|
456
|
+
* Models built from `{_id}` JSON by BaseModel.fromJSON: what
|
|
457
|
+
* BaseAPI.createItem hands the hooks for a ModelForm create.
|
|
458
|
+
*/
|
|
459
|
+
function asIncidentModels(list: Array<ObjectID>): Array<IncidentSeverity> {
|
|
460
|
+
return BaseModel.fromJSONArray<IncidentSeverity>(
|
|
461
|
+
asJson(list),
|
|
462
|
+
IncidentSeverity,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function asStrings(list: Array<ObjectID>): Array<string> {
|
|
467
|
+
return list.map((id: ObjectID): string => {
|
|
468
|
+
return id.toString();
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function buildRule(input: RuleInput): TeamComplianceSetting {
|
|
473
|
+
const setting: TeamComplianceSetting = new TeamComplianceSetting();
|
|
474
|
+
setting.projectId = input.projectId || projectId;
|
|
475
|
+
setting.teamId = input.teamId || teamId;
|
|
476
|
+
setting.ruleType = input.ruleType;
|
|
477
|
+
setting.enabled = input.enabled === undefined ? true : input.enabled;
|
|
478
|
+
|
|
479
|
+
/*
|
|
480
|
+
* The hooks see whatever shape the caller sends, so the severity lists
|
|
481
|
+
* go on the model untyped, exactly as they arrive.
|
|
482
|
+
*/
|
|
483
|
+
const raw: Record<string, unknown> = setting as unknown as Record<
|
|
484
|
+
string,
|
|
485
|
+
unknown
|
|
486
|
+
>;
|
|
487
|
+
|
|
488
|
+
if (input.notificationChannel !== undefined) {
|
|
489
|
+
raw["notificationChannel"] = input.notificationChannel;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (input.incidentSeverities !== undefined) {
|
|
493
|
+
raw["incidentSeverities"] = input.incidentSeverities;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (input.alertSeverities !== undefined) {
|
|
497
|
+
raw["alertSeverities"] = input.alertSeverities;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return setting;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function createRule(
|
|
504
|
+
input: RuleInput,
|
|
505
|
+
props: DatabaseCommonInteractionProps = ROOT,
|
|
506
|
+
): Promise<ObjectID> {
|
|
507
|
+
const created: TeamComplianceSetting =
|
|
508
|
+
await TeamComplianceSettingService.create({
|
|
509
|
+
data: buildRule(input),
|
|
510
|
+
props: props,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
expect(created.id).toBeTruthy();
|
|
514
|
+
|
|
515
|
+
return created.id!;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function updateRule(
|
|
519
|
+
id: ObjectID,
|
|
520
|
+
data: JSONObject,
|
|
521
|
+
props: DatabaseCommonInteractionProps = ROOT,
|
|
522
|
+
): Promise<void> {
|
|
523
|
+
await TeamComplianceSettingService.updateOneById({
|
|
524
|
+
id: id,
|
|
525
|
+
data: data as unknown as UpdateBy<TeamComplianceSetting>["data"],
|
|
526
|
+
props: props,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function joinRows(
|
|
531
|
+
table: string,
|
|
532
|
+
settingId: ObjectID,
|
|
533
|
+
): Promise<Array<string>> {
|
|
534
|
+
const column: string =
|
|
535
|
+
table === INCIDENT_JOIN_TABLE
|
|
536
|
+
? "incidentSeverityId"
|
|
537
|
+
: "alertSeverityId";
|
|
538
|
+
const rows: Array<SqlRow> = await database.query(
|
|
539
|
+
`SELECT "${column}" AS "id" FROM "${schema}"."${table}" WHERE "teamComplianceSettingId" = $1 ORDER BY "${column}"`,
|
|
540
|
+
[settingId.toString()],
|
|
541
|
+
);
|
|
542
|
+
return rows.map((row: SqlRow): string => {
|
|
543
|
+
return String(row["id"]);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function stored(settingId: ObjectID): Promise<StoredRule | null> {
|
|
548
|
+
const rows: Array<SqlRow> = await database.query(
|
|
549
|
+
`SELECT "ruleType", "notificationChannel", "enabled" FROM "${schema}"."TeamComplianceSetting" WHERE "_id" = $1`,
|
|
550
|
+
[settingId.toString()],
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
if (!rows[0]) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
ruleType: String(rows[0]["ruleType"]),
|
|
559
|
+
notificationChannel:
|
|
560
|
+
rows[0]["notificationChannel"] === null
|
|
561
|
+
? null
|
|
562
|
+
: String(rows[0]["notificationChannel"]),
|
|
563
|
+
enabled: rows[0]["enabled"] === true,
|
|
564
|
+
incidentSeverityIds: await joinRows(INCIDENT_JOIN_TABLE, settingId),
|
|
565
|
+
alertSeverityIds: await joinRows(ALERT_JOIN_TABLE, settingId),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// The rule's options column, as Postgres holds it.
|
|
570
|
+
async function storedOptions(settingId: ObjectID): Promise<unknown> {
|
|
571
|
+
const rows: Array<SqlRow> = await database.query(
|
|
572
|
+
`SELECT "options" FROM "${schema}"."TeamComplianceSetting" WHERE "_id" = $1`,
|
|
573
|
+
[settingId.toString()],
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
return rows[0]?.["options"];
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/*
|
|
580
|
+
* Runs `deletes` side by side, holding each one's rule lookup until every
|
|
581
|
+
* one of them has looked: what two requests (or two App replicas)
|
|
582
|
+
* deleting at the same moment do. Without the hold, Promise.all may run
|
|
583
|
+
* one delete to completion before the other looks, which hides the race.
|
|
584
|
+
*/
|
|
585
|
+
async function deleteSideBySide(
|
|
586
|
+
deletes: Array<() => Promise<unknown>>,
|
|
587
|
+
): Promise<void> {
|
|
588
|
+
const lookup: (typeof TeamComplianceSettingService)["getRulesScopedToAnyOf"] =
|
|
589
|
+
TeamComplianceSettingService.getRulesScopedToAnyOf.bind(
|
|
590
|
+
TeamComplianceSettingService,
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
let arrived: number = 0;
|
|
594
|
+
let releaseAll: () => void = (): void => {
|
|
595
|
+
return;
|
|
596
|
+
};
|
|
597
|
+
const everyoneHasLooked: Promise<void> = new Promise<void>(
|
|
598
|
+
(resolve: () => void): void => {
|
|
599
|
+
releaseAll = resolve;
|
|
600
|
+
},
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
const spy: jest.SpyInstance = jest
|
|
604
|
+
.spyOn(TeamComplianceSettingService, "getRulesScopedToAnyOf")
|
|
605
|
+
.mockImplementation(
|
|
606
|
+
async (data: {
|
|
607
|
+
severityKind: ComplianceSeverityKind;
|
|
608
|
+
severities: Array<DeletedSeverity>;
|
|
609
|
+
}): Promise<Array<string>> => {
|
|
610
|
+
const found: Array<string> = await lookup(data);
|
|
611
|
+
|
|
612
|
+
arrived++;
|
|
613
|
+
|
|
614
|
+
if (arrived === deletes.length) {
|
|
615
|
+
releaseAll();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
await everyoneHasLooked;
|
|
619
|
+
|
|
620
|
+
return found;
|
|
621
|
+
},
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
await Promise.all(
|
|
626
|
+
deletes.map((run: () => Promise<unknown>): Promise<unknown> => {
|
|
627
|
+
return run();
|
|
628
|
+
}),
|
|
629
|
+
);
|
|
630
|
+
} finally {
|
|
631
|
+
spy.mockRestore();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
expect(arrived).toBe(deletes.length);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function countRules(): Promise<number> {
|
|
638
|
+
const rows: Array<{ count: string }> = await database.query(
|
|
639
|
+
`SELECT COUNT(*)::text AS "count" FROM "${schema}"."TeamComplianceSetting"`,
|
|
640
|
+
);
|
|
641
|
+
return Number(rows[0]!.count);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function countJoinRows(): Promise<number> {
|
|
645
|
+
const rows: Array<{ count: string }> = await database.query(
|
|
646
|
+
`SELECT ((SELECT COUNT(*) FROM "${schema}"."${INCIDENT_JOIN_TABLE}") + (SELECT COUNT(*) FROM "${schema}"."${ALERT_JOIN_TABLE}"))::text AS "count"`,
|
|
647
|
+
);
|
|
648
|
+
return Number(rows[0]!.count);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function toReadSeverities(
|
|
652
|
+
list: Array<IncidentSeverity | AlertSeverity> | undefined,
|
|
653
|
+
): Array<ReadSeverity> {
|
|
654
|
+
return (list || [])
|
|
655
|
+
.map((severity: IncidentSeverity | AlertSeverity): ReadSeverity => {
|
|
656
|
+
return {
|
|
657
|
+
id: severity.id!.toString(),
|
|
658
|
+
name: severity.name,
|
|
659
|
+
color: severity.color ? severity.color.toString() : undefined,
|
|
660
|
+
order: severity.order,
|
|
661
|
+
};
|
|
662
|
+
})
|
|
663
|
+
.sort((a: ReadSeverity, b: ReadSeverity): number => {
|
|
664
|
+
return (a.order || 0) - (b.order || 0);
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// The rule read back the way the compliance page and the edit form do.
|
|
669
|
+
async function readRule(
|
|
670
|
+
id: ObjectID,
|
|
671
|
+
props: DatabaseCommonInteractionProps = ROOT,
|
|
672
|
+
): Promise<TeamComplianceSetting | null> {
|
|
673
|
+
return await TeamComplianceSettingService.findOneById({
|
|
674
|
+
id: id,
|
|
675
|
+
select: {
|
|
676
|
+
_id: true,
|
|
677
|
+
ruleType: true,
|
|
678
|
+
enabled: true,
|
|
679
|
+
notificationChannel: true,
|
|
680
|
+
incidentSeverities: {
|
|
681
|
+
_id: true,
|
|
682
|
+
name: true,
|
|
683
|
+
color: true,
|
|
684
|
+
order: true,
|
|
685
|
+
},
|
|
686
|
+
alertSeverities: {
|
|
687
|
+
_id: true,
|
|
688
|
+
name: true,
|
|
689
|
+
color: true,
|
|
690
|
+
order: true,
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
props: props,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// A signed-in user holding `permissions` in `project`, owning nothing.
|
|
698
|
+
function memberProps(
|
|
699
|
+
project: ObjectID,
|
|
700
|
+
permissions: Array<Permission>,
|
|
701
|
+
): DatabaseCommonInteractionProps {
|
|
702
|
+
const tenantPermission: UserTenantAccessPermission = {
|
|
703
|
+
projectId: project,
|
|
704
|
+
_type: "UserTenantAccessPermission",
|
|
705
|
+
permissions: permissions.map((permission: Permission) => {
|
|
706
|
+
return {
|
|
707
|
+
_type: "UserPermission",
|
|
708
|
+
permission: permission,
|
|
709
|
+
labelIds: [],
|
|
710
|
+
isBlockPermission: false,
|
|
711
|
+
};
|
|
712
|
+
}),
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
return {
|
|
716
|
+
tenantId: project,
|
|
717
|
+
userId: ObjectID.generate(),
|
|
718
|
+
userType: UserType.User,
|
|
719
|
+
userTenantAccessPermission: {
|
|
720
|
+
[project.toString()]: tenantPermission,
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
describe("the migrated tables", () => {
|
|
726
|
+
test("a severity join row goes with its rule or its severity", () => {
|
|
727
|
+
expect(
|
|
728
|
+
migratedForeignKeys.map((foreignKey: ForeignKeyRow) => {
|
|
729
|
+
return [
|
|
730
|
+
foreignKey.table,
|
|
731
|
+
foreignKey.column,
|
|
732
|
+
foreignKey.referencedTable,
|
|
733
|
+
foreignKey.onDelete,
|
|
734
|
+
];
|
|
735
|
+
}),
|
|
736
|
+
).toEqual([
|
|
737
|
+
// confdeltype: c = CASCADE
|
|
738
|
+
[
|
|
739
|
+
"TeamComplianceSettingAlertSeverity",
|
|
740
|
+
"alertSeverityId",
|
|
741
|
+
"AlertSeverity",
|
|
742
|
+
"c",
|
|
743
|
+
],
|
|
744
|
+
[
|
|
745
|
+
"TeamComplianceSettingAlertSeverity",
|
|
746
|
+
"teamComplianceSettingId",
|
|
747
|
+
"TeamComplianceSetting",
|
|
748
|
+
"c",
|
|
749
|
+
],
|
|
750
|
+
[
|
|
751
|
+
"TeamComplianceSettingIncidentSeverity",
|
|
752
|
+
"incidentSeverityId",
|
|
753
|
+
"IncidentSeverity",
|
|
754
|
+
"c",
|
|
755
|
+
],
|
|
756
|
+
[
|
|
757
|
+
"TeamComplianceSettingIncidentSeverity",
|
|
758
|
+
"teamComplianceSettingId",
|
|
759
|
+
"TeamComplianceSetting",
|
|
760
|
+
"c",
|
|
761
|
+
],
|
|
762
|
+
]);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test("every existing rule reads as 'any channel', and one team may hold several rules of a type", async () => {
|
|
766
|
+
const columns: Array<SqlRow> = await database.query(
|
|
767
|
+
`SELECT data_type, is_nullable, column_default, character_maximum_length
|
|
768
|
+
FROM information_schema.columns
|
|
769
|
+
WHERE table_schema = 'public' AND table_name = 'TeamComplianceSetting' AND column_name = 'notificationChannel'`,
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
expect(columns).toEqual([
|
|
773
|
+
{
|
|
774
|
+
data_type: "character varying",
|
|
775
|
+
is_nullable: "YES",
|
|
776
|
+
column_default: null,
|
|
777
|
+
character_maximum_length: 100,
|
|
778
|
+
},
|
|
779
|
+
]);
|
|
780
|
+
|
|
781
|
+
const uniqueIndexes: Array<SqlRow> = await database.query(
|
|
782
|
+
`SELECT indexname FROM pg_indexes
|
|
783
|
+
WHERE schemaname = 'public' AND tablename = 'TeamComplianceSetting'
|
|
784
|
+
AND indexdef LIKE 'CREATE UNIQUE INDEX%' AND indexdef LIKE '%"ruleType"%'`,
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
expect(uniqueIndexes).toEqual([]);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
describe("creating a rule", () => {
|
|
792
|
+
test("severity ids sent as strings land as join rows, and read back with their name, colour and order", async () => {
|
|
793
|
+
const id: ObjectID = await createRule({
|
|
794
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
795
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
796
|
+
incidentSeverities: asStrings([major, critical]),
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
expect(await stored(id)).toEqual({
|
|
800
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
801
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
802
|
+
enabled: true,
|
|
803
|
+
incidentSeverityIds: ids([critical, major]),
|
|
804
|
+
alertSeverityIds: [],
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
const rule: TeamComplianceSetting | null = await readRule(id);
|
|
808
|
+
|
|
809
|
+
expect(rule?.notificationChannel).toBe(
|
|
810
|
+
ComplianceNotificationChannel.Call,
|
|
811
|
+
);
|
|
812
|
+
expect(toReadSeverities(rule?.incidentSeverities)).toEqual([
|
|
813
|
+
{
|
|
814
|
+
id: critical.toString(),
|
|
815
|
+
name: "Critical Incident",
|
|
816
|
+
color: "#FF0000",
|
|
817
|
+
order: 1,
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
id: major.toString(),
|
|
821
|
+
name: "Major Incident",
|
|
822
|
+
color: "#FFA500",
|
|
823
|
+
order: 2,
|
|
824
|
+
},
|
|
825
|
+
]);
|
|
826
|
+
expect(rule?.alertSeverities || []).toEqual([]);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
test("severity ids sent as {_id} JSON land as join rows", async () => {
|
|
830
|
+
const id: ObjectID = await createRule({
|
|
831
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
832
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
833
|
+
alertSeverities: asJson([criticalAlert]),
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
expect(await stored(id)).toEqual({
|
|
837
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
838
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
839
|
+
enabled: true,
|
|
840
|
+
incidentSeverityIds: [],
|
|
841
|
+
alertSeverityIds: ids([criticalAlert]),
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
const rule: TeamComplianceSetting | null = await readRule(id);
|
|
845
|
+
|
|
846
|
+
expect(toReadSeverities(rule?.alertSeverities)).toEqual([
|
|
847
|
+
{
|
|
848
|
+
id: criticalAlert.toString(),
|
|
849
|
+
name: "Critical Alert",
|
|
850
|
+
color: "#DC2626",
|
|
851
|
+
order: 1,
|
|
852
|
+
},
|
|
853
|
+
]);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
test("severities as the models BaseAPI builds from a ModelForm create land as join rows", async () => {
|
|
857
|
+
const setting: TeamComplianceSetting =
|
|
858
|
+
BaseModel.fromJSON<TeamComplianceSetting>(
|
|
859
|
+
{
|
|
860
|
+
ruleType: ComplianceRuleType.HasIncidentEpisodeOnCallRules,
|
|
861
|
+
notificationChannel: ComplianceNotificationChannel.SMS,
|
|
862
|
+
enabled: true,
|
|
863
|
+
incidentSeverities: asJson([critical, minor]),
|
|
864
|
+
},
|
|
865
|
+
TeamComplianceSetting,
|
|
866
|
+
) as TeamComplianceSetting;
|
|
867
|
+
setting.projectId = projectId;
|
|
868
|
+
setting.teamId = teamId;
|
|
869
|
+
|
|
870
|
+
const created: TeamComplianceSetting =
|
|
871
|
+
await TeamComplianceSettingService.create({
|
|
872
|
+
data: setting,
|
|
873
|
+
props: ROOT,
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
expect(await stored(created.id!)).toEqual({
|
|
877
|
+
ruleType: ComplianceRuleType.HasIncidentEpisodeOnCallRules,
|
|
878
|
+
notificationChannel: ComplianceNotificationChannel.SMS,
|
|
879
|
+
enabled: true,
|
|
880
|
+
incidentSeverityIds: ids([critical, minor]),
|
|
881
|
+
alertSeverityIds: [],
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
test("no severities and no channel stores an 'any channel, every severity' rule with no join rows", async () => {
|
|
886
|
+
const id: ObjectID = await createRule({
|
|
887
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
expect(await stored(id)).toEqual({
|
|
891
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
892
|
+
notificationChannel: null,
|
|
893
|
+
enabled: true,
|
|
894
|
+
incidentSeverityIds: [],
|
|
895
|
+
alertSeverityIds: [],
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
const rule: TeamComplianceSetting | null = await readRule(id);
|
|
899
|
+
|
|
900
|
+
expect(rule?.incidentSeverities || []).toEqual([]);
|
|
901
|
+
expect(rule?.notificationChannel ?? null).toBeNull();
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
test("options the rule type does not use are not stored", async () => {
|
|
905
|
+
const methodRule: ObjectID = await createRule({
|
|
906
|
+
ruleType: ComplianceRuleType.HasNotificationCallMethod,
|
|
907
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
908
|
+
incidentSeverities: asStrings([critical]),
|
|
909
|
+
alertSeverities: asJson([criticalAlert]),
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
expect(await stored(methodRule)).toEqual({
|
|
913
|
+
ruleType: ComplianceRuleType.HasNotificationCallMethod,
|
|
914
|
+
notificationChannel: null,
|
|
915
|
+
enabled: true,
|
|
916
|
+
incidentSeverityIds: [],
|
|
917
|
+
alertSeverityIds: [],
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
const incidentRule: ObjectID = await createRule({
|
|
921
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
922
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
923
|
+
incidentSeverities: asStrings([critical]),
|
|
924
|
+
alertSeverities: asJson([criticalAlert]),
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
expect(await stored(incidentRule)).toEqual({
|
|
928
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
929
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
930
|
+
enabled: true,
|
|
931
|
+
incidentSeverityIds: ids([critical]),
|
|
932
|
+
alertSeverityIds: [],
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
const alertRule: ObjectID = await createRule({
|
|
936
|
+
ruleType: ComplianceRuleType.HasAlertEpisodeOnCallRules,
|
|
937
|
+
incidentSeverities: asStrings([critical]),
|
|
938
|
+
alertSeverities: asStrings([warningAlert]),
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
expect(await stored(alertRule)).toEqual({
|
|
942
|
+
ruleType: ComplianceRuleType.HasAlertEpisodeOnCallRules,
|
|
943
|
+
notificationChannel: null,
|
|
944
|
+
enabled: true,
|
|
945
|
+
incidentSeverityIds: [],
|
|
946
|
+
alertSeverityIds: ids([warningAlert]),
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
test("an exact duplicate is refused whatever the shape, order or case of its severity ids", async () => {
|
|
951
|
+
await createRule({
|
|
952
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
953
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
954
|
+
incidentSeverities: asStrings([critical, major]),
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
const duplicates: Array<SeverityList> = [
|
|
958
|
+
asStrings([critical, major]),
|
|
959
|
+
asStrings([major, critical]),
|
|
960
|
+
[upper(critical), upper(major)],
|
|
961
|
+
[upper(major), critical.toString()],
|
|
962
|
+
asJson([major, critical]),
|
|
963
|
+
asIncidentModels([major, critical]),
|
|
964
|
+
asStrings([critical, major, critical]),
|
|
965
|
+
];
|
|
966
|
+
|
|
967
|
+
for (const incidentSeverities of duplicates) {
|
|
968
|
+
await expect(
|
|
969
|
+
createRule({
|
|
970
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
971
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
972
|
+
incidentSeverities: incidentSeverities,
|
|
973
|
+
}),
|
|
974
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
expect(await countRules()).toBe(1);
|
|
978
|
+
expect(await countJoinRows()).toBe(2);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
/*
|
|
982
|
+
* The premise of getIds' lower-casing: Postgres parses a uuid without
|
|
983
|
+
* regard to case and hands it back lower case.
|
|
984
|
+
*/
|
|
985
|
+
test("an upper-case id is stored as the lower-case uuid, and the lower-case spelling is then a duplicate", async () => {
|
|
986
|
+
const id: ObjectID = await createRule({
|
|
987
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
988
|
+
notificationChannel: ComplianceNotificationChannel.WhatsApp,
|
|
989
|
+
incidentSeverities: [upper(minor)],
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, id)).toEqual(ids([minor]));
|
|
993
|
+
|
|
994
|
+
await expect(
|
|
995
|
+
createRule({
|
|
996
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
997
|
+
notificationChannel: ComplianceNotificationChannel.WhatsApp,
|
|
998
|
+
incidentSeverities: asStrings([minor]),
|
|
999
|
+
}),
|
|
1000
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
test("the legacy 'any channel, every severity' rule and a method rule are still one per team", async () => {
|
|
1004
|
+
await createRule({
|
|
1005
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
await expect(
|
|
1009
|
+
createRule({
|
|
1010
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1011
|
+
notificationChannel: null,
|
|
1012
|
+
incidentSeverities: [],
|
|
1013
|
+
}),
|
|
1014
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
1015
|
+
|
|
1016
|
+
await createRule({
|
|
1017
|
+
ruleType: ComplianceRuleType.HasNotificationCallMethod,
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
// The channel and severities a method rule does not use change nothing.
|
|
1021
|
+
await expect(
|
|
1022
|
+
createRule({
|
|
1023
|
+
ruleType: ComplianceRuleType.HasNotificationCallMethod,
|
|
1024
|
+
notificationChannel: ComplianceNotificationChannel.SMS,
|
|
1025
|
+
incidentSeverities: asStrings([critical]),
|
|
1026
|
+
}),
|
|
1027
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
1028
|
+
|
|
1029
|
+
expect(await countRules()).toBe(2);
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
test("a different channel, severity set, rule type or team is a different rule", async () => {
|
|
1033
|
+
const scope: Array<ObjectID> = [critical, major];
|
|
1034
|
+
|
|
1035
|
+
await createRule({
|
|
1036
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1037
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1038
|
+
incidentSeverities: asStrings(scope),
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
const allowed: Array<RuleInput> = [
|
|
1042
|
+
// Push for the same severities.
|
|
1043
|
+
{
|
|
1044
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1045
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1046
|
+
incidentSeverities: asStrings(scope),
|
|
1047
|
+
},
|
|
1048
|
+
// Any channel for the same severities.
|
|
1049
|
+
{
|
|
1050
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1051
|
+
incidentSeverities: asStrings(scope),
|
|
1052
|
+
},
|
|
1053
|
+
// Call for a subset.
|
|
1054
|
+
{
|
|
1055
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1056
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1057
|
+
incidentSeverities: asStrings([critical]),
|
|
1058
|
+
},
|
|
1059
|
+
// Call for a superset.
|
|
1060
|
+
{
|
|
1061
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1062
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1063
|
+
incidentSeverities: asStrings([critical, major, minor]),
|
|
1064
|
+
},
|
|
1065
|
+
// Call for every severity.
|
|
1066
|
+
{
|
|
1067
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1068
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1069
|
+
},
|
|
1070
|
+
// The episode rule type.
|
|
1071
|
+
{
|
|
1072
|
+
ruleType: ComplianceRuleType.HasIncidentEpisodeOnCallRules,
|
|
1073
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1074
|
+
incidentSeverities: asStrings(scope),
|
|
1075
|
+
},
|
|
1076
|
+
// The same rule on another team.
|
|
1077
|
+
{
|
|
1078
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1079
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1080
|
+
incidentSeverities: asStrings(scope),
|
|
1081
|
+
teamId: otherTeamId,
|
|
1082
|
+
},
|
|
1083
|
+
];
|
|
1084
|
+
|
|
1085
|
+
for (const input of allowed) {
|
|
1086
|
+
await createRule(input);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
expect(await countRules()).toBe(1 + allowed.length);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
test("a severity of another project, of the other kind, or that does not exist is refused and nothing is written", async () => {
|
|
1093
|
+
const refused: Array<{ input: RuleInput; message: string }> = [
|
|
1094
|
+
{
|
|
1095
|
+
input: {
|
|
1096
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1097
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1098
|
+
incidentSeverities: asStrings([otherProjectCritical]),
|
|
1099
|
+
},
|
|
1100
|
+
message: FOREIGN_SEVERITY_MESSAGE,
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
input: {
|
|
1104
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1105
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1106
|
+
incidentSeverities: asJson([critical, otherProjectCritical]),
|
|
1107
|
+
},
|
|
1108
|
+
message: FOREIGN_SEVERITY_MESSAGE,
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
input: {
|
|
1112
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1113
|
+
incidentSeverities: asStrings([criticalAlert]),
|
|
1114
|
+
},
|
|
1115
|
+
message: FOREIGN_SEVERITY_MESSAGE,
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
input: {
|
|
1119
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
1120
|
+
alertSeverities: asStrings([otherProjectCriticalAlert]),
|
|
1121
|
+
},
|
|
1122
|
+
message:
|
|
1123
|
+
"One or more of the selected alert severities do not exist in this project.",
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
input: {
|
|
1127
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1128
|
+
incidentSeverities: asStrings([ObjectID.generate()]),
|
|
1129
|
+
},
|
|
1130
|
+
message: FOREIGN_SEVERITY_MESSAGE,
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
input: {
|
|
1134
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1135
|
+
incidentSeverities: ["not-a-uuid"],
|
|
1136
|
+
},
|
|
1137
|
+
message: 'Invalid ID format: "not-a-uuid".',
|
|
1138
|
+
},
|
|
1139
|
+
];
|
|
1140
|
+
|
|
1141
|
+
for (const { input, message } of refused) {
|
|
1142
|
+
await expect(createRule(input)).rejects.toThrow(message);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
expect(await countRules()).toBe(0);
|
|
1146
|
+
expect(await countJoinRows()).toBe(0);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
/*
|
|
1150
|
+
* The relation save inserts one join row per list item, and the join
|
|
1151
|
+
* table's primary key refuses the same severity twice. The service
|
|
1152
|
+
* writes the list it checked - de-duplicated, lower-cased - so a
|
|
1153
|
+
* repeat, in any case and any shape, is one join row, on create and on
|
|
1154
|
+
* update (where a re-sent id in another case must also not be INSERTed
|
|
1155
|
+
* next to the stored one).
|
|
1156
|
+
*/
|
|
1157
|
+
test("a repeated severity id (in any case, on create or update) is stored once", async () => {
|
|
1158
|
+
const id: ObjectID = await createRule({
|
|
1159
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1160
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1161
|
+
incidentSeverities: [critical.toString(), upper(critical)],
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, id)).toEqual(
|
|
1165
|
+
ids([critical]),
|
|
1166
|
+
);
|
|
1167
|
+
|
|
1168
|
+
await updateRule(id, {
|
|
1169
|
+
incidentSeverities: asJson([major, major]),
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, id)).toEqual(ids([major]));
|
|
1173
|
+
|
|
1174
|
+
await updateRule(id, {
|
|
1175
|
+
incidentSeverities: [
|
|
1176
|
+
{ _id: upper(major) },
|
|
1177
|
+
{ id: critical.toString() },
|
|
1178
|
+
],
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, id)).toEqual(
|
|
1182
|
+
ids([critical, major]),
|
|
1183
|
+
);
|
|
1184
|
+
|
|
1185
|
+
const alertRule: ObjectID = await createRule({
|
|
1186
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
1187
|
+
alertSeverities: [
|
|
1188
|
+
...asJson([warningAlert, warningAlert]),
|
|
1189
|
+
upper(warningAlert),
|
|
1190
|
+
],
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
expect(await joinRows(ALERT_JOIN_TABLE, alertRule)).toEqual(
|
|
1194
|
+
ids([warningAlert]),
|
|
1195
|
+
);
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
test("a severity list item is checked by the id the save writes: one read by its `id` cannot smuggle in another project's severity", async () => {
|
|
1199
|
+
const id: ObjectID = await createRule({
|
|
1200
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1201
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1202
|
+
incidentSeverities: asStrings([critical]),
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
/*
|
|
1206
|
+
* What BaseAPI.updateItem hands the hooks: the JSON body, deserialized
|
|
1207
|
+
* (an `{_type: "ObjectID"}` value becomes an ObjectID), not models.
|
|
1208
|
+
* The save reads an `_id` only when it is a string, and otherwise the
|
|
1209
|
+
* item's `id`.
|
|
1210
|
+
*/
|
|
1211
|
+
for (const item of [
|
|
1212
|
+
{ _id: 1, id: otherProjectCritical.toString() },
|
|
1213
|
+
{ _id: true, id: otherProjectCritical.toString() },
|
|
1214
|
+
{ _id: critical, id: otherProjectCritical.toString() },
|
|
1215
|
+
]) {
|
|
1216
|
+
await expect(
|
|
1217
|
+
updateRule(id, { incidentSeverities: [item] as never }),
|
|
1218
|
+
).rejects.toThrow(FOREIGN_SEVERITY_MESSAGE);
|
|
1219
|
+
|
|
1220
|
+
await expect(
|
|
1221
|
+
createRule({
|
|
1222
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1223
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1224
|
+
incidentSeverities: [item as unknown as JSONObject],
|
|
1225
|
+
}),
|
|
1226
|
+
).rejects.toThrow(FOREIGN_SEVERITY_MESSAGE);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/*
|
|
1230
|
+
* An item the save would drop is refused: the rule would otherwise be
|
|
1231
|
+
* stored with no severity at all - a rule for every severity.
|
|
1232
|
+
*/
|
|
1233
|
+
await expect(
|
|
1234
|
+
updateRule(id, { incidentSeverities: [{ _id: major }] as never }),
|
|
1235
|
+
).rejects.toThrow(
|
|
1236
|
+
"Every incident severity of a compliance rule must be a severity id.",
|
|
1237
|
+
);
|
|
1238
|
+
|
|
1239
|
+
expect(await stored(id)).toEqual({
|
|
1240
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1241
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1242
|
+
enabled: true,
|
|
1243
|
+
incidentSeverityIds: ids([critical]),
|
|
1244
|
+
alertSeverityIds: [],
|
|
1245
|
+
});
|
|
1246
|
+
expect(await countRules()).toBe(1);
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
test("a refused create leaves no rule behind", async () => {
|
|
1250
|
+
/*
|
|
1251
|
+
* A severity that passes the service's check but not the join
|
|
1252
|
+
* table's foreign key - the check is told it exists - so the refusal
|
|
1253
|
+
* comes from Postgres, after the rule row was inserted in the same
|
|
1254
|
+
* save.
|
|
1255
|
+
*/
|
|
1256
|
+
const severityCheck: jest.SpyInstance = jest
|
|
1257
|
+
.spyOn(IncidentSeverityService, "countBy")
|
|
1258
|
+
.mockResolvedValueOnce(new PositiveNumber(1));
|
|
1259
|
+
|
|
1260
|
+
try {
|
|
1261
|
+
await expect(
|
|
1262
|
+
createRule({
|
|
1263
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1264
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1265
|
+
incidentSeverities: asStrings([ObjectID.generate()]),
|
|
1266
|
+
}),
|
|
1267
|
+
).rejects.toThrow();
|
|
1268
|
+
} finally {
|
|
1269
|
+
severityCheck.mockRestore();
|
|
1270
|
+
failedQueries.failures = [];
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/*
|
|
1274
|
+
* The rule row and its join rows are one save: a rule that lost its
|
|
1275
|
+
* severities would read as "every severity" - a stricter rule than
|
|
1276
|
+
* the one asked for.
|
|
1277
|
+
*/
|
|
1278
|
+
expect(await countRules()).toBe(0);
|
|
1279
|
+
expect(await countJoinRows()).toBe(0);
|
|
1280
|
+
});
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
describe("updating a rule", () => {
|
|
1284
|
+
async function callForCriticalAndMajor(): Promise<ObjectID> {
|
|
1285
|
+
return await createRule({
|
|
1286
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1287
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1288
|
+
incidentSeverities: asStrings([critical, major]),
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
test("toggling enabled leaves the scope alone; switching off reads nothing, switching on reads only the row", async () => {
|
|
1293
|
+
const id: ObjectID = await callForCriticalAndMajor();
|
|
1294
|
+
|
|
1295
|
+
const findBy: jest.SpyInstance = jest.spyOn(
|
|
1296
|
+
TeamComplianceSettingService,
|
|
1297
|
+
"findBy",
|
|
1298
|
+
);
|
|
1299
|
+
const incidentCount: jest.SpyInstance = jest.spyOn(
|
|
1300
|
+
IncidentSeverityService,
|
|
1301
|
+
"countBy",
|
|
1302
|
+
);
|
|
1303
|
+
const alertCount: jest.SpyInstance = jest.spyOn(
|
|
1304
|
+
AlertSeverityService,
|
|
1305
|
+
"countBy",
|
|
1306
|
+
);
|
|
1307
|
+
|
|
1308
|
+
try {
|
|
1309
|
+
await updateRule(id, { enabled: false });
|
|
1310
|
+
|
|
1311
|
+
expect(await stored(id)).toEqual({
|
|
1312
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1313
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1314
|
+
enabled: false,
|
|
1315
|
+
incidentSeverityIds: ids([critical, major]),
|
|
1316
|
+
alertSeverityIds: [],
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
expect(findBy).not.toHaveBeenCalled();
|
|
1320
|
+
|
|
1321
|
+
await updateRule(id, { enabled: true });
|
|
1322
|
+
|
|
1323
|
+
expect((await stored(id))?.enabled).toBe(true);
|
|
1324
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, id)).toEqual(
|
|
1325
|
+
ids([critical, major]),
|
|
1326
|
+
);
|
|
1327
|
+
|
|
1328
|
+
// Whether a severity delete emptied it: one read of the row.
|
|
1329
|
+
expect(findBy).toHaveBeenCalledTimes(1);
|
|
1330
|
+
expect(incidentCount).not.toHaveBeenCalled();
|
|
1331
|
+
expect(alertCount).not.toHaveBeenCalled();
|
|
1332
|
+
} finally {
|
|
1333
|
+
findBy.mockRestore();
|
|
1334
|
+
incidentCount.mockRestore();
|
|
1335
|
+
alertCount.mockRestore();
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
test("changing an incident rule into a method rule clears its channel and severities", async () => {
|
|
1340
|
+
const id: ObjectID = await callForCriticalAndMajor();
|
|
1341
|
+
|
|
1342
|
+
await updateRule(id, {
|
|
1343
|
+
ruleType: ComplianceRuleType.HasNotificationCallMethod,
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
expect(await stored(id)).toEqual({
|
|
1347
|
+
ruleType: ComplianceRuleType.HasNotificationCallMethod,
|
|
1348
|
+
notificationChannel: null,
|
|
1349
|
+
enabled: true,
|
|
1350
|
+
incidentSeverityIds: [],
|
|
1351
|
+
alertSeverityIds: [],
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
const rule: TeamComplianceSetting | null = await readRule(id);
|
|
1355
|
+
|
|
1356
|
+
expect(rule?.incidentSeverities || []).toEqual([]);
|
|
1357
|
+
expect(rule?.notificationChannel ?? null).toBeNull();
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
test("changing an incident rule into an alert rule drops the incident severities and keeps the channel", async () => {
|
|
1361
|
+
const id: ObjectID = await callForCriticalAndMajor();
|
|
1362
|
+
|
|
1363
|
+
await updateRule(id, {
|
|
1364
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
1365
|
+
alertSeverities: asJson([criticalAlert]),
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
expect(await stored(id)).toEqual({
|
|
1369
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
1370
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1371
|
+
enabled: true,
|
|
1372
|
+
incidentSeverityIds: [],
|
|
1373
|
+
alertSeverityIds: ids([criticalAlert]),
|
|
1374
|
+
});
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
test("sending severities replaces the join rows, and an empty list clears them", async () => {
|
|
1378
|
+
const id: ObjectID = await callForCriticalAndMajor();
|
|
1379
|
+
|
|
1380
|
+
await updateRule(id, { incidentSeverities: asJson([minor]) });
|
|
1381
|
+
|
|
1382
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, id)).toEqual(ids([minor]));
|
|
1383
|
+
|
|
1384
|
+
await updateRule(id, {
|
|
1385
|
+
incidentSeverities: asStrings([major, critical]),
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, id)).toEqual(
|
|
1389
|
+
ids([critical, major]),
|
|
1390
|
+
);
|
|
1391
|
+
|
|
1392
|
+
await updateRule(id, { incidentSeverities: [] });
|
|
1393
|
+
|
|
1394
|
+
expect(await stored(id)).toEqual({
|
|
1395
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1396
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1397
|
+
enabled: true,
|
|
1398
|
+
incidentSeverityIds: [],
|
|
1399
|
+
alertSeverityIds: [],
|
|
1400
|
+
});
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
test("changing only the channel leaves the severity join rows alone", async () => {
|
|
1404
|
+
const id: ObjectID = await callForCriticalAndMajor();
|
|
1405
|
+
|
|
1406
|
+
await updateRule(id, {
|
|
1407
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
expect(await stored(id)).toEqual({
|
|
1411
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1412
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1413
|
+
enabled: true,
|
|
1414
|
+
incidentSeverityIds: ids([critical, major]),
|
|
1415
|
+
alertSeverityIds: [],
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
await updateRule(id, { notificationChannel: null });
|
|
1419
|
+
|
|
1420
|
+
expect((await stored(id))?.notificationChannel).toBeNull();
|
|
1421
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, id)).toEqual(
|
|
1422
|
+
ids([critical, major]),
|
|
1423
|
+
);
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
test("alert severities sent to an incident rule are dropped and its incident severities stay", async () => {
|
|
1427
|
+
const id: ObjectID = await callForCriticalAndMajor();
|
|
1428
|
+
|
|
1429
|
+
await updateRule(id, { alertSeverities: asJson([criticalAlert]) });
|
|
1430
|
+
|
|
1431
|
+
expect(await stored(id)).toEqual({
|
|
1432
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1433
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1434
|
+
enabled: true,
|
|
1435
|
+
incidentSeverityIds: ids([critical, major]),
|
|
1436
|
+
alertSeverityIds: [],
|
|
1437
|
+
});
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
test("re-saving a rule's own scope is not a duplicate of itself", async () => {
|
|
1441
|
+
const id: ObjectID = await callForCriticalAndMajor();
|
|
1442
|
+
|
|
1443
|
+
await updateRule(id, {
|
|
1444
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1445
|
+
incidentSeverities: asJson([major, critical]),
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, id)).toEqual(
|
|
1449
|
+
ids([critical, major]),
|
|
1450
|
+
);
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
test("a change that makes the rule a duplicate of a sibling, or points it at a foreign severity, is refused and changes nothing", async () => {
|
|
1454
|
+
const id: ObjectID = await callForCriticalAndMajor();
|
|
1455
|
+
|
|
1456
|
+
await createRule({
|
|
1457
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1458
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1459
|
+
incidentSeverities: asStrings([critical]),
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
await expect(
|
|
1463
|
+
updateRule(id, { incidentSeverities: asJson([critical]) }),
|
|
1464
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
1465
|
+
|
|
1466
|
+
await expect(
|
|
1467
|
+
updateRule(id, { incidentSeverities: [upper(critical)] }),
|
|
1468
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
1469
|
+
|
|
1470
|
+
await expect(
|
|
1471
|
+
updateRule(id, {
|
|
1472
|
+
incidentSeverities: asJson([critical, otherProjectCritical]),
|
|
1473
|
+
}),
|
|
1474
|
+
).rejects.toThrow(FOREIGN_SEVERITY_MESSAGE);
|
|
1475
|
+
|
|
1476
|
+
expect(await stored(id)).toEqual({
|
|
1477
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1478
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1479
|
+
enabled: true,
|
|
1480
|
+
incidentSeverityIds: ids([critical, major]),
|
|
1481
|
+
alertSeverityIds: [],
|
|
1482
|
+
});
|
|
1483
|
+
});
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
describe("deleting", () => {
|
|
1487
|
+
test("deleting a severity removes its join row and keeps the rule", async () => {
|
|
1488
|
+
const id: ObjectID = await createRule({
|
|
1489
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1490
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1491
|
+
incidentSeverities: asStrings([critical, major]),
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
// What DatabaseService.deleteBy issues once a delete is permitted.
|
|
1495
|
+
await database.query(
|
|
1496
|
+
`DELETE FROM "${schema}"."IncidentSeverity" WHERE "_id" = $1`,
|
|
1497
|
+
[critical.toString()],
|
|
1498
|
+
);
|
|
1499
|
+
|
|
1500
|
+
expect(await stored(id)).toEqual({
|
|
1501
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1502
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1503
|
+
enabled: true,
|
|
1504
|
+
incidentSeverityIds: ids([major]),
|
|
1505
|
+
alertSeverityIds: [],
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
const rule: TeamComplianceSetting | null = await readRule(id);
|
|
1509
|
+
|
|
1510
|
+
expect(toReadSeverities(rule?.incidentSeverities)).toEqual([
|
|
1511
|
+
{
|
|
1512
|
+
id: major.toString(),
|
|
1513
|
+
name: "Major Incident",
|
|
1514
|
+
color: "#FFA500",
|
|
1515
|
+
order: 2,
|
|
1516
|
+
},
|
|
1517
|
+
]);
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
test("deleting an alert severity removes its join row too", async () => {
|
|
1521
|
+
const id: ObjectID = await createRule({
|
|
1522
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
1523
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1524
|
+
alertSeverities: asStrings([criticalAlert, warningAlert]),
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
await database.query(
|
|
1528
|
+
`DELETE FROM "${schema}"."AlertSeverity" WHERE "_id" = $1`,
|
|
1529
|
+
[warningAlert.toString()],
|
|
1530
|
+
);
|
|
1531
|
+
|
|
1532
|
+
expect(await joinRows(ALERT_JOIN_TABLE, id)).toEqual(
|
|
1533
|
+
ids([criticalAlert]),
|
|
1534
|
+
);
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
/*
|
|
1538
|
+
* The raw deletes above show the cascade itself. Through the severity
|
|
1539
|
+
* services - the only way the product deletes a severity - a rule the
|
|
1540
|
+
* cascade leaves with no severity is paused rather than left to read
|
|
1541
|
+
* as a rule for every severity.
|
|
1542
|
+
*/
|
|
1543
|
+
test("deleting a rule's only severity through the service pauses the rule; a rule with another severity left keeps it", async () => {
|
|
1544
|
+
const onlyCritical: ObjectID = await createRule({
|
|
1545
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1546
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1547
|
+
incidentSeverities: asStrings([critical]),
|
|
1548
|
+
});
|
|
1549
|
+
const episodeOnlyCritical: ObjectID = await createRule({
|
|
1550
|
+
ruleType: ComplianceRuleType.HasIncidentEpisodeOnCallRules,
|
|
1551
|
+
notificationChannel: ComplianceNotificationChannel.SMS,
|
|
1552
|
+
incidentSeverities: asStrings([critical]),
|
|
1553
|
+
});
|
|
1554
|
+
const criticalAndMajor: ObjectID = await createRule({
|
|
1555
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1556
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1557
|
+
incidentSeverities: asStrings([critical, major]),
|
|
1558
|
+
});
|
|
1559
|
+
const everySeverity: ObjectID = await createRule({
|
|
1560
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1561
|
+
notificationChannel: ComplianceNotificationChannel.Email,
|
|
1562
|
+
});
|
|
1563
|
+
const alertRule: ObjectID = await createRule({
|
|
1564
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
1565
|
+
alertSeverities: asStrings([criticalAlert]),
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
await IncidentSeverityService.deleteOneById({
|
|
1569
|
+
id: critical,
|
|
1570
|
+
props: ROOT,
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
expect(await stored(onlyCritical)).toEqual({
|
|
1574
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1575
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1576
|
+
enabled: false,
|
|
1577
|
+
incidentSeverityIds: [],
|
|
1578
|
+
alertSeverityIds: [],
|
|
1579
|
+
});
|
|
1580
|
+
expect(await storedOptions(onlyCritical)).toEqual(MARKED);
|
|
1581
|
+
expect((await stored(episodeOnlyCritical))?.enabled).toBe(false);
|
|
1582
|
+
expect(await storedOptions(episodeOnlyCritical)).toEqual(MARKED);
|
|
1583
|
+
expect(await storedOptions(criticalAndMajor)).toBeNull();
|
|
1584
|
+
expect(await storedOptions(everySeverity)).toBeNull();
|
|
1585
|
+
|
|
1586
|
+
expect(await stored(criticalAndMajor)).toEqual({
|
|
1587
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1588
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1589
|
+
enabled: true,
|
|
1590
|
+
incidentSeverityIds: ids([major]),
|
|
1591
|
+
alertSeverityIds: [],
|
|
1592
|
+
});
|
|
1593
|
+
expect((await stored(everySeverity))?.enabled).toBe(true);
|
|
1594
|
+
expect(await stored(alertRule)).toMatchObject({
|
|
1595
|
+
enabled: true,
|
|
1596
|
+
alertSeverityIds: ids([criticalAlert]),
|
|
1597
|
+
});
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
test("deleting an alert rule's only severity through the service pauses it too", async () => {
|
|
1601
|
+
const onlyWarning: ObjectID = await createRule({
|
|
1602
|
+
ruleType: ComplianceRuleType.HasAlertEpisodeOnCallRules,
|
|
1603
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1604
|
+
alertSeverities: asStrings([warningAlert]),
|
|
1605
|
+
});
|
|
1606
|
+
const incidentRule: ObjectID = await createRule({
|
|
1607
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1608
|
+
incidentSeverities: asStrings([critical]),
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
await AlertSeverityService.deleteOneById({
|
|
1612
|
+
id: warningAlert,
|
|
1613
|
+
props: ROOT,
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
expect(await stored(onlyWarning)).toMatchObject({
|
|
1617
|
+
enabled: false,
|
|
1618
|
+
alertSeverityIds: [],
|
|
1619
|
+
});
|
|
1620
|
+
expect((await stored(incidentRule))?.enabled).toBe(true);
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
test.each([
|
|
1624
|
+
["incident", false],
|
|
1625
|
+
["alert", true],
|
|
1626
|
+
])(
|
|
1627
|
+
"two %s severity deletes side by side - the two severities of one rule - pause and mark it",
|
|
1628
|
+
async (_label: string, isAlert: boolean) => {
|
|
1629
|
+
const both: ObjectID = isAlert
|
|
1630
|
+
? await createRule({
|
|
1631
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
1632
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1633
|
+
alertSeverities: asStrings([criticalAlert, warningAlert]),
|
|
1634
|
+
})
|
|
1635
|
+
: await createRule({
|
|
1636
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1637
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1638
|
+
incidentSeverities: asStrings([critical, major]),
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
// A rule with a severity neither delete touches.
|
|
1642
|
+
const keepsMinor: ObjectID = await createRule({
|
|
1643
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1644
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1645
|
+
incidentSeverities: asStrings([critical, minor]),
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
await deleteSideBySide(
|
|
1649
|
+
(isAlert ? [criticalAlert, warningAlert] : [critical, major]).map(
|
|
1650
|
+
(id: ObjectID): (() => Promise<unknown>) => {
|
|
1651
|
+
return (): Promise<unknown> => {
|
|
1652
|
+
return isAlert
|
|
1653
|
+
? AlertSeverityService.deleteOneById({
|
|
1654
|
+
id: id,
|
|
1655
|
+
props: ROOT,
|
|
1656
|
+
})
|
|
1657
|
+
: IncidentSeverityService.deleteOneById({
|
|
1658
|
+
id: id,
|
|
1659
|
+
props: ROOT,
|
|
1660
|
+
});
|
|
1661
|
+
};
|
|
1662
|
+
},
|
|
1663
|
+
),
|
|
1664
|
+
);
|
|
1665
|
+
|
|
1666
|
+
expect(await stored(both)).toMatchObject({
|
|
1667
|
+
enabled: false,
|
|
1668
|
+
incidentSeverityIds: [],
|
|
1669
|
+
alertSeverityIds: [],
|
|
1670
|
+
});
|
|
1671
|
+
expect(await storedOptions(both)).toEqual(MARKED);
|
|
1672
|
+
|
|
1673
|
+
expect(await stored(keepsMinor)).toMatchObject({
|
|
1674
|
+
enabled: true,
|
|
1675
|
+
incidentSeverityIds: isAlert
|
|
1676
|
+
? ids([critical, minor])
|
|
1677
|
+
: ids([minor]),
|
|
1678
|
+
});
|
|
1679
|
+
expect(await storedOptions(keepsMinor)).toBeNull();
|
|
1680
|
+
},
|
|
1681
|
+
);
|
|
1682
|
+
|
|
1683
|
+
test("a rule an admin had paused is marked too, so switching it on later cannot widen it", async () => {
|
|
1684
|
+
const paused: ObjectID = await createRule({
|
|
1685
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1686
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1687
|
+
incidentSeverities: asStrings([critical]),
|
|
1688
|
+
enabled: false,
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
await IncidentSeverityService.deleteOneById({
|
|
1692
|
+
id: critical,
|
|
1693
|
+
props: ROOT,
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
expect(await storedOptions(paused)).toEqual(MARKED);
|
|
1697
|
+
|
|
1698
|
+
await expect(updateRule(paused, { enabled: true })).rejects.toThrow(
|
|
1699
|
+
SEVERITIES_DELETED_ENABLE_MESSAGE,
|
|
1700
|
+
);
|
|
1701
|
+
expect((await stored(paused))?.enabled).toBe(false);
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
/*
|
|
1705
|
+
* The rule a delete emptied is stored with no severities - exactly how
|
|
1706
|
+
* the team's real "every severity" rule of the same type and channel
|
|
1707
|
+
* is stored. It must neither block that rule's edit form as a
|
|
1708
|
+
* duplicate, nor be switched back on as a second copy of it.
|
|
1709
|
+
*/
|
|
1710
|
+
test("a rule a delete emptied duplicates nothing, cannot be switched on as it is, and is re-scoped from its edit form", async () => {
|
|
1711
|
+
const everySeverity: ObjectID = await createRule({
|
|
1712
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1713
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1714
|
+
});
|
|
1715
|
+
const onlyCritical: ObjectID = await createRule({
|
|
1716
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1717
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1718
|
+
incidentSeverities: asStrings([critical]),
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
await IncidentSeverityService.deleteOneById({
|
|
1722
|
+
id: critical,
|
|
1723
|
+
props: ROOT,
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
// The every-severity rule's edit form sends its whole scope.
|
|
1727
|
+
await updateRule(everySeverity, {
|
|
1728
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1729
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1730
|
+
incidentSeverities: [],
|
|
1731
|
+
alertSeverities: [],
|
|
1732
|
+
enabled: false,
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
expect((await stored(everySeverity))?.enabled).toBe(false);
|
|
1736
|
+
|
|
1737
|
+
await expect(
|
|
1738
|
+
updateRule(onlyCritical, { enabled: true }),
|
|
1739
|
+
).rejects.toThrow(SEVERITIES_DELETED_ENABLE_MESSAGE);
|
|
1740
|
+
|
|
1741
|
+
expect((await stored(onlyCritical))?.enabled).toBe(false);
|
|
1742
|
+
expect(await storedOptions(onlyCritical)).toEqual(MARKED);
|
|
1743
|
+
|
|
1744
|
+
// The emptied rule's edit form: new severities, switched back on.
|
|
1745
|
+
await updateRule(onlyCritical, {
|
|
1746
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1747
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1748
|
+
incidentSeverities: asJson([major]),
|
|
1749
|
+
alertSeverities: [],
|
|
1750
|
+
enabled: true,
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
expect(await stored(onlyCritical)).toEqual({
|
|
1754
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1755
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1756
|
+
enabled: true,
|
|
1757
|
+
incidentSeverityIds: ids([major]),
|
|
1758
|
+
alertSeverityIds: [],
|
|
1759
|
+
});
|
|
1760
|
+
expect(await storedOptions(onlyCritical)).toBeNull();
|
|
1761
|
+
|
|
1762
|
+
// An ordinary rule again: it can now be switched off and on.
|
|
1763
|
+
await updateRule(onlyCritical, { enabled: false });
|
|
1764
|
+
await updateRule(onlyCritical, { enabled: true });
|
|
1765
|
+
expect((await stored(onlyCritical))?.enabled).toBe(true);
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
test("re-scoping an emptied rule keeps every other option it carries", async () => {
|
|
1769
|
+
const onlyCritical: ObjectID = await createRule({
|
|
1770
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1771
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1772
|
+
incidentSeverities: asStrings([critical]),
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
await database.query(
|
|
1776
|
+
`UPDATE "${schema}"."TeamComplianceSetting" SET "options" = $1 WHERE "_id" = $2`,
|
|
1777
|
+
[JSON.stringify({ note: "keep me" }), onlyCritical.toString()],
|
|
1778
|
+
);
|
|
1779
|
+
|
|
1780
|
+
await IncidentSeverityService.deleteOneById({
|
|
1781
|
+
id: critical,
|
|
1782
|
+
props: ROOT,
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
expect(await storedOptions(onlyCritical)).toEqual({
|
|
1786
|
+
note: "keep me",
|
|
1787
|
+
...MARKED,
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
await updateRule(onlyCritical, {
|
|
1791
|
+
incidentSeverities: asJson([minor]),
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
expect(await storedOptions(onlyCritical)).toEqual({ note: "keep me" });
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
test("a delete the database refuses - the severity is still in use - pauses nothing", async () => {
|
|
1798
|
+
const onlyCritical: ObjectID = await createRule({
|
|
1799
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1800
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1801
|
+
incidentSeverities: asStrings([critical]),
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
/*
|
|
1805
|
+
* Incidents reference their severity with ON DELETE NO ACTION, so a
|
|
1806
|
+
* severity any incident used cannot be deleted. A stand-in table
|
|
1807
|
+
* with the same kind of reference.
|
|
1808
|
+
*/
|
|
1809
|
+
await database.query(
|
|
1810
|
+
`CREATE TABLE "${schema}"."SeverityInUse" ("incidentSeverityId" uuid NOT NULL REFERENCES "${schema}"."IncidentSeverity"("_id"))`,
|
|
1811
|
+
);
|
|
1812
|
+
|
|
1813
|
+
try {
|
|
1814
|
+
await database.query(
|
|
1815
|
+
`INSERT INTO "${schema}"."SeverityInUse" ("incidentSeverityId") VALUES ($1)`,
|
|
1816
|
+
[critical.toString()],
|
|
1817
|
+
);
|
|
1818
|
+
|
|
1819
|
+
await expect(
|
|
1820
|
+
IncidentSeverityService.deleteOneById({
|
|
1821
|
+
id: critical,
|
|
1822
|
+
props: ROOT,
|
|
1823
|
+
}),
|
|
1824
|
+
).rejects.toThrow();
|
|
1825
|
+
} finally {
|
|
1826
|
+
await database.query(`DROP TABLE "${schema}"."SeverityInUse"`);
|
|
1827
|
+
failedQueries.failures = [];
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
expect(await stored(onlyCritical)).toEqual({
|
|
1831
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1832
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1833
|
+
enabled: true,
|
|
1834
|
+
incidentSeverityIds: ids([critical]),
|
|
1835
|
+
alertSeverityIds: [],
|
|
1836
|
+
});
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
test("deleting a rule through the service removes its join rows and nobody else's", async () => {
|
|
1840
|
+
const id: ObjectID = await createRule({
|
|
1841
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1842
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1843
|
+
incidentSeverities: asStrings([critical, major]),
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
const sibling: ObjectID = await createRule({
|
|
1847
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1848
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1849
|
+
incidentSeverities: asStrings([critical]),
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
await TeamComplianceSettingService.deleteOneById({
|
|
1853
|
+
id: id,
|
|
1854
|
+
props: ROOT,
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
expect(await stored(id)).toBeNull();
|
|
1858
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, id)).toEqual([]);
|
|
1859
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, sibling)).toEqual(
|
|
1860
|
+
ids([critical]),
|
|
1861
|
+
);
|
|
1862
|
+
});
|
|
1863
|
+
});
|
|
1864
|
+
|
|
1865
|
+
/*
|
|
1866
|
+
* The dashboard's path: a signed-in project admin, through the permission
|
|
1867
|
+
* layer, with the request's project as the tenant.
|
|
1868
|
+
*/
|
|
1869
|
+
describe("as a signed-in project admin", () => {
|
|
1870
|
+
let licenceCheck: jest.SpyInstance;
|
|
1871
|
+
|
|
1872
|
+
beforeAll(() => {
|
|
1873
|
+
licenceCheck = jest
|
|
1874
|
+
.spyOn(EnterpriseEdition, "assertFeatureAvailableSync")
|
|
1875
|
+
.mockReturnValue(undefined);
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
afterAll(() => {
|
|
1879
|
+
licenceCheck.mockRestore();
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
test("creates, reads back and rescopes a rule of their project", async () => {
|
|
1883
|
+
const admin: DatabaseCommonInteractionProps = memberProps(projectId, [
|
|
1884
|
+
Permission.ProjectAdmin,
|
|
1885
|
+
]);
|
|
1886
|
+
|
|
1887
|
+
const id: ObjectID = await createRule(
|
|
1888
|
+
{
|
|
1889
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1890
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1891
|
+
incidentSeverities: asIncidentModels([critical]),
|
|
1892
|
+
},
|
|
1893
|
+
admin,
|
|
1894
|
+
);
|
|
1895
|
+
|
|
1896
|
+
expect(await stored(id)).toEqual({
|
|
1897
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1898
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1899
|
+
enabled: true,
|
|
1900
|
+
incidentSeverityIds: ids([critical]),
|
|
1901
|
+
alertSeverityIds: [],
|
|
1902
|
+
});
|
|
1903
|
+
|
|
1904
|
+
const rule: TeamComplianceSetting | null = await readRule(id, admin);
|
|
1905
|
+
|
|
1906
|
+
expect(toReadSeverities(rule?.incidentSeverities)).toEqual([
|
|
1907
|
+
{
|
|
1908
|
+
id: critical.toString(),
|
|
1909
|
+
name: "Critical Incident",
|
|
1910
|
+
color: "#FF0000",
|
|
1911
|
+
order: 1,
|
|
1912
|
+
},
|
|
1913
|
+
]);
|
|
1914
|
+
|
|
1915
|
+
await updateRule(
|
|
1916
|
+
id,
|
|
1917
|
+
{ incidentSeverities: asJson([critical, major]) },
|
|
1918
|
+
admin,
|
|
1919
|
+
);
|
|
1920
|
+
|
|
1921
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, id)).toEqual(
|
|
1922
|
+
ids([critical, major]),
|
|
1923
|
+
);
|
|
1924
|
+
|
|
1925
|
+
await expect(
|
|
1926
|
+
createRule(
|
|
1927
|
+
{
|
|
1928
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1929
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1930
|
+
incidentSeverities: asJson([major, critical]),
|
|
1931
|
+
},
|
|
1932
|
+
admin,
|
|
1933
|
+
),
|
|
1934
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
test("a signed-in delete by a project admin pauses the rule too, and still re-ranks the severities that remain", async () => {
|
|
1938
|
+
const admin: DatabaseCommonInteractionProps = memberProps(projectId, [
|
|
1939
|
+
Permission.ProjectAdmin,
|
|
1940
|
+
]);
|
|
1941
|
+
|
|
1942
|
+
const onlyCritical: ObjectID = await createRule({
|
|
1943
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1944
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1945
|
+
incidentSeverities: asStrings([critical]),
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
await IncidentSeverityService.deleteOneById({
|
|
1949
|
+
id: critical,
|
|
1950
|
+
props: admin,
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
expect((await stored(onlyCritical))?.enabled).toBe(false);
|
|
1954
|
+
|
|
1955
|
+
const orders: Array<SqlRow> = await database.query(
|
|
1956
|
+
`SELECT "_id", "order" FROM "${schema}"."IncidentSeverity" WHERE "projectId" = $1 ORDER BY "order"`,
|
|
1957
|
+
[projectId.toString()],
|
|
1958
|
+
);
|
|
1959
|
+
|
|
1960
|
+
expect(orders).toEqual([
|
|
1961
|
+
{ _id: major.toString(), order: 1 },
|
|
1962
|
+
{ _id: minor.toString(), order: 2 },
|
|
1963
|
+
]);
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
/*
|
|
1967
|
+
* The hooks read as root in the project the request names, and
|
|
1968
|
+
* DatabaseService only checks the caller's permission after them. They
|
|
1969
|
+
* check it first, so a caller with no say over the project's rules is
|
|
1970
|
+
* refused before any of their answers - "not a severity of this
|
|
1971
|
+
* project", "this team already has that rule" - can describe it.
|
|
1972
|
+
*/
|
|
1973
|
+
test("a caller who may not change the project's rules learns nothing about its severities or rules", async () => {
|
|
1974
|
+
await createRule({
|
|
1975
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1976
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1977
|
+
incidentSeverities: asStrings([critical]),
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
const outsiders: Array<DatabaseCommonInteractionProps> = [
|
|
1981
|
+
// Signed in elsewhere, naming this project.
|
|
1982
|
+
{
|
|
1983
|
+
...memberProps(otherProjectId, [Permission.ProjectAdmin]),
|
|
1984
|
+
tenantId: projectId,
|
|
1985
|
+
},
|
|
1986
|
+
// A member who may only read the project's teams.
|
|
1987
|
+
memberProps(projectId, [
|
|
1988
|
+
Permission.ProjectMember,
|
|
1989
|
+
Permission.ReadProjectTeam,
|
|
1990
|
+
]),
|
|
1991
|
+
];
|
|
1992
|
+
|
|
1993
|
+
for (const props of outsiders) {
|
|
1994
|
+
// A foreign severity, and an exact duplicate: both refused alike.
|
|
1995
|
+
for (const severity of [otherProjectCritical, critical]) {
|
|
1996
|
+
await expect(
|
|
1997
|
+
createRule(
|
|
1998
|
+
{
|
|
1999
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2000
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
2001
|
+
incidentSeverities: asStrings([severity]),
|
|
2002
|
+
},
|
|
2003
|
+
props,
|
|
2004
|
+
),
|
|
2005
|
+
).rejects.toThrow(NotAuthorizedException);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
expect(await countRules()).toBe(1);
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2012
|
+
test("a team editor creates and rescopes a rule; cleared options pass the permission check", async () => {
|
|
2013
|
+
const editor: DatabaseCommonInteractionProps = memberProps(projectId, [
|
|
2014
|
+
Permission.ReadProjectTeam,
|
|
2015
|
+
Permission.EditProjectTeam,
|
|
2016
|
+
]);
|
|
2017
|
+
|
|
2018
|
+
// A method rule sent with options it does not take: cleared, allowed.
|
|
2019
|
+
const method: ObjectID = await createRule(
|
|
2020
|
+
{
|
|
2021
|
+
ruleType: ComplianceRuleType.HasNotificationCallMethod,
|
|
2022
|
+
notificationChannel: ComplianceNotificationChannel.SMS,
|
|
2023
|
+
incidentSeverities: asStrings([critical]),
|
|
2024
|
+
},
|
|
2025
|
+
editor,
|
|
2026
|
+
);
|
|
2027
|
+
|
|
2028
|
+
expect(await stored(method)).toEqual({
|
|
2029
|
+
ruleType: ComplianceRuleType.HasNotificationCallMethod,
|
|
2030
|
+
notificationChannel: null,
|
|
2031
|
+
enabled: true,
|
|
2032
|
+
incidentSeverityIds: [],
|
|
2033
|
+
alertSeverityIds: [],
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
const id: ObjectID = await createRule(
|
|
2037
|
+
{
|
|
2038
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2039
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
2040
|
+
incidentSeverities: asIncidentModels([critical]),
|
|
2041
|
+
alertSeverities: asStrings([criticalAlert]),
|
|
2042
|
+
},
|
|
2043
|
+
editor,
|
|
2044
|
+
);
|
|
2045
|
+
|
|
2046
|
+
await updateRule(
|
|
2047
|
+
id,
|
|
2048
|
+
{ incidentSeverities: [upper(major), { _id: critical.toString() }] },
|
|
2049
|
+
editor,
|
|
2050
|
+
);
|
|
2051
|
+
|
|
2052
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, id)).toEqual(
|
|
2053
|
+
ids([critical, major]),
|
|
2054
|
+
);
|
|
2055
|
+
|
|
2056
|
+
await updateRule(
|
|
2057
|
+
id,
|
|
2058
|
+
{ ruleType: ComplianceRuleType.HasNotificationPushMethod },
|
|
2059
|
+
editor,
|
|
2060
|
+
);
|
|
2061
|
+
|
|
2062
|
+
expect(await stored(id)).toEqual({
|
|
2063
|
+
ruleType: ComplianceRuleType.HasNotificationPushMethod,
|
|
2064
|
+
notificationChannel: null,
|
|
2065
|
+
enabled: true,
|
|
2066
|
+
incidentSeverityIds: [],
|
|
2067
|
+
alertSeverityIds: [],
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
// Edit Teams on its own creates, as it did before.
|
|
2071
|
+
await createRule(
|
|
2072
|
+
{ ruleType: ComplianceRuleType.HasNotificationEmailMethod },
|
|
2073
|
+
memberProps(projectId, [Permission.EditProjectTeam]),
|
|
2074
|
+
);
|
|
2075
|
+
|
|
2076
|
+
expect(await countRules()).toBe(3);
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
/*
|
|
2080
|
+
* A row whose stored rule type this build does not recognise - legacy
|
|
2081
|
+
* data, or written by a newer build before a rollback - still gets
|
|
2082
|
+
* whatever severity list an update sends written to it, so the list is
|
|
2083
|
+
* checked against the row's project by its own kind.
|
|
2084
|
+
*/
|
|
2085
|
+
test("cannot link another project's severity to a rule of an unrecognised type", async () => {
|
|
2086
|
+
const admin: DatabaseCommonInteractionProps = memberProps(projectId, [
|
|
2087
|
+
Permission.ProjectAdmin,
|
|
2088
|
+
]);
|
|
2089
|
+
|
|
2090
|
+
const id: ObjectID = await createRule({
|
|
2091
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2092
|
+
});
|
|
2093
|
+
|
|
2094
|
+
await database.query(
|
|
2095
|
+
`UPDATE "${schema}"."TeamComplianceSetting" SET "ruleType" = 'LegacyRule' WHERE "_id" = $1`,
|
|
2096
|
+
[id.toString()],
|
|
2097
|
+
);
|
|
2098
|
+
|
|
2099
|
+
await expect(
|
|
2100
|
+
updateRule(
|
|
2101
|
+
id,
|
|
2102
|
+
{ incidentSeverities: asJson([otherProjectCritical]) },
|
|
2103
|
+
admin,
|
|
2104
|
+
),
|
|
2105
|
+
).rejects.toThrow(FOREIGN_SEVERITY_MESSAGE);
|
|
2106
|
+
|
|
2107
|
+
await expect(
|
|
2108
|
+
updateRule(
|
|
2109
|
+
id,
|
|
2110
|
+
{ alertSeverities: asJson([otherProjectCriticalAlert]) },
|
|
2111
|
+
admin,
|
|
2112
|
+
),
|
|
2113
|
+
).rejects.toThrow(FOREIGN_ALERT_SEVERITY_MESSAGE);
|
|
2114
|
+
|
|
2115
|
+
expect(await countJoinRows()).toBe(0);
|
|
2116
|
+
|
|
2117
|
+
// This project's own severities are still accepted.
|
|
2118
|
+
await updateRule(id, { incidentSeverities: asJson([critical]) }, admin);
|
|
2119
|
+
|
|
2120
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, id)).toEqual(
|
|
2121
|
+
ids([critical]),
|
|
2122
|
+
);
|
|
2123
|
+
});
|
|
2124
|
+
|
|
2125
|
+
test("a project admin cannot switch a rule a delete emptied back on, but can re-scope it", async () => {
|
|
2126
|
+
const admin: DatabaseCommonInteractionProps = memberProps(projectId, [
|
|
2127
|
+
Permission.ProjectAdmin,
|
|
2128
|
+
]);
|
|
2129
|
+
|
|
2130
|
+
const onlyCritical: ObjectID = await createRule({
|
|
2131
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2132
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
2133
|
+
incidentSeverities: asStrings([critical]),
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
await IncidentSeverityService.deleteOneById({
|
|
2137
|
+
id: critical,
|
|
2138
|
+
props: admin,
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
await expect(
|
|
2142
|
+
updateRule(onlyCritical, { enabled: true }, admin),
|
|
2143
|
+
).rejects.toThrow(SEVERITIES_DELETED_ENABLE_MESSAGE);
|
|
2144
|
+
|
|
2145
|
+
// A caller with no say in the project is refused before any read.
|
|
2146
|
+
await expect(
|
|
2147
|
+
updateRule(
|
|
2148
|
+
onlyCritical,
|
|
2149
|
+
{ enabled: true },
|
|
2150
|
+
{
|
|
2151
|
+
...memberProps(otherProjectId, [Permission.ProjectAdmin]),
|
|
2152
|
+
tenantId: projectId,
|
|
2153
|
+
},
|
|
2154
|
+
),
|
|
2155
|
+
).rejects.toThrow(NotAuthorizedException);
|
|
2156
|
+
|
|
2157
|
+
await updateRule(
|
|
2158
|
+
onlyCritical,
|
|
2159
|
+
{ incidentSeverities: asJson([major]), enabled: true },
|
|
2160
|
+
admin,
|
|
2161
|
+
);
|
|
2162
|
+
|
|
2163
|
+
expect(await stored(onlyCritical)).toMatchObject({
|
|
2164
|
+
enabled: true,
|
|
2165
|
+
incidentSeverityIds: ids([major]),
|
|
2166
|
+
});
|
|
2167
|
+
expect(await storedOptions(onlyCritical)).toBeNull();
|
|
2168
|
+
});
|
|
2169
|
+
|
|
2170
|
+
test("cannot rescope another project's rule", async () => {
|
|
2171
|
+
const id: ObjectID = await createRule({
|
|
2172
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2173
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
2174
|
+
incidentSeverities: asStrings([critical]),
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
const otherAdmin: DatabaseCommonInteractionProps = memberProps(
|
|
2178
|
+
otherProjectId,
|
|
2179
|
+
[Permission.ProjectAdmin],
|
|
2180
|
+
);
|
|
2181
|
+
|
|
2182
|
+
await updateRule(
|
|
2183
|
+
id,
|
|
2184
|
+
{ incidentSeverities: asJson([otherProjectCritical]) },
|
|
2185
|
+
otherAdmin,
|
|
2186
|
+
);
|
|
2187
|
+
|
|
2188
|
+
expect(await joinRows(INCIDENT_JOIN_TABLE, id)).toEqual(
|
|
2189
|
+
ids([critical]),
|
|
2190
|
+
);
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
/*
|
|
2194
|
+
* The request's project is stamped on the payload before the hooks run
|
|
2195
|
+
* (DatabaseService._onBeforeCreate), so the severity check is made
|
|
2196
|
+
* against the admin's own project whatever projectId the payload names.
|
|
2197
|
+
*/
|
|
2198
|
+
test("cannot attach another project's severity by naming that project in the payload", async () => {
|
|
2199
|
+
const admin: DatabaseCommonInteractionProps = memberProps(projectId, [
|
|
2200
|
+
Permission.ProjectAdmin,
|
|
2201
|
+
]);
|
|
2202
|
+
|
|
2203
|
+
await expect(
|
|
2204
|
+
createRule(
|
|
2205
|
+
{
|
|
2206
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2207
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
2208
|
+
incidentSeverities: asIncidentModels([otherProjectCritical]),
|
|
2209
|
+
projectId: otherProjectId,
|
|
2210
|
+
},
|
|
2211
|
+
admin,
|
|
2212
|
+
),
|
|
2213
|
+
).rejects.toThrow(FOREIGN_SEVERITY_MESSAGE);
|
|
2214
|
+
|
|
2215
|
+
expect(await countRules()).toBe(0);
|
|
2216
|
+
expect(await countJoinRows()).toBe(0);
|
|
2217
|
+
|
|
2218
|
+
const id: ObjectID = await createRule(
|
|
2219
|
+
{
|
|
2220
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2221
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
2222
|
+
incidentSeverities: asIncidentModels([critical]),
|
|
2223
|
+
projectId: otherProjectId,
|
|
2224
|
+
},
|
|
2225
|
+
admin,
|
|
2226
|
+
);
|
|
2227
|
+
|
|
2228
|
+
const rows: Array<SqlRow> = await database.query(
|
|
2229
|
+
`SELECT "projectId" FROM "${schema}"."TeamComplianceSetting" WHERE "_id" = $1`,
|
|
2230
|
+
[id.toString()],
|
|
2231
|
+
);
|
|
2232
|
+
|
|
2233
|
+
expect(rows).toEqual([{ projectId: projectId.toString() }]);
|
|
2234
|
+
});
|
|
2235
|
+
});
|
|
2236
|
+
},
|
|
2237
|
+
);
|