@oneuptime/common 14.0.7 → 14.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/Incident.ts +267 -1
- package/Models/DatabaseModels/IncidentCustomField.ts +202 -0
- package/Models/DatabaseModels/IncidentEpisode.ts +30 -0
- package/Models/DatabaseModels/IncidentTemplate.ts +126 -1
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
- package/Models/DatabaseModels/Project.ts +55 -5
- package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
- package/Models/DatabaseModels/RunnerJob.ts +75 -0
- package/Models/DatabaseModels/StatusPage.ts +55 -0
- package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
- package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
- 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 +207 -25
- package/Server/API/IncidentAPI.ts +162 -0
- package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
- package/Server/API/MicrosoftTeamsAPI.ts +93 -14
- package/Server/API/ProxmoxResourceAPI.ts +244 -14
- package/Server/API/StatusPageAPI.ts +487 -171
- package/Server/API/StatusPageSubscriberAPI.ts +63 -15
- package/Server/Infrastructure/GlobalCache.ts +32 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -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/Index.ts +20 -0
- package/Server/Infrastructure/Semaphore.ts +10 -0
- package/Server/Middleware/IdentityRateLimit.ts +108 -3
- package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
- package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
- package/Server/Middleware/TelemetryIngest.ts +41 -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/AutoRemediationRuleService.ts +1 -1
- package/Server/Services/DatabaseService.ts +93 -10
- package/Server/Services/IncidentCustomFieldService.ts +276 -1
- package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
- package/Server/Services/IncidentEpisodeService.ts +15 -0
- package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
- package/Server/Services/IncidentPublicNoteService.ts +30 -1
- package/Server/Services/IncidentService.ts +1788 -142
- package/Server/Services/IncidentSeverityService.ts +66 -15
- package/Server/Services/IncidentStateTimelineService.ts +13 -0
- package/Server/Services/IncidentTemplateService.ts +109 -0
- 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/ProjectService.ts +32 -0
- package/Server/Services/ProxmoxResourceService.ts +322 -5
- package/Server/Services/RunnerJobService.ts +241 -104
- package/Server/Services/RunnerService.ts +1 -1
- package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
- package/Server/Services/ScheduledMaintenanceService.ts +805 -19
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
- package/Server/Services/SmsService.ts +10 -0
- package/Server/Services/StatusPageService.ts +65 -8
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
- package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
- package/Server/Services/StatusPageSubscriberService.ts +1161 -64
- package/Server/Services/TeamComplianceSettingService.ts +1125 -9
- package/Server/Types/Markdown.ts +153 -6
- package/Server/Types/Workflow/Components/Email.ts +61 -2
- 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 +37 -15
- package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +67 -24
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +48 -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 +6 -1
- package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
- package/Server/Utils/DataSource/EgressGuard.ts +66 -21
- package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
- package/Server/Utils/SSRFProtection.ts +76 -16
- package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
- package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
- package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
- package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
- package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
- package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
- package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
- package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
- package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
- package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
- package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
- package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
- package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
- package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
- package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
- package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
- package/Server/Utils/StatusPageResource.ts +51 -20
- 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/VM/VMAPI.ts +1 -0
- package/Server/Utils/VM/VMRunner.ts +10 -0
- package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +684 -47
- package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
- package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
- package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
- package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
- package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
- package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -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 +91 -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/CustomFieldsSettingsIncidentOptions.test.tsx +357 -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/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 +778 -2
- package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
- package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +828 -17
- 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/IncidentChangeStateNotifyDefault.test.tsx +91 -1
- package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
- package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
- package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
- package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
- package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
- package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
- package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
- package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
- package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
- package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
- package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -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/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/PublicNotePagesNotifyDefault.test.tsx +113 -1
- package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
- package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
- package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
- package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
- 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/SubscriberAudienceSummary.test.tsx +512 -0
- package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
- package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
- package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
- package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
- package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -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/SensitiveUrlTokenBootstrap.test.ts +64 -3
- package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
- package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
- package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
- package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
- package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
- package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
- package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -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/ExpiredSessionRouteGuards.test.ts +7 -0
- package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -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/StatusPageIncidentScope.test.ts +1868 -0
- package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
- package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
- package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
- package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
- package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
- package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
- package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -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/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
- package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
- package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
- package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
- package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
- package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +61 -4
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +98 -2
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +105 -2
- package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
- package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -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/AutoRemediationRuleCommandRunners.test.ts +2 -1
- package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
- package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
- package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
- package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
- package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
- package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
- package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
- package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
- package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
- package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
- package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
- package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
- package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
- package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
- package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -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/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/PublicNoteEditFeedEventType.test.ts +213 -0
- package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
- package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +48 -15
- package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
- package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
- 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/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
- package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
- package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
- package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
- package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
- package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
- package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
- package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
- package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
- package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
- package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -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/KubectlInvestigationToolkit.test.ts +70 -0
- package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
- 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/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/RemediationKubectlCopyParity.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -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/CustomField/CustomFieldRename.test.ts +586 -0
- package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
- package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
- package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
- package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
- package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
- package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
- package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
- package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
- package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
- package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
- package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
- package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
- package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
- package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
- package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
- package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
- 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 +45 -3
- package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
- package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
- package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
- package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
- package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
- package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
- package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
- package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
- package/Tests/Types/SafeHtml.test.ts +102 -0
- package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
- package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
- package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
- package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
- package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
- package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
- package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
- package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
- 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/CustomFields/CustomFieldFormFields.test.ts +432 -0
- package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -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/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
- package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -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/CustomFieldColumns.test.tsx +22 -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/Monitor/NetworkPathView.test.tsx +289 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
- package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
- 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/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
- package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
- package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
- package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
- package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
- package/Types/AI/AIRunType.ts +9 -4
- package/Types/AI/InvestigationNotStartedReason.ts +6 -0
- package/Types/AutoRemediation/AiRemediationCommandPlan.ts +14 -16
- package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
- package/Types/CustomField/CustomFieldOrder.ts +55 -0
- package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
- package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
- package/Types/CustomField/CustomFieldType.ts +14 -0
- package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
- package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
- package/Types/CustomField/CustomFieldVariableKey.ts +211 -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/SafeHtml.ts +93 -0
- package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
- package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
- package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
- package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
- package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
- package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
- package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
- package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
- package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
- package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
- package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
- package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
- 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 +28 -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/CustomFields/CustomFieldFormFields.ts +317 -0
- package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
- package/UI/Components/Forms/Fields/FormField.tsx +10 -0
- package/UI/Components/Forms/ModelForm.tsx +70 -8
- package/UI/Components/Forms/Validation.ts +1 -1
- 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/ModelFormModal/ModelFormModal.tsx +2 -4
- package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
- package/UI/Components/ModelTable/BaseModelTable.tsx +21 -9
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
- package/UI/Components/ModelTable/ModelTable.tsx +6 -7
- 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/UI/Utils/SensitiveUrlToken.ts +10 -5
- package/Utils/API.ts +247 -2
- package/Utils/AiRemediation/CommandPolicy.ts +1 -1
- package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
- package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
- package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
- package/Utils/Markdown/MarkdownEscape.ts +65 -0
- package/Utils/ProbeNetworkFailureUtil.ts +673 -0
- package/build/dist/Models/DatabaseModels/Incident.js +267 -1
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -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/Project.js +56 -5
- package/build/dist/Models/DatabaseModels/Project.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/RunnerJob.js +74 -0
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.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/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 +144 -24
- package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
- package/build/dist/Server/API/IncidentAPI.js +100 -1
- package/build/dist/Server/API/IncidentAPI.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/StatusPageAPI.js +417 -176
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
- package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
- 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/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
- 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/Index.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
- package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
- package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
- package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
- package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
- package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
- package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
- package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
- package/build/dist/Server/Middleware/TelemetryIngest.js +34 -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/AutoRemediationRuleService.js +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +56 -7
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
- package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
- package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +1251 -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/IncidentStateTimelineService.js +12 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
- package/build/dist/Server/Services/IncidentTemplateService.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/ProjectService.js +22 -0
- package/build/dist/Server/Services/ProjectService.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/RunnerJobService.js +178 -92
- 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/ScheduledMaintenancePublicNoteService.js +26 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +582 -20
- 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/SmsService.js +2 -22
- package/build/dist/Server/Services/SmsService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +47 -9
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
- package/build/dist/Server/Services/StatusPageSubscriberService.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/Types/Markdown.js +110 -6
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
- package/build/dist/Server/Types/Workflow/Components/Email.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 +29 -15
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +56 -20
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +39 -5
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.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 +4 -2
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
- package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
- package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
- package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
- package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/SSRFProtection.js +48 -6
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
- package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
- package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
- package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPageResource.js +39 -7
- package/build/dist/Server/Utils/StatusPageResource.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/VM/VMAPI.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +505 -42
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/AI/AIRunType.js +9 -4
- package/build/dist/Types/AI/AIRunType.js.map +1 -1
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +10 -16
- 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/CustomField/CustomFieldOrder.js +36 -0
- package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
- package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
- package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
- package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
- package/build/dist/Types/CustomField/CustomFieldVariableKey.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/SafeHtml.js +84 -0
- package/build/dist/Types/SafeHtml.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
- package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
- package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
- package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
- package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
- package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
- package/build/dist/Types/StatusPage/SubscriberUpdateNotification.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 +25 -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/CustomFields/CustomFieldFormFields.js +169 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.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/Forms/ModelForm.js +39 -5
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +1 -1
- package/build/dist/UI/Components/Forms/Validation.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/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -2
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.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/UI/Utils/SensitiveUrlToken.js +10 -5
- package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
- package/build/dist/Utils/API.js +177 -1
- package/build/dist/Utils/API.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/Dashboard/DashboardTimeRangeZoom.js +27 -1
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
- package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
- package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
- package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
- package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
- package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,191 +1,4041 @@
|
|
|
1
|
-
import TeamComplianceSettingService
|
|
1
|
+
import TeamComplianceSettingService, {
|
|
2
|
+
ComplianceRuleScope,
|
|
3
|
+
DeletedSeverity,
|
|
4
|
+
DUPLICATE_COMPLIANCE_RULE_MESSAGE,
|
|
5
|
+
OPTIONS_DIFFER_MESSAGE,
|
|
6
|
+
SEVERITIES_DELETED_ENABLE_MESSAGE,
|
|
7
|
+
SEVERITIES_DELETED_OPTION,
|
|
8
|
+
TeamComplianceSettingService as TeamComplianceSettingServiceClass,
|
|
9
|
+
} from "../../../Server/Services/TeamComplianceSettingService";
|
|
10
|
+
import EnterpriseEdition from "../../../Server/Enterprise/EnterpriseEdition";
|
|
11
|
+
import AlertSeverityService from "../../../Server/Services/AlertSeverityService";
|
|
12
|
+
import IncidentSeverityService from "../../../Server/Services/IncidentSeverityService";
|
|
2
13
|
import CreateBy from "../../../Server/Types/Database/CreateBy";
|
|
3
|
-
import { OnCreate } from "../../../Server/Types/Database/Hooks";
|
|
14
|
+
import { OnCreate, OnUpdate } from "../../../Server/Types/Database/Hooks";
|
|
15
|
+
import ModelPermission from "../../../Server/Types/Database/Permissions/Index";
|
|
16
|
+
import UpdateBy from "../../../Server/Types/Database/UpdateBy";
|
|
17
|
+
import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
|
|
18
|
+
import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
|
|
4
19
|
import TeamComplianceSetting from "../../../Models/DatabaseModels/TeamComplianceSetting";
|
|
20
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
21
|
+
import Includes from "../../../Types/BaseDatabase/Includes";
|
|
22
|
+
import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
|
|
5
23
|
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
24
|
+
import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
|
|
25
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
6
26
|
import ObjectID from "../../../Types/ObjectID";
|
|
7
|
-
import
|
|
27
|
+
import Permission, {
|
|
28
|
+
UserTenantAccessPermission,
|
|
29
|
+
} from "../../../Types/Permission";
|
|
30
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
31
|
+
import ComplianceNotificationChannel from "../../../Types/Team/ComplianceNotificationChannel";
|
|
32
|
+
import ComplianceRule, {
|
|
33
|
+
COMPLIANCE_RULE_DEFINITIONS,
|
|
34
|
+
ComplianceRuleDefinition,
|
|
35
|
+
ComplianceSeverityKind,
|
|
36
|
+
} from "../../../Types/Team/ComplianceRule";
|
|
37
|
+
import ComplianceRuleType from "../../../Types/Team/ComplianceRuleType";
|
|
38
|
+
import UserType from "../../../Types/UserType";
|
|
39
|
+
import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
|
|
40
|
+
import { FindOperator } from "typeorm";
|
|
8
41
|
|
|
9
42
|
/*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* (
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
43
|
+
* TeamComplianceSettingService guards what a team compliance rule may say.
|
|
44
|
+
*
|
|
45
|
+
* A rule is a type (ComplianceRuleType), optionally a channel ("Call") and a
|
|
46
|
+
* severity scope ("Critical"). One team may hold several rules of one type -
|
|
47
|
+
* "Call for Critical incidents" and "Push for Critical incidents" are both
|
|
48
|
+
* HasIncidentOnCallRules - so there is no unique index any more; the service
|
|
49
|
+
* refuses EXACT duplicates instead (same type, channel and severity set).
|
|
50
|
+
*
|
|
51
|
+
* These tests pin, against an in-memory team of stored rules and project
|
|
52
|
+
* severities:
|
|
53
|
+
*
|
|
54
|
+
* - create refuses unknown rule types and channels, and anything without a
|
|
55
|
+
* project or team, before reading anything;
|
|
56
|
+
* - options a rule type does not use are cleared, per kind, so nothing is
|
|
57
|
+
* stored that looks like part of a rule without being checked;
|
|
58
|
+
* - selected severities must exist in the rule's own project, of the rule's
|
|
59
|
+
* own kind - checked in one count, as root, and never for a stray option;
|
|
60
|
+
* - duplicates are judged on content (order, repeats and id case do not
|
|
61
|
+
* matter), legacy "no channel, every severity" rows included, while a
|
|
62
|
+
* different channel, severity set, rule type or team is a different rule;
|
|
63
|
+
* - an update that only switches a rule off reads nothing at all; one that
|
|
64
|
+
* switches it on reads only whether a severity delete left it with
|
|
65
|
+
* nothing to check, and refuses that;
|
|
66
|
+
* - a rule a severity delete emptied (options.severitiesDeleted) duplicates
|
|
67
|
+
* nothing, and any re-scope clears the mark in the same write;
|
|
68
|
+
* - an update that changes the scope re-validates it, compares against
|
|
69
|
+
* siblings excluding the row itself, reads only inside the caller's
|
|
70
|
+
* project, and a rule type change clears the options the new type does not
|
|
71
|
+
* use, including ones already stored on the row;
|
|
72
|
+
* - the static scope helpers the ee server can reuse (getIds, getScope,
|
|
73
|
+
* isSameScope).
|
|
74
|
+
*
|
|
75
|
+
* The protected hooks are called directly, as the other service tests do; a
|
|
76
|
+
* last block drives updateOneById - the path the API takes - to show that what
|
|
77
|
+
* the hook normalises is what reaches the write.
|
|
78
|
+
*
|
|
79
|
+
* The caller is a signed-in team editor (EditProjectTeam) of the project
|
|
80
|
+
* unless a test says otherwise: the hooks check the caller's permission
|
|
81
|
+
* before they read anything, so a caller with no say in the project learns
|
|
82
|
+
* nothing about its severities or rules from what they refuse.
|
|
17
83
|
*/
|
|
18
84
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
const TEAM_ID: ObjectID = new ObjectID("11111111-1111-4111-8111-111111111111");
|
|
24
|
-
const OTHER_TEAM_ID: ObjectID = new ObjectID(
|
|
85
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
86
|
+
"11111111-1111-4111-8111-111111111111",
|
|
87
|
+
);
|
|
88
|
+
const OTHER_PROJECT_ID: ObjectID = new ObjectID(
|
|
25
89
|
"22222222-2222-4222-8222-222222222222",
|
|
26
90
|
);
|
|
27
|
-
const
|
|
91
|
+
const TEAM_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
|
|
92
|
+
const OTHER_TEAM_ID: ObjectID = new ObjectID(
|
|
93
|
+
"44444444-4444-4444-8444-444444444444",
|
|
94
|
+
);
|
|
95
|
+
const USER_ID: ObjectID = new ObjectID("55555555-5555-4555-8555-555555555555");
|
|
28
96
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
97
|
+
const SETTING_ID: string = "66666666-6666-4666-8666-000000000001";
|
|
98
|
+
const SIBLING_SETTING_ID: string = "66666666-6666-4666-8666-000000000002";
|
|
99
|
+
const OTHER_PROJECT_SETTING_ID: string = "66666666-6666-4666-8666-000000000003";
|
|
100
|
+
|
|
101
|
+
const CRITICAL_INCIDENT: string = "c1c1c1c1-0000-4000-8000-000000000001";
|
|
102
|
+
const MAJOR_INCIDENT: string = "c1c1c1c1-0000-4000-8000-000000000002";
|
|
103
|
+
const MINOR_INCIDENT: string = "c1c1c1c1-0000-4000-8000-000000000003";
|
|
104
|
+
const FOREIGN_INCIDENT: string = "c1c1c1c1-0000-4000-8000-000000000009";
|
|
105
|
+
|
|
106
|
+
const CRITICAL_ALERT: string = "a1a1a1a1-0000-4000-8000-000000000001";
|
|
107
|
+
const MAJOR_ALERT: string = "a1a1a1a1-0000-4000-8000-000000000002";
|
|
108
|
+
const FOREIGN_ALERT: string = "a1a1a1a1-0000-4000-8000-000000000009";
|
|
109
|
+
|
|
110
|
+
const UNKNOWN_CHANNEL_MESSAGE: (value: string) => string = (
|
|
111
|
+
value: string,
|
|
112
|
+
): string => {
|
|
113
|
+
return `"${value}" is not a notification channel a compliance rule can require.`;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const UNKNOWN_RULE_TYPE_MESSAGE: (value: string) => string = (
|
|
117
|
+
value: string,
|
|
118
|
+
): string => {
|
|
119
|
+
return `"${value}" is not a compliance rule type.`;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const FOREIGN_INCIDENT_SEVERITY_MESSAGE: string =
|
|
123
|
+
"One or more of the selected incident severities do not exist in this project.";
|
|
124
|
+
const FOREIGN_ALERT_SEVERITY_MESSAGE: string =
|
|
125
|
+
"One or more of the selected alert severities do not exist in this project.";
|
|
126
|
+
|
|
127
|
+
const ON_CALL_RULE_TYPES: Array<ComplianceRuleType> = [
|
|
128
|
+
ComplianceRuleType.HasIncidentOnCallRules,
|
|
129
|
+
ComplianceRuleType.HasAlertOnCallRules,
|
|
130
|
+
ComplianceRuleType.HasIncidentEpisodeOnCallRules,
|
|
131
|
+
ComplianceRuleType.HasAlertEpisodeOnCallRules,
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const INCIDENT_RULE_TYPES: Array<ComplianceRuleType> = [
|
|
135
|
+
ComplianceRuleType.HasIncidentOnCallRules,
|
|
136
|
+
ComplianceRuleType.HasIncidentEpisodeOnCallRules,
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const ALERT_RULE_TYPES: Array<ComplianceRuleType> = [
|
|
140
|
+
ComplianceRuleType.HasAlertOnCallRules,
|
|
141
|
+
ComplianceRuleType.HasAlertEpisodeOnCallRules,
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const METHOD_RULE_TYPES: Array<ComplianceRuleType> = [
|
|
145
|
+
ComplianceRuleType.HasNotificationEmailMethod,
|
|
146
|
+
ComplianceRuleType.HasNotificationSMSMethod,
|
|
147
|
+
ComplianceRuleType.HasNotificationCallMethod,
|
|
148
|
+
ComplianceRuleType.HasNotificationPushMethod,
|
|
149
|
+
ComplianceRuleType.HasNotificationWhatsAppMethod,
|
|
150
|
+
ComplianceRuleType.HasNotificationTelegramMethod,
|
|
151
|
+
ComplianceRuleType.HasNotificationSlackMethod,
|
|
152
|
+
ComplianceRuleType.HasNotificationMicrosoftTeamsMethod,
|
|
153
|
+
ComplianceRuleType.HasNotificationWebhookMethod,
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const ALL_CHANNELS: Array<ComplianceNotificationChannel> = Object.values(
|
|
157
|
+
ComplianceNotificationChannel,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
/*
|
|
161
|
+
* ---------------------------------------------------------------------------
|
|
162
|
+
* The in-memory world the service reads from.
|
|
163
|
+
* ---------------------------------------------------------------------------
|
|
164
|
+
*/
|
|
165
|
+
|
|
166
|
+
// The rules already stored, across every team and project.
|
|
167
|
+
let storedSettings: Array<TeamComplianceSetting> = [];
|
|
168
|
+
|
|
169
|
+
// Severity ids per project, as the severity tables hold them (lower case).
|
|
170
|
+
const INCIDENT_SEVERITIES_BY_PROJECT: Map<string, Array<string>> = new Map<
|
|
171
|
+
string,
|
|
172
|
+
Array<string>
|
|
173
|
+
>([
|
|
174
|
+
[PROJECT_ID.toString(), [CRITICAL_INCIDENT, MAJOR_INCIDENT, MINOR_INCIDENT]],
|
|
175
|
+
[OTHER_PROJECT_ID.toString(), [FOREIGN_INCIDENT]],
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
const ALERT_SEVERITIES_BY_PROJECT: Map<string, Array<string>> = new Map<
|
|
179
|
+
string,
|
|
180
|
+
Array<string>
|
|
181
|
+
>([
|
|
182
|
+
[PROJECT_ID.toString(), [CRITICAL_ALERT, MAJOR_ALERT]],
|
|
183
|
+
[OTHER_PROJECT_ID.toString(), [FOREIGN_ALERT]],
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
let settingsFindBy: jest.SpyInstance;
|
|
187
|
+
let settingsFindOneBy: jest.SpyInstance;
|
|
188
|
+
let settingsCountBy: jest.SpyInstance;
|
|
189
|
+
let incidentSeverityCountBy: jest.SpyInstance;
|
|
190
|
+
let alertSeverityCountBy: jest.SpyInstance;
|
|
191
|
+
|
|
192
|
+
/*
|
|
193
|
+
* A query as DatabaseService would run it: every key must match, ids compare
|
|
194
|
+
* as strings, Includes matches any of its values, and QueryHelper.notEquals -
|
|
195
|
+
* a Raw "!=" carrying the excluded id as its only parameter - excludes that
|
|
196
|
+
* row.
|
|
197
|
+
*/
|
|
198
|
+
const matchesQuery: (
|
|
199
|
+
row: TeamComplianceSetting,
|
|
200
|
+
query: JSONObject,
|
|
201
|
+
) => boolean = (row: TeamComplianceSetting, query: JSONObject): boolean => {
|
|
202
|
+
return Object.keys(query).every((key: string): boolean => {
|
|
203
|
+
const expected: unknown = query[key];
|
|
204
|
+
const actual: unknown = (row as unknown as Record<string, unknown>)[key];
|
|
205
|
+
|
|
206
|
+
if (expected instanceof Includes) {
|
|
207
|
+
return expected.values
|
|
208
|
+
.map((value: unknown): string => {
|
|
209
|
+
return String(value);
|
|
210
|
+
})
|
|
211
|
+
.includes(String(actual));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (expected instanceof FindOperator) {
|
|
215
|
+
const excluded: Array<string> = Object.values(
|
|
216
|
+
expected.objectLiteralParameters || {},
|
|
217
|
+
).map((value: unknown): string => {
|
|
218
|
+
return String(value);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return !excluded.includes(String(actual));
|
|
35
222
|
}
|
|
36
|
-
|
|
223
|
+
|
|
224
|
+
return String(actual) === String(expected);
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/*
|
|
229
|
+
* countBy({_id: Includes(ids), projectId}) the way Postgres answers it: the
|
|
230
|
+
* number of severity ROWS in that project whose uuid is in the list, uuids
|
|
231
|
+
* compared without case.
|
|
232
|
+
*/
|
|
233
|
+
const countSeverities: (
|
|
234
|
+
severitiesByProject: Map<string, Array<string>>,
|
|
235
|
+
query: JSONObject,
|
|
236
|
+
) => PositiveNumber = (
|
|
237
|
+
severitiesByProject: Map<string, Array<string>>,
|
|
238
|
+
query: JSONObject,
|
|
239
|
+
): PositiveNumber => {
|
|
240
|
+
const wanted: Array<string> = (query["_id"] as Includes).values.map(
|
|
241
|
+
(id: unknown): string => {
|
|
242
|
+
return String(id).toLowerCase();
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const inProject: Array<string> =
|
|
247
|
+
severitiesByProject.get(String(query["projectId"])) || [];
|
|
248
|
+
|
|
249
|
+
return new PositiveNumber(
|
|
250
|
+
inProject.filter((id: string): boolean => {
|
|
251
|
+
return wanted.includes(id);
|
|
252
|
+
}).length,
|
|
253
|
+
);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
beforeEach(() => {
|
|
257
|
+
storedSettings = [];
|
|
258
|
+
|
|
259
|
+
/*
|
|
260
|
+
* TeamComplianceSetting is enterprise configuration, and the permission
|
|
261
|
+
* check asks the licence; no test machine holds one.
|
|
262
|
+
*/
|
|
263
|
+
jest
|
|
264
|
+
.spyOn(EnterpriseEdition, "assertFeatureAvailableSync")
|
|
265
|
+
.mockReturnValue(undefined);
|
|
266
|
+
|
|
267
|
+
settingsFindBy = jest
|
|
268
|
+
.spyOn(TeamComplianceSettingService, "findBy")
|
|
269
|
+
.mockImplementation(((findBy: { query: JSONObject }) => {
|
|
270
|
+
return Promise.resolve(
|
|
271
|
+
storedSettings.filter((row: TeamComplianceSetting): boolean => {
|
|
272
|
+
return matchesQuery(row, findBy.query);
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
}) as never);
|
|
276
|
+
|
|
277
|
+
// The service reads through findBy only; any other read is a regression.
|
|
278
|
+
settingsFindOneBy = jest
|
|
279
|
+
.spyOn(TeamComplianceSettingService, "findOneBy")
|
|
280
|
+
.mockRejectedValue(new Error("unexpected findOneBy") as never);
|
|
281
|
+
settingsCountBy = jest
|
|
282
|
+
.spyOn(TeamComplianceSettingService, "countBy")
|
|
283
|
+
.mockRejectedValue(new Error("unexpected countBy") as never);
|
|
284
|
+
|
|
285
|
+
incidentSeverityCountBy = jest
|
|
286
|
+
.spyOn(IncidentSeverityService, "countBy")
|
|
287
|
+
.mockImplementation(((countBy: { query: JSONObject }) => {
|
|
288
|
+
return Promise.resolve(
|
|
289
|
+
countSeverities(INCIDENT_SEVERITIES_BY_PROJECT, countBy.query),
|
|
290
|
+
);
|
|
291
|
+
}) as never);
|
|
292
|
+
|
|
293
|
+
alertSeverityCountBy = jest
|
|
294
|
+
.spyOn(AlertSeverityService, "countBy")
|
|
295
|
+
.mockImplementation(((countBy: { query: JSONObject }) => {
|
|
296
|
+
return Promise.resolve(
|
|
297
|
+
countSeverities(ALERT_SEVERITIES_BY_PROJECT, countBy.query),
|
|
298
|
+
);
|
|
299
|
+
}) as never);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
afterEach(() => {
|
|
303
|
+
jest.restoreAllMocks();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
/*
|
|
307
|
+
* ---------------------------------------------------------------------------
|
|
308
|
+
* Builders.
|
|
309
|
+
* ---------------------------------------------------------------------------
|
|
310
|
+
*/
|
|
311
|
+
|
|
312
|
+
interface StoredRuleInput {
|
|
313
|
+
id: string;
|
|
314
|
+
ruleType: string;
|
|
315
|
+
teamId?: ObjectID | undefined;
|
|
316
|
+
projectId?: ObjectID | undefined;
|
|
317
|
+
notificationChannel?: string | null | undefined;
|
|
318
|
+
incidentSeverityIds?: Array<string> | undefined;
|
|
319
|
+
alertSeverityIds?: Array<string> | undefined;
|
|
320
|
+
// Stored rules are enabled unless a test pauses one.
|
|
321
|
+
enabled?: boolean | undefined;
|
|
322
|
+
// The row's options JSON; unset (NULL) unless a test gives it some.
|
|
323
|
+
options?: JSONObject | undefined;
|
|
324
|
+
omitTeam?: boolean | undefined;
|
|
325
|
+
omitProject?: boolean | undefined;
|
|
37
326
|
}
|
|
38
327
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
328
|
+
// A row as the service's findBy returns it, relations loaded as models.
|
|
329
|
+
const storedRule: (input: StoredRuleInput) => TeamComplianceSetting = (
|
|
330
|
+
input: StoredRuleInput,
|
|
331
|
+
): TeamComplianceSetting => {
|
|
43
332
|
const setting: TeamComplianceSetting = new TeamComplianceSetting();
|
|
44
|
-
|
|
45
|
-
|
|
333
|
+
setting._id = input.id;
|
|
334
|
+
|
|
335
|
+
if (!input.omitTeam) {
|
|
336
|
+
setting.teamId = input.teamId || TEAM_ID;
|
|
46
337
|
}
|
|
47
|
-
|
|
48
|
-
|
|
338
|
+
|
|
339
|
+
if (!input.omitProject) {
|
|
340
|
+
setting.projectId = input.projectId || PROJECT_ID;
|
|
49
341
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
342
|
+
|
|
343
|
+
(setting as unknown as Record<string, unknown>)["ruleType"] = input.ruleType;
|
|
344
|
+
(setting as unknown as Record<string, unknown>)["notificationChannel"] =
|
|
345
|
+
input.notificationChannel === undefined ? null : input.notificationChannel;
|
|
346
|
+
setting.enabled = input.enabled === undefined ? true : input.enabled;
|
|
347
|
+
|
|
348
|
+
if (input.options !== undefined) {
|
|
349
|
+
setting.options = input.options;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
setting.incidentSeverities = (input.incidentSeverityIds || []).map(
|
|
353
|
+
(id: string): IncidentSeverity => {
|
|
354
|
+
const severity: IncidentSeverity = new IncidentSeverity();
|
|
355
|
+
severity._id = id;
|
|
356
|
+
return severity;
|
|
357
|
+
},
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
setting.alertSeverities = (input.alertSeverityIds || []).map(
|
|
361
|
+
(id: string): AlertSeverity => {
|
|
362
|
+
const severity: AlertSeverity = new AlertSeverity();
|
|
363
|
+
severity._id = id;
|
|
364
|
+
return severity;
|
|
365
|
+
},
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
return setting;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const store: (...rows: Array<StoredRuleInput>) => void = (
|
|
372
|
+
...rows: Array<StoredRuleInput>
|
|
373
|
+
): void => {
|
|
374
|
+
for (const row of rows) {
|
|
375
|
+
storedSettings.push(storedRule(row));
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
interface NewRuleInput {
|
|
380
|
+
ruleType?: unknown;
|
|
381
|
+
notificationChannel?: unknown;
|
|
382
|
+
incidentSeverities?: unknown;
|
|
383
|
+
alertSeverities?: unknown;
|
|
384
|
+
teamId?: ObjectID | null | undefined;
|
|
385
|
+
projectId?: ObjectID | null | undefined;
|
|
54
386
|
}
|
|
55
387
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
388
|
+
// The payload of a create, as BaseAPI hands it over: a model, any values.
|
|
389
|
+
const newRule: (input: NewRuleInput) => TeamComplianceSetting = (
|
|
390
|
+
input: NewRuleInput,
|
|
391
|
+
): TeamComplianceSetting => {
|
|
392
|
+
const setting: TeamComplianceSetting = new TeamComplianceSetting();
|
|
393
|
+
const target: Record<string, unknown> = setting as unknown as Record<
|
|
394
|
+
string,
|
|
395
|
+
unknown
|
|
396
|
+
>;
|
|
397
|
+
|
|
398
|
+
if (input.teamId !== null) {
|
|
399
|
+
setting.teamId = input.teamId || TEAM_ID;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (input.projectId !== null) {
|
|
403
|
+
setting.projectId = input.projectId || PROJECT_ID;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const key of [
|
|
407
|
+
"ruleType",
|
|
408
|
+
"notificationChannel",
|
|
409
|
+
"incidentSeverities",
|
|
410
|
+
"alertSeverities",
|
|
411
|
+
]) {
|
|
412
|
+
if (key in input) {
|
|
413
|
+
target[key] = (input as Record<string, unknown>)[key];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return setting;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// A signed-in user holding `permissions` in `project`, and nothing else.
|
|
421
|
+
const memberProps: (
|
|
422
|
+
permissions: Array<Permission>,
|
|
423
|
+
project?: ObjectID,
|
|
424
|
+
) => DatabaseCommonInteractionProps = (
|
|
425
|
+
permissions: Array<Permission>,
|
|
426
|
+
project?: ObjectID,
|
|
427
|
+
): DatabaseCommonInteractionProps => {
|
|
428
|
+
const tenantId: ObjectID = project || PROJECT_ID;
|
|
429
|
+
|
|
430
|
+
const tenantPermission: UserTenantAccessPermission = {
|
|
431
|
+
projectId: tenantId,
|
|
432
|
+
_type: "UserTenantAccessPermission",
|
|
433
|
+
permissions: permissions.map((permission: Permission) => {
|
|
434
|
+
return {
|
|
435
|
+
_type: "UserPermission",
|
|
436
|
+
permission: permission,
|
|
437
|
+
labelIds: [],
|
|
438
|
+
isBlockPermission: false,
|
|
439
|
+
};
|
|
440
|
+
}),
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
tenantId: tenantId,
|
|
445
|
+
userId: USER_ID,
|
|
446
|
+
userType: UserType.User,
|
|
447
|
+
userTenantAccessPermission: {
|
|
448
|
+
[tenantId.toString()]: tenantPermission,
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
/*
|
|
454
|
+
* A team editor: Edit Teams, plus Read Teams to see what they edit. (An
|
|
455
|
+
* update is located by a query on the project, which only a reader of the
|
|
456
|
+
* rules may filter on - the update permission check refuses Edit Teams on
|
|
457
|
+
* its own, here and in _updateBy alike.)
|
|
458
|
+
*/
|
|
459
|
+
const EDITOR_PROPS: DatabaseCommonInteractionProps = memberProps([
|
|
460
|
+
Permission.ReadProjectTeam,
|
|
461
|
+
Permission.EditProjectTeam,
|
|
462
|
+
]);
|
|
463
|
+
|
|
464
|
+
/*
|
|
465
|
+
* Signed in, with the project named as the tenant, but no permission in it -
|
|
466
|
+
* what any user of another project can send.
|
|
467
|
+
*/
|
|
468
|
+
const OUTSIDER_PROPS: DatabaseCommonInteractionProps = {
|
|
469
|
+
tenantId: PROJECT_ID,
|
|
470
|
+
userId: USER_ID,
|
|
471
|
+
userType: UserType.User,
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const createProps: DatabaseCommonInteractionProps = EDITOR_PROPS;
|
|
475
|
+
|
|
476
|
+
const createByFor: (
|
|
477
|
+
data: TeamComplianceSetting,
|
|
478
|
+
props?: DatabaseCommonInteractionProps,
|
|
479
|
+
) => CreateBy<TeamComplianceSetting> = (
|
|
480
|
+
data: TeamComplianceSetting,
|
|
481
|
+
props?: DatabaseCommonInteractionProps,
|
|
482
|
+
): CreateBy<TeamComplianceSetting> => {
|
|
483
|
+
return {
|
|
484
|
+
data: data,
|
|
485
|
+
props: props || createProps,
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
type OnBeforeCreateFunction = (
|
|
490
|
+
createBy: CreateBy<TeamComplianceSetting>,
|
|
491
|
+
) => Promise<OnCreate<TeamComplianceSetting>>;
|
|
492
|
+
|
|
493
|
+
type OnBeforeUpdateFunction = (
|
|
494
|
+
updateBy: UpdateBy<TeamComplianceSetting>,
|
|
495
|
+
) => Promise<OnUpdate<TeamComplianceSetting>>;
|
|
496
|
+
|
|
497
|
+
interface ServiceHooks {
|
|
498
|
+
onBeforeCreate: OnBeforeCreateFunction;
|
|
499
|
+
onBeforeUpdate: OnBeforeUpdateFunction;
|
|
500
|
+
_onBeforeCreate: OnBeforeCreateFunction;
|
|
62
501
|
}
|
|
63
502
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
503
|
+
const hooks: () => ServiceHooks = (): ServiceHooks => {
|
|
504
|
+
return TeamComplianceSettingService as unknown as ServiceHooks;
|
|
505
|
+
};
|
|
67
506
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
507
|
+
const onBeforeCreate: (
|
|
508
|
+
createBy: CreateBy<TeamComplianceSetting>,
|
|
509
|
+
) => Promise<OnCreate<TeamComplianceSetting>> = (
|
|
510
|
+
createBy: CreateBy<TeamComplianceSetting>,
|
|
511
|
+
): Promise<OnCreate<TeamComplianceSetting>> => {
|
|
512
|
+
return hooks().onBeforeCreate(createBy);
|
|
513
|
+
};
|
|
71
514
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
515
|
+
const create: (
|
|
516
|
+
input: NewRuleInput,
|
|
517
|
+
) => Promise<OnCreate<TeamComplianceSetting>> = (
|
|
518
|
+
input: NewRuleInput,
|
|
519
|
+
): Promise<OnCreate<TeamComplianceSetting>> => {
|
|
520
|
+
return onBeforeCreate(createByFor(newRule(input)));
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const updateByFor: (
|
|
524
|
+
data: Record<string, unknown>,
|
|
525
|
+
options?: {
|
|
526
|
+
query?: Record<string, unknown> | undefined;
|
|
527
|
+
props?: DatabaseCommonInteractionProps | undefined;
|
|
528
|
+
},
|
|
529
|
+
) => UpdateBy<TeamComplianceSetting> = (
|
|
530
|
+
data: Record<string, unknown>,
|
|
531
|
+
options?: {
|
|
532
|
+
query?: Record<string, unknown> | undefined;
|
|
533
|
+
props?: DatabaseCommonInteractionProps | undefined;
|
|
534
|
+
},
|
|
535
|
+
): UpdateBy<TeamComplianceSetting> => {
|
|
536
|
+
return {
|
|
537
|
+
query: options?.query || { _id: SETTING_ID },
|
|
538
|
+
data: data,
|
|
539
|
+
limit: 1,
|
|
540
|
+
skip: 0,
|
|
541
|
+
props: options?.props || EDITOR_PROPS,
|
|
542
|
+
} as unknown as UpdateBy<TeamComplianceSetting>;
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const onBeforeUpdate: (
|
|
546
|
+
updateBy: UpdateBy<TeamComplianceSetting>,
|
|
547
|
+
) => Promise<OnUpdate<TeamComplianceSetting>> = (
|
|
548
|
+
updateBy: UpdateBy<TeamComplianceSetting>,
|
|
549
|
+
): Promise<OnUpdate<TeamComplianceSetting>> => {
|
|
550
|
+
return hooks().onBeforeUpdate(updateBy);
|
|
551
|
+
};
|
|
78
552
|
|
|
79
|
-
|
|
80
|
-
|
|
553
|
+
const update: (
|
|
554
|
+
data: Record<string, unknown>,
|
|
555
|
+
) => Promise<OnUpdate<TeamComplianceSetting>> = (
|
|
556
|
+
data: Record<string, unknown>,
|
|
557
|
+
): Promise<OnUpdate<TeamComplianceSetting>> => {
|
|
558
|
+
return onBeforeUpdate(updateByFor(data));
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const findByCalls: () => Array<JSONObject> = (): Array<JSONObject> => {
|
|
562
|
+
return settingsFindBy.mock.calls.map((call: Array<unknown>): JSONObject => {
|
|
563
|
+
return call[0] as JSONObject;
|
|
564
|
+
});
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
/*
|
|
568
|
+
* The duplicate lookups. An update first reads the rows it changes, selecting
|
|
569
|
+
* their team and project; a duplicate lookup already knows both.
|
|
570
|
+
*/
|
|
571
|
+
const siblingReads: () => Array<JSONObject> = (): Array<JSONObject> => {
|
|
572
|
+
return findByCalls().filter((call: JSONObject): boolean => {
|
|
573
|
+
return (call["select"] as JSONObject)["teamId"] === undefined;
|
|
81
574
|
});
|
|
575
|
+
};
|
|
82
576
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
577
|
+
const countCalls: (spy: jest.SpyInstance) => Array<JSONObject> = (
|
|
578
|
+
spy: jest.SpyInstance,
|
|
579
|
+
): Array<JSONObject> => {
|
|
580
|
+
return spy.mock.calls.map((call: Array<unknown>): JSONObject => {
|
|
581
|
+
return call[0] as JSONObject;
|
|
582
|
+
});
|
|
583
|
+
};
|
|
87
584
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
585
|
+
const includedIds: (query: JSONObject) => Array<string> = (
|
|
586
|
+
query: JSONObject,
|
|
587
|
+
): Array<string> => {
|
|
588
|
+
return (query["_id"] as Includes).values.map((id: unknown): string => {
|
|
589
|
+
return String(id);
|
|
91
590
|
});
|
|
591
|
+
};
|
|
92
592
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
593
|
+
const expectNoReads: () => void = (): void => {
|
|
594
|
+
expect(settingsFindBy).not.toHaveBeenCalled();
|
|
595
|
+
expect(settingsFindOneBy).not.toHaveBeenCalled();
|
|
596
|
+
expect(settingsCountBy).not.toHaveBeenCalled();
|
|
597
|
+
expect(incidentSeverityCountBy).not.toHaveBeenCalled();
|
|
598
|
+
expect(alertSeverityCountBy).not.toHaveBeenCalled();
|
|
599
|
+
};
|
|
97
600
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
601
|
+
const dataOf: (
|
|
602
|
+
updateBy: UpdateBy<TeamComplianceSetting>,
|
|
603
|
+
) => Record<string, unknown> = (
|
|
604
|
+
updateBy: UpdateBy<TeamComplianceSetting>,
|
|
605
|
+
): Record<string, unknown> => {
|
|
606
|
+
return updateBy.data as unknown as Record<string, unknown>;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const modelValue: (setting: TeamComplianceSetting, key: string) => unknown = (
|
|
610
|
+
setting: TeamComplianceSetting,
|
|
611
|
+
key: string,
|
|
612
|
+
): unknown => {
|
|
613
|
+
return (setting as unknown as Record<string, unknown>)[key];
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
/*
|
|
617
|
+
* A severity list as the hooks leave it for the relation save: models of the
|
|
618
|
+
* list's own kind, each carrying nothing but its id. Returns those ids.
|
|
619
|
+
*/
|
|
620
|
+
const writtenIds: (
|
|
621
|
+
value: unknown,
|
|
622
|
+
modelType: typeof IncidentSeverity | typeof AlertSeverity,
|
|
623
|
+
) => Array<string> = (
|
|
624
|
+
value: unknown,
|
|
625
|
+
modelType: typeof IncidentSeverity | typeof AlertSeverity,
|
|
626
|
+
): Array<string> => {
|
|
627
|
+
expect(Array.isArray(value)).toBe(true);
|
|
628
|
+
|
|
629
|
+
return (value as Array<unknown>).map((item: unknown): string => {
|
|
630
|
+
expect(item).toBeInstanceOf(modelType);
|
|
631
|
+
|
|
632
|
+
const model: IncidentSeverity | AlertSeverity = item as
|
|
633
|
+
| IncidentSeverity
|
|
634
|
+
| AlertSeverity;
|
|
635
|
+
|
|
636
|
+
expect(model.name).toBeUndefined();
|
|
637
|
+
expect(model.projectId).toBeUndefined();
|
|
638
|
+
|
|
639
|
+
return String(model._id);
|
|
640
|
+
});
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
/*
|
|
644
|
+
* ---------------------------------------------------------------------------
|
|
645
|
+
* create
|
|
646
|
+
* ---------------------------------------------------------------------------
|
|
647
|
+
*/
|
|
648
|
+
|
|
649
|
+
describe("TeamComplianceSettingService onBeforeCreate - what may be created", () => {
|
|
650
|
+
test.each(Object.values(ComplianceRuleType))(
|
|
651
|
+
"accepts a %s rule on a team that has none",
|
|
652
|
+
async (ruleType: ComplianceRuleType) => {
|
|
653
|
+
const createBy: CreateBy<TeamComplianceSetting> = createByFor(
|
|
654
|
+
newRule({ ruleType: ruleType }),
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
const result: OnCreate<TeamComplianceSetting> =
|
|
658
|
+
await onBeforeCreate(createBy);
|
|
659
|
+
|
|
660
|
+
// The hook hands back the very same request, nothing carried forward.
|
|
661
|
+
expect(result.createBy).toBe(createBy);
|
|
662
|
+
expect(result.carryForward).toBeNull();
|
|
663
|
+
expect(result.createBy.data.ruleType).toBe(ruleType);
|
|
664
|
+
},
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
test.each([
|
|
668
|
+
["a rule type from the old free-text era", "RequireTwoFactorAuth"],
|
|
669
|
+
["a near miss in case", "hasincidentoncallrules"],
|
|
670
|
+
["an empty string", ""],
|
|
671
|
+
[
|
|
672
|
+
"a notification rule type, not a compliance one",
|
|
673
|
+
"WHEN_USER_GOES_ON_CALL",
|
|
674
|
+
],
|
|
675
|
+
])(
|
|
676
|
+
"refuses %s before reading anything",
|
|
677
|
+
async (_label: string, ruleType: string) => {
|
|
678
|
+
await expect(create({ ruleType: ruleType })).rejects.toThrow(
|
|
679
|
+
new BadDataException(UNKNOWN_RULE_TYPE_MESSAGE(ruleType)),
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
expectNoReads();
|
|
683
|
+
},
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
test.each([
|
|
687
|
+
["missing", undefined],
|
|
688
|
+
["null", null],
|
|
689
|
+
["a number", 42],
|
|
690
|
+
["an object", { ruleType: "HasIncidentOnCallRules" }],
|
|
691
|
+
])(
|
|
692
|
+
"refuses a rule type that is %s",
|
|
693
|
+
async (_label: string, ruleType: unknown) => {
|
|
694
|
+
await expect(create({ ruleType: ruleType })).rejects.toThrow(
|
|
695
|
+
BadDataException,
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
expectNoReads();
|
|
699
|
+
},
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
test("the refusal is a BadDataException that names the value, so the form can show it", async () => {
|
|
703
|
+
await expect(create({ ruleType: "RequirePhoneNumber" })).rejects.toThrow(
|
|
704
|
+
'"RequirePhoneNumber" is not a compliance rule type.',
|
|
102
705
|
);
|
|
103
706
|
});
|
|
104
707
|
|
|
105
|
-
test(
|
|
106
|
-
|
|
708
|
+
test.each(ALL_CHANNELS)(
|
|
709
|
+
"accepts the %s channel on every on-call rule type and keeps it",
|
|
710
|
+
async (channel: ComplianceNotificationChannel) => {
|
|
711
|
+
for (const ruleType of ON_CALL_RULE_TYPES) {
|
|
712
|
+
const result: OnCreate<TeamComplianceSetting> = await create({
|
|
713
|
+
ruleType: ruleType,
|
|
714
|
+
notificationChannel: channel,
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
expect({
|
|
718
|
+
ruleType,
|
|
719
|
+
channel: result.createBy.data.notificationChannel,
|
|
720
|
+
}).toEqual({ ruleType, channel });
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
test.each([
|
|
726
|
+
["undefined", undefined],
|
|
727
|
+
["null", null],
|
|
728
|
+
])(
|
|
729
|
+
"a %s channel is 'any channel', and is valid",
|
|
730
|
+
async (_label: string, channel: unknown) => {
|
|
731
|
+
const result: OnCreate<TeamComplianceSetting> = await create({
|
|
732
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
733
|
+
notificationChannel: channel,
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
expect(modelValue(result.createBy.data, "notificationChannel")).toBe(
|
|
737
|
+
channel,
|
|
738
|
+
);
|
|
739
|
+
},
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
test.each([
|
|
743
|
+
["a channel that does not exist", "Pager"],
|
|
744
|
+
["a channel in the wrong case", "call"],
|
|
745
|
+
["an empty string", ""],
|
|
746
|
+
["a method rule type", "HasNotificationCallMethod"],
|
|
747
|
+
])(
|
|
748
|
+
"refuses %s as the channel, before reading anything",
|
|
749
|
+
async (_label: string, channel: string) => {
|
|
750
|
+
await expect(
|
|
751
|
+
create({
|
|
752
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
753
|
+
notificationChannel: channel,
|
|
754
|
+
}),
|
|
755
|
+
).rejects.toThrow(new BadDataException(UNKNOWN_CHANNEL_MESSAGE(channel)));
|
|
756
|
+
|
|
757
|
+
expectNoReads();
|
|
758
|
+
},
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
test("refuses a channel that is not a channel even on a method rule - garbage is refused, not quietly dropped", async () => {
|
|
762
|
+
await expect(
|
|
763
|
+
create({
|
|
764
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
765
|
+
notificationChannel: "Pager",
|
|
766
|
+
}),
|
|
767
|
+
).rejects.toThrow(UNKNOWN_CHANNEL_MESSAGE("Pager"));
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
test("refuses a rule with no project, before reading anything", async () => {
|
|
771
|
+
await expect(
|
|
772
|
+
onBeforeCreate(
|
|
773
|
+
createByFor(
|
|
774
|
+
newRule({
|
|
775
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
776
|
+
projectId: null,
|
|
777
|
+
}),
|
|
778
|
+
{ isRoot: true },
|
|
779
|
+
),
|
|
780
|
+
),
|
|
781
|
+
).rejects.toThrow(new BadDataException("Project ID is required."));
|
|
782
|
+
|
|
783
|
+
expectNoReads();
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
test("refuses a rule with no team, before reading anything", async () => {
|
|
787
|
+
await expect(
|
|
788
|
+
create({
|
|
789
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
790
|
+
teamId: null,
|
|
791
|
+
}),
|
|
792
|
+
).rejects.toThrow(new BadDataException("Team ID is required."));
|
|
793
|
+
|
|
794
|
+
expectNoReads();
|
|
795
|
+
});
|
|
107
796
|
|
|
797
|
+
test("takes the project from the caller's tenant when the payload has none", async () => {
|
|
108
798
|
await onBeforeCreate(
|
|
109
|
-
|
|
799
|
+
createByFor(
|
|
800
|
+
newRule({
|
|
801
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
802
|
+
projectId: null,
|
|
803
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
804
|
+
}),
|
|
805
|
+
EDITOR_PROPS,
|
|
806
|
+
),
|
|
110
807
|
);
|
|
111
808
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
809
|
+
expect(countCalls(incidentSeverityCountBy)[0]?.["query"]).toMatchObject({
|
|
810
|
+
projectId: PROJECT_ID,
|
|
811
|
+
});
|
|
812
|
+
expect(siblingReads()[0]?.["query"]).toMatchObject({
|
|
813
|
+
projectId: PROJECT_ID,
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test("a projectId smuggled into the payload cannot move the checks into another project", async () => {
|
|
818
|
+
/*
|
|
819
|
+
* DatabaseService.create stamps the tenant onto the payload before this
|
|
820
|
+
* hook runs (_onBeforeCreate), so the severity check and the duplicate
|
|
821
|
+
* lookup run in the caller's project, not the one the payload names.
|
|
822
|
+
*/
|
|
823
|
+
await expect(
|
|
824
|
+
hooks()._onBeforeCreate(
|
|
825
|
+
createByFor(
|
|
826
|
+
newRule({
|
|
827
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
828
|
+
projectId: OTHER_PROJECT_ID,
|
|
829
|
+
incidentSeverities: [FOREIGN_INCIDENT],
|
|
830
|
+
}),
|
|
831
|
+
EDITOR_PROPS,
|
|
832
|
+
),
|
|
833
|
+
),
|
|
834
|
+
).rejects.toThrow(FOREIGN_INCIDENT_SEVERITY_MESSAGE);
|
|
835
|
+
|
|
836
|
+
expect(countCalls(incidentSeverityCountBy)[0]?.["query"]).toMatchObject({
|
|
837
|
+
projectId: PROJECT_ID,
|
|
117
838
|
});
|
|
118
|
-
// Only the id is needed to know whether one exists.
|
|
119
|
-
expect(findArgs["select"]).toEqual({ _id: true });
|
|
120
839
|
});
|
|
840
|
+
});
|
|
121
841
|
|
|
122
|
-
|
|
123
|
-
|
|
842
|
+
describe("TeamComplianceSettingService onBeforeCreate - options a rule type does not use are cleared", () => {
|
|
843
|
+
test.each(METHOD_RULE_TYPES)(
|
|
844
|
+
"%s keeps no channel and no severities of either kind",
|
|
845
|
+
async (ruleType: ComplianceRuleType) => {
|
|
846
|
+
const result: OnCreate<TeamComplianceSetting> = await create({
|
|
847
|
+
ruleType: ruleType,
|
|
848
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
849
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
850
|
+
alertSeverities: [CRITICAL_ALERT],
|
|
851
|
+
});
|
|
124
852
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
unknown
|
|
143
|
-
>;
|
|
144
|
-
// Only the first team already holds this rule.
|
|
145
|
-
if (query["teamId"] === TEAM_ID) {
|
|
146
|
-
const existing: TeamComplianceSetting = new TeamComplianceSetting();
|
|
147
|
-
existing._id = "33333333-3333-4333-8333-333333333333";
|
|
148
|
-
return Promise.resolve(existing);
|
|
149
|
-
}
|
|
150
|
-
return Promise.resolve(null);
|
|
151
|
-
}) as never);
|
|
853
|
+
expect(
|
|
854
|
+
modelValue(result.createBy.data, "notificationChannel"),
|
|
855
|
+
).toBeNull();
|
|
856
|
+
expect(result.createBy.data.incidentSeverities).toEqual([]);
|
|
857
|
+
expect(result.createBy.data.alertSeverities).toEqual([]);
|
|
858
|
+
},
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
test.each(INCIDENT_RULE_TYPES)(
|
|
862
|
+
"%s keeps its channel and incident severities, and drops alert severities",
|
|
863
|
+
async (ruleType: ComplianceRuleType) => {
|
|
864
|
+
const result: OnCreate<TeamComplianceSetting> = await create({
|
|
865
|
+
ruleType: ruleType,
|
|
866
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
867
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
868
|
+
alertSeverities: [CRITICAL_ALERT],
|
|
869
|
+
});
|
|
152
870
|
|
|
871
|
+
expect(result.createBy.data.notificationChannel).toBe(
|
|
872
|
+
ComplianceNotificationChannel.Push,
|
|
873
|
+
);
|
|
874
|
+
expect(
|
|
875
|
+
writtenIds(
|
|
876
|
+
modelValue(result.createBy.data, "incidentSeverities"),
|
|
877
|
+
IncidentSeverity,
|
|
878
|
+
),
|
|
879
|
+
).toEqual([CRITICAL_INCIDENT]);
|
|
880
|
+
expect(result.createBy.data.alertSeverities).toEqual([]);
|
|
881
|
+
},
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
test.each(ALERT_RULE_TYPES)(
|
|
885
|
+
"%s keeps its channel and alert severities, and drops incident severities",
|
|
886
|
+
async (ruleType: ComplianceRuleType) => {
|
|
887
|
+
const result: OnCreate<TeamComplianceSetting> = await create({
|
|
888
|
+
ruleType: ruleType,
|
|
889
|
+
notificationChannel: ComplianceNotificationChannel.SMS,
|
|
890
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
891
|
+
alertSeverities: [CRITICAL_ALERT],
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
expect(result.createBy.data.notificationChannel).toBe(
|
|
895
|
+
ComplianceNotificationChannel.SMS,
|
|
896
|
+
);
|
|
897
|
+
expect(
|
|
898
|
+
writtenIds(
|
|
899
|
+
modelValue(result.createBy.data, "alertSeverities"),
|
|
900
|
+
AlertSeverity,
|
|
901
|
+
),
|
|
902
|
+
).toEqual([CRITICAL_ALERT]);
|
|
903
|
+
expect(result.createBy.data.incidentSeverities).toEqual([]);
|
|
904
|
+
},
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
test("a stray option is dropped, not validated: a foreign severity on a method rule is not an error", async () => {
|
|
153
908
|
await expect(
|
|
154
|
-
|
|
155
|
-
|
|
909
|
+
create({
|
|
910
|
+
ruleType: ComplianceRuleType.HasNotificationPushMethod,
|
|
911
|
+
incidentSeverities: [FOREIGN_INCIDENT],
|
|
912
|
+
alertSeverities: ["not-a-uuid"],
|
|
913
|
+
}),
|
|
914
|
+
).resolves.toBeDefined();
|
|
156
915
|
|
|
916
|
+
expect(incidentSeverityCountBy).not.toHaveBeenCalled();
|
|
917
|
+
expect(alertSeverityCountBy).not.toHaveBeenCalled();
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
test("a stray severity of the other kind is dropped, not validated", async () => {
|
|
157
921
|
await expect(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
922
|
+
create({
|
|
923
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
924
|
+
alertSeverities: [FOREIGN_ALERT],
|
|
925
|
+
}),
|
|
161
926
|
).resolves.toBeDefined();
|
|
162
927
|
|
|
163
|
-
expect(
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("
|
|
167
|
-
const
|
|
168
|
-
.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
928
|
+
expect(alertSeverityCountBy).not.toHaveBeenCalled();
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test("options that were not sent are left unset rather than invented", async () => {
|
|
932
|
+
const result: OnCreate<TeamComplianceSetting> = await create({
|
|
933
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
expect(result.createBy.data.notificationChannel).toBeUndefined();
|
|
937
|
+
expect(result.createBy.data.incidentSeverities).toBeUndefined();
|
|
938
|
+
expect(result.createBy.data.alertSeverities).toBeUndefined();
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
describe("TeamComplianceSettingService onBeforeCreate - severities must belong to the project", () => {
|
|
943
|
+
test.each(INCIDENT_RULE_TYPES)(
|
|
944
|
+
"%s counts its incident severities in the rule's project, once, as root",
|
|
945
|
+
async (ruleType: ComplianceRuleType) => {
|
|
946
|
+
await create({
|
|
947
|
+
ruleType: ruleType,
|
|
948
|
+
incidentSeverities: [MAJOR_INCIDENT, CRITICAL_INCIDENT],
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
expect(incidentSeverityCountBy).toHaveBeenCalledTimes(1);
|
|
952
|
+
expect(alertSeverityCountBy).not.toHaveBeenCalled();
|
|
953
|
+
|
|
954
|
+
const countBy: JSONObject = countCalls(incidentSeverityCountBy)[0]!;
|
|
955
|
+
expect(includedIds(countBy["query"] as JSONObject)).toEqual(
|
|
956
|
+
[CRITICAL_INCIDENT, MAJOR_INCIDENT].sort(),
|
|
957
|
+
);
|
|
958
|
+
expect((countBy["query"] as JSONObject)["projectId"]).toBe(PROJECT_ID);
|
|
959
|
+
expect(Object.keys(countBy["query"] as JSONObject).sort()).toEqual([
|
|
960
|
+
"_id",
|
|
961
|
+
"projectId",
|
|
962
|
+
]);
|
|
963
|
+
expect(countBy["props"]).toEqual({ isRoot: true });
|
|
964
|
+
},
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
test.each(ALERT_RULE_TYPES)(
|
|
968
|
+
"%s counts its alert severities in the rule's project, once, as root",
|
|
969
|
+
async (ruleType: ComplianceRuleType) => {
|
|
970
|
+
await create({
|
|
971
|
+
ruleType: ruleType,
|
|
972
|
+
alertSeverities: [CRITICAL_ALERT, MAJOR_ALERT],
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
expect(alertSeverityCountBy).toHaveBeenCalledTimes(1);
|
|
976
|
+
expect(incidentSeverityCountBy).not.toHaveBeenCalled();
|
|
977
|
+
|
|
978
|
+
const countBy: JSONObject = countCalls(alertSeverityCountBy)[0]!;
|
|
979
|
+
expect(includedIds(countBy["query"] as JSONObject)).toEqual(
|
|
980
|
+
[CRITICAL_ALERT, MAJOR_ALERT].sort(),
|
|
981
|
+
);
|
|
982
|
+
expect((countBy["query"] as JSONObject)["projectId"]).toBe(PROJECT_ID);
|
|
983
|
+
expect(countBy["props"]).toEqual({ isRoot: true });
|
|
984
|
+
},
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
test.each(INCIDENT_RULE_TYPES)(
|
|
988
|
+
"%s refuses an incident severity from another project",
|
|
989
|
+
async (ruleType: ComplianceRuleType) => {
|
|
990
|
+
await expect(
|
|
991
|
+
create({
|
|
992
|
+
ruleType: ruleType,
|
|
993
|
+
incidentSeverities: [CRITICAL_INCIDENT, FOREIGN_INCIDENT],
|
|
994
|
+
}),
|
|
995
|
+
).rejects.toThrow(
|
|
996
|
+
new BadDataException(FOREIGN_INCIDENT_SEVERITY_MESSAGE),
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
// Refused before the duplicate lookup.
|
|
1000
|
+
expect(settingsFindBy).not.toHaveBeenCalled();
|
|
1001
|
+
},
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
test.each(ALERT_RULE_TYPES)(
|
|
1005
|
+
"%s refuses an alert severity from another project",
|
|
1006
|
+
async (ruleType: ComplianceRuleType) => {
|
|
1007
|
+
await expect(
|
|
1008
|
+
create({
|
|
1009
|
+
ruleType: ruleType,
|
|
1010
|
+
alertSeverities: [FOREIGN_ALERT],
|
|
1011
|
+
}),
|
|
1012
|
+
).rejects.toThrow(new BadDataException(FOREIGN_ALERT_SEVERITY_MESSAGE));
|
|
1013
|
+
},
|
|
1014
|
+
);
|
|
183
1015
|
|
|
1016
|
+
test("an alert severity id is not an incident severity: the kinds are checked against their own table", async () => {
|
|
184
1017
|
await expect(
|
|
185
|
-
|
|
186
|
-
|
|
1018
|
+
create({
|
|
1019
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1020
|
+
incidentSeverities: [CRITICAL_ALERT],
|
|
1021
|
+
}),
|
|
1022
|
+
).rejects.toThrow(FOREIGN_INCIDENT_SEVERITY_MESSAGE);
|
|
1023
|
+
|
|
1024
|
+
expect(alertSeverityCountBy).not.toHaveBeenCalled();
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
test("refuses an id that is not a uuid, without querying with it", async () => {
|
|
1028
|
+
await expect(
|
|
1029
|
+
create({
|
|
1030
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1031
|
+
incidentSeverities: [CRITICAL_INCIDENT, "not-a-uuid"],
|
|
1032
|
+
}),
|
|
1033
|
+
).rejects.toThrow(/Invalid ID format: "not-a-uuid"/);
|
|
1034
|
+
|
|
1035
|
+
expect(incidentSeverityCountBy).not.toHaveBeenCalled();
|
|
1036
|
+
expect(settingsFindBy).not.toHaveBeenCalled();
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
test("a repeated severity is one severity, and is counted once", async () => {
|
|
1040
|
+
await expect(
|
|
1041
|
+
create({
|
|
1042
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1043
|
+
incidentSeverities: [
|
|
1044
|
+
CRITICAL_INCIDENT,
|
|
1045
|
+
CRITICAL_INCIDENT,
|
|
1046
|
+
CRITICAL_INCIDENT.toUpperCase(),
|
|
1047
|
+
],
|
|
1048
|
+
}),
|
|
1049
|
+
).resolves.toBeDefined();
|
|
1050
|
+
|
|
1051
|
+
expect(
|
|
1052
|
+
includedIds(
|
|
1053
|
+
countCalls(incidentSeverityCountBy)[0]!["query"] as JSONObject,
|
|
1054
|
+
),
|
|
1055
|
+
).toEqual([CRITICAL_INCIDENT]);
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
test("every shape a relation value arrives in is understood", async () => {
|
|
1059
|
+
const asModel: IncidentSeverity = new IncidentSeverity();
|
|
1060
|
+
asModel._id = MINOR_INCIDENT;
|
|
1061
|
+
|
|
1062
|
+
await create({
|
|
1063
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1064
|
+
incidentSeverities: [
|
|
1065
|
+
CRITICAL_INCIDENT,
|
|
1066
|
+
new ObjectID(MAJOR_INCIDENT),
|
|
1067
|
+
{ _id: MINOR_INCIDENT },
|
|
1068
|
+
{ id: CRITICAL_INCIDENT },
|
|
1069
|
+
asModel,
|
|
1070
|
+
],
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
expect(
|
|
1074
|
+
includedIds(
|
|
1075
|
+
countCalls(incidentSeverityCountBy)[0]!["query"] as JSONObject,
|
|
187
1076
|
),
|
|
1077
|
+
).toEqual([CRITICAL_INCIDENT, MAJOR_INCIDENT, MINOR_INCIDENT].sort());
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
test.each([
|
|
1081
|
+
["not sent", undefined],
|
|
1082
|
+
["null", null],
|
|
1083
|
+
["empty", []],
|
|
1084
|
+
])(
|
|
1085
|
+
"severities %s mean every severity, and read no severity table",
|
|
1086
|
+
async (_label: string, severities: unknown) => {
|
|
1087
|
+
await create({
|
|
1088
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1089
|
+
incidentSeverities: severities,
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
expect(incidentSeverityCountBy).not.toHaveBeenCalled();
|
|
1093
|
+
expect(alertSeverityCountBy).not.toHaveBeenCalled();
|
|
1094
|
+
},
|
|
1095
|
+
);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
describe("TeamComplianceSettingService onBeforeCreate - exact duplicates are refused", () => {
|
|
1099
|
+
test("the duplicate lookup reads the team's rules of that type in the project, as root", async () => {
|
|
1100
|
+
await create({
|
|
1101
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1102
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
expect(settingsFindBy).toHaveBeenCalledTimes(1);
|
|
1106
|
+
|
|
1107
|
+
const findBy: JSONObject = findByCalls()[0]!;
|
|
1108
|
+
expect(findBy["query"]).toEqual({
|
|
1109
|
+
teamId: TEAM_ID,
|
|
1110
|
+
projectId: PROJECT_ID,
|
|
1111
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1112
|
+
});
|
|
1113
|
+
expect(findBy["select"]).toEqual({
|
|
1114
|
+
_id: true,
|
|
1115
|
+
ruleType: true,
|
|
1116
|
+
notificationChannel: true,
|
|
1117
|
+
// A rule a severity delete emptied is no duplicate: see below.
|
|
1118
|
+
options: true,
|
|
1119
|
+
incidentSeverities: { _id: true },
|
|
1120
|
+
alertSeverities: { _id: true },
|
|
1121
|
+
});
|
|
1122
|
+
expect(findBy["limit"]).toBe(LIMIT_PER_PROJECT);
|
|
1123
|
+
expect(findBy["skip"]).toBe(0);
|
|
1124
|
+
// The existing row may be one the caller cannot see.
|
|
1125
|
+
expect(findBy["props"]).toEqual({ isRoot: true });
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
test("the message tells the user what to do instead", () => {
|
|
1129
|
+
expect(DUPLICATE_COMPLIANCE_RULE_MESSAGE).toBe(
|
|
1130
|
+
"This team already has a compliance rule that checks exactly the same thing. Edit that rule instead of adding another.",
|
|
1131
|
+
);
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
test.each<[string, StoredRuleInput, NewRuleInput]>([
|
|
1135
|
+
[
|
|
1136
|
+
"the same channel and severities",
|
|
1137
|
+
{
|
|
1138
|
+
id: SIBLING_SETTING_ID,
|
|
1139
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1140
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1141
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1145
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1146
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
1147
|
+
},
|
|
1148
|
+
],
|
|
1149
|
+
[
|
|
1150
|
+
"the same severities in another order, with repeats",
|
|
1151
|
+
{
|
|
1152
|
+
id: SIBLING_SETTING_ID,
|
|
1153
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1154
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1155
|
+
incidentSeverityIds: [MAJOR_INCIDENT, CRITICAL_INCIDENT],
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1159
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1160
|
+
incidentSeverities: [
|
|
1161
|
+
{ _id: CRITICAL_INCIDENT },
|
|
1162
|
+
{ _id: MAJOR_INCIDENT },
|
|
1163
|
+
{ _id: CRITICAL_INCIDENT },
|
|
1164
|
+
],
|
|
1165
|
+
},
|
|
1166
|
+
],
|
|
1167
|
+
[
|
|
1168
|
+
"the same severity ids in upper case",
|
|
1169
|
+
{
|
|
1170
|
+
id: SIBLING_SETTING_ID,
|
|
1171
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
1172
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1173
|
+
alertSeverityIds: [CRITICAL_ALERT],
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
1177
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1178
|
+
alertSeverities: [CRITICAL_ALERT.toUpperCase()],
|
|
1179
|
+
},
|
|
1180
|
+
],
|
|
1181
|
+
[
|
|
1182
|
+
"a legacy rule: no channel, every severity",
|
|
1183
|
+
{
|
|
1184
|
+
id: SIBLING_SETTING_ID,
|
|
1185
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1189
|
+
},
|
|
1190
|
+
],
|
|
1191
|
+
[
|
|
1192
|
+
"a legacy rule, with the options sent explicitly empty",
|
|
1193
|
+
{
|
|
1194
|
+
id: SIBLING_SETTING_ID,
|
|
1195
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
1196
|
+
},
|
|
1197
|
+
{
|
|
1198
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
1199
|
+
notificationChannel: null,
|
|
1200
|
+
alertSeverities: [],
|
|
1201
|
+
},
|
|
1202
|
+
],
|
|
1203
|
+
[
|
|
1204
|
+
"a legacy method rule",
|
|
1205
|
+
{
|
|
1206
|
+
id: SIBLING_SETTING_ID,
|
|
1207
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
1211
|
+
},
|
|
1212
|
+
],
|
|
1213
|
+
[
|
|
1214
|
+
"a method rule sent with a stray channel and severities, which it does not use",
|
|
1215
|
+
{
|
|
1216
|
+
id: SIBLING_SETTING_ID,
|
|
1217
|
+
ruleType: ComplianceRuleType.HasNotificationCallMethod,
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
ruleType: ComplianceRuleType.HasNotificationCallMethod,
|
|
1221
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1222
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
1223
|
+
alertSeverities: [CRITICAL_ALERT],
|
|
1224
|
+
},
|
|
1225
|
+
],
|
|
1226
|
+
[
|
|
1227
|
+
"a stored rule carrying stray options of the other kind, which never counted",
|
|
1228
|
+
{
|
|
1229
|
+
id: SIBLING_SETTING_ID,
|
|
1230
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1231
|
+
alertSeverityIds: [CRITICAL_ALERT],
|
|
1232
|
+
},
|
|
1233
|
+
{
|
|
1234
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1235
|
+
},
|
|
1236
|
+
],
|
|
1237
|
+
[
|
|
1238
|
+
"a stored rule whose channel is garbage, which reads as 'any channel'",
|
|
1239
|
+
{
|
|
1240
|
+
id: SIBLING_SETTING_ID,
|
|
1241
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1242
|
+
notificationChannel: "Pager",
|
|
1243
|
+
},
|
|
1244
|
+
{
|
|
1245
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1246
|
+
},
|
|
1247
|
+
],
|
|
1248
|
+
[
|
|
1249
|
+
"the same episode rule",
|
|
1250
|
+
{
|
|
1251
|
+
id: SIBLING_SETTING_ID,
|
|
1252
|
+
ruleType: ComplianceRuleType.HasAlertEpisodeOnCallRules,
|
|
1253
|
+
notificationChannel: ComplianceNotificationChannel.Webhook,
|
|
1254
|
+
alertSeverityIds: [MAJOR_ALERT, CRITICAL_ALERT],
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
ruleType: ComplianceRuleType.HasAlertEpisodeOnCallRules,
|
|
1258
|
+
notificationChannel: ComplianceNotificationChannel.Webhook,
|
|
1259
|
+
alertSeverities: [CRITICAL_ALERT, MAJOR_ALERT],
|
|
1260
|
+
},
|
|
1261
|
+
],
|
|
1262
|
+
])(
|
|
1263
|
+
"refuses %s",
|
|
1264
|
+
async (
|
|
1265
|
+
_label: string,
|
|
1266
|
+
existing: StoredRuleInput,
|
|
1267
|
+
incoming: NewRuleInput,
|
|
1268
|
+
) => {
|
|
1269
|
+
store(existing);
|
|
1270
|
+
|
|
1271
|
+
await expect(create(incoming)).rejects.toThrow(
|
|
1272
|
+
new BadDataException(DUPLICATE_COMPLIANCE_RULE_MESSAGE),
|
|
1273
|
+
);
|
|
1274
|
+
},
|
|
1275
|
+
);
|
|
1276
|
+
|
|
1277
|
+
test.each<[string, StoredRuleInput, NewRuleInput]>([
|
|
1278
|
+
[
|
|
1279
|
+
"another channel for the same severities",
|
|
1280
|
+
{
|
|
1281
|
+
id: SIBLING_SETTING_ID,
|
|
1282
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1283
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1284
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1288
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1289
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
1290
|
+
},
|
|
1291
|
+
],
|
|
1292
|
+
[
|
|
1293
|
+
"'any channel' next to a Call rule",
|
|
1294
|
+
{
|
|
1295
|
+
id: SIBLING_SETTING_ID,
|
|
1296
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1297
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1298
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1302
|
+
notificationChannel: null,
|
|
1303
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
1304
|
+
},
|
|
1305
|
+
],
|
|
1306
|
+
[
|
|
1307
|
+
"a Call rule next to a legacy 'any channel' rule",
|
|
1308
|
+
{
|
|
1309
|
+
id: SIBLING_SETTING_ID,
|
|
1310
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1311
|
+
},
|
|
1312
|
+
{
|
|
1313
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1314
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1315
|
+
},
|
|
1316
|
+
],
|
|
1317
|
+
[
|
|
1318
|
+
"a wider severity set",
|
|
1319
|
+
{
|
|
1320
|
+
id: SIBLING_SETTING_ID,
|
|
1321
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1322
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1323
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1327
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1328
|
+
incidentSeverities: [CRITICAL_INCIDENT, MAJOR_INCIDENT],
|
|
1329
|
+
},
|
|
1330
|
+
],
|
|
1331
|
+
[
|
|
1332
|
+
"every severity next to one severity",
|
|
1333
|
+
{
|
|
1334
|
+
id: SIBLING_SETTING_ID,
|
|
1335
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
1336
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1337
|
+
alertSeverityIds: [CRITICAL_ALERT],
|
|
1338
|
+
},
|
|
1339
|
+
{
|
|
1340
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
1341
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1342
|
+
},
|
|
1343
|
+
],
|
|
1344
|
+
[
|
|
1345
|
+
"the same scope on the episode rule type",
|
|
1346
|
+
{
|
|
1347
|
+
id: SIBLING_SETTING_ID,
|
|
1348
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1349
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1350
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
ruleType: ComplianceRuleType.HasIncidentEpisodeOnCallRules,
|
|
1354
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1355
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
1356
|
+
},
|
|
1357
|
+
],
|
|
1358
|
+
[
|
|
1359
|
+
"the same rule on another team",
|
|
1360
|
+
{
|
|
1361
|
+
id: SIBLING_SETTING_ID,
|
|
1362
|
+
teamId: OTHER_TEAM_ID,
|
|
1363
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
1367
|
+
},
|
|
1368
|
+
],
|
|
1369
|
+
[
|
|
1370
|
+
"another method rule",
|
|
1371
|
+
{
|
|
1372
|
+
id: SIBLING_SETTING_ID,
|
|
1373
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
ruleType: ComplianceRuleType.HasNotificationSMSMethod,
|
|
1377
|
+
},
|
|
1378
|
+
],
|
|
1379
|
+
])(
|
|
1380
|
+
"allows %s",
|
|
1381
|
+
async (
|
|
1382
|
+
_label: string,
|
|
1383
|
+
existing: StoredRuleInput,
|
|
1384
|
+
incoming: NewRuleInput,
|
|
1385
|
+
) => {
|
|
1386
|
+
store(existing);
|
|
1387
|
+
|
|
1388
|
+
await expect(create(incoming)).resolves.toBeDefined();
|
|
1389
|
+
},
|
|
1390
|
+
);
|
|
1391
|
+
|
|
1392
|
+
test("a team with the same team id in another project does not block the rule", async () => {
|
|
1393
|
+
store({
|
|
1394
|
+
id: OTHER_PROJECT_SETTING_ID,
|
|
1395
|
+
projectId: OTHER_PROJECT_ID,
|
|
1396
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
await expect(
|
|
1400
|
+
create({ ruleType: ComplianceRuleType.HasNotificationEmailMethod }),
|
|
188
1401
|
).resolves.toBeDefined();
|
|
189
|
-
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
test("several rules of one type can live side by side, and only the exact one is refused", async () => {
|
|
1405
|
+
store(
|
|
1406
|
+
{
|
|
1407
|
+
id: "66666666-6666-4666-8666-00000000000a",
|
|
1408
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1409
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1410
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
1411
|
+
},
|
|
1412
|
+
{
|
|
1413
|
+
id: "66666666-6666-4666-8666-00000000000b",
|
|
1414
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1415
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1416
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
1417
|
+
},
|
|
1418
|
+
{
|
|
1419
|
+
id: "66666666-6666-4666-8666-00000000000c",
|
|
1420
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1421
|
+
},
|
|
1422
|
+
);
|
|
1423
|
+
|
|
1424
|
+
await expect(
|
|
1425
|
+
create({
|
|
1426
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1427
|
+
notificationChannel: ComplianceNotificationChannel.SMS,
|
|
1428
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
1429
|
+
}),
|
|
1430
|
+
).resolves.toBeDefined();
|
|
1431
|
+
|
|
1432
|
+
await expect(
|
|
1433
|
+
create({
|
|
1434
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1435
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1436
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
1437
|
+
}),
|
|
1438
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
1439
|
+
});
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
/*
|
|
1443
|
+
* ---------------------------------------------------------------------------
|
|
1444
|
+
* update
|
|
1445
|
+
* ---------------------------------------------------------------------------
|
|
1446
|
+
*/
|
|
1447
|
+
|
|
1448
|
+
const INCIDENT_CALL_CRITICAL: StoredRuleInput = {
|
|
1449
|
+
id: SETTING_ID,
|
|
1450
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1451
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1452
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
/*
|
|
1456
|
+
* "Call for Critical incidents" after Critical was deleted: the join row
|
|
1457
|
+
* cascaded away, and pauseRulesLeftWithoutSeverities paused and marked it.
|
|
1458
|
+
*/
|
|
1459
|
+
const EMPTIED_BY_DELETE: StoredRuleInput = {
|
|
1460
|
+
id: SETTING_ID,
|
|
1461
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1462
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1463
|
+
enabled: false,
|
|
1464
|
+
options: { [SEVERITIES_DELETED_OPTION]: true },
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
describe("TeamComplianceSettingService onBeforeUpdate - updates that do not touch the scope", () => {
|
|
1468
|
+
test.each([
|
|
1469
|
+
["switching a rule off", { enabled: false }],
|
|
1470
|
+
["switching a paused rule off again", { enabled: false, options: {} }],
|
|
1471
|
+
["editing the unused options JSON", { options: { note: "x" } }],
|
|
1472
|
+
])(
|
|
1473
|
+
"%s reads nothing and passes the update through untouched",
|
|
1474
|
+
async (_label: string, data: Record<string, unknown>) => {
|
|
1475
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
1476
|
+
|
|
1477
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
1478
|
+
...data,
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
const result: OnUpdate<TeamComplianceSetting> =
|
|
1482
|
+
await onBeforeUpdate(updateBy);
|
|
1483
|
+
|
|
1484
|
+
expect(result.updateBy).toBe(updateBy);
|
|
1485
|
+
expect(result.carryForward).toBeNull();
|
|
1486
|
+
expect(dataOf(result.updateBy)).toEqual(data);
|
|
1487
|
+
expectNoReads();
|
|
1488
|
+
},
|
|
1489
|
+
);
|
|
1490
|
+
|
|
1491
|
+
test("a disable never fails because of the rule's configuration - even on a rule whose stored type is garbage", async () => {
|
|
1492
|
+
store({ id: SETTING_ID, ruleType: "RequireTwoFactorAuth" });
|
|
1493
|
+
|
|
1494
|
+
await expect(update({ enabled: false })).resolves.toBeDefined();
|
|
1495
|
+
expectNoReads();
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
test("a disable never fails on a rule a severity delete emptied either, and reads nothing", async () => {
|
|
1499
|
+
store({ ...EMPTIED_BY_DELETE });
|
|
1500
|
+
|
|
1501
|
+
await expect(update({ enabled: false })).resolves.toBeDefined();
|
|
1502
|
+
expectNoReads();
|
|
1503
|
+
});
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
/*
|
|
1507
|
+
* ---------------------------------------------------------------------------
|
|
1508
|
+
* Rules a severity delete left with nothing to check.
|
|
1509
|
+
* ---------------------------------------------------------------------------
|
|
1510
|
+
*
|
|
1511
|
+
* pauseRulesLeftWithoutSeverities pauses such a rule and marks it
|
|
1512
|
+
* (options.severitiesDeleted). With no severities left it is stored exactly
|
|
1513
|
+
* like a rule for EVERY severity of its kind, so turned back on as it is it
|
|
1514
|
+
* would check every severity - and it would look like a duplicate of the
|
|
1515
|
+
* team's real "every severity" rule of the same type and channel.
|
|
1516
|
+
*/
|
|
1517
|
+
|
|
1518
|
+
describe("TeamComplianceSettingService onBeforeUpdate - turning a rule on", () => {
|
|
1519
|
+
test("a rule a severity delete emptied cannot be turned back on as it is", async () => {
|
|
1520
|
+
store({ ...EMPTIED_BY_DELETE });
|
|
1521
|
+
|
|
1522
|
+
await expect(update({ enabled: true })).rejects.toThrow(
|
|
1523
|
+
new BadDataException(SEVERITIES_DELETED_ENABLE_MESSAGE),
|
|
1524
|
+
);
|
|
1525
|
+
expect(SEVERITIES_DELETED_ENABLE_MESSAGE).toBe(
|
|
1526
|
+
"Every severity this rule was scoped to has been deleted. Edit the rule to choose new severities before turning it back on.",
|
|
1527
|
+
);
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
test("an ordinary rule - scoped, or for every severity - is turned on", async () => {
|
|
1531
|
+
store(INCIDENT_CALL_CRITICAL, {
|
|
1532
|
+
id: SIBLING_SETTING_ID,
|
|
1533
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1534
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1535
|
+
enabled: false,
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
1539
|
+
enabled: true,
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
await expect(onBeforeUpdate(updateBy)).resolves.toBeDefined();
|
|
1543
|
+
expect(dataOf(updateBy)).toEqual({ enabled: true });
|
|
1544
|
+
|
|
1545
|
+
await expect(
|
|
1546
|
+
onBeforeUpdate(
|
|
1547
|
+
updateByFor({ enabled: true }, { query: { _id: SIBLING_SETTING_ID } }),
|
|
1548
|
+
),
|
|
1549
|
+
).resolves.toBeDefined();
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
test("reads only the rows being turned on, inside the caller's project, as root, with what the check needs", async () => {
|
|
1553
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
1554
|
+
|
|
1555
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
1556
|
+
enabled: true,
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
await onBeforeUpdate(updateBy);
|
|
1560
|
+
|
|
1561
|
+
expect(settingsFindBy).toHaveBeenCalledTimes(1);
|
|
1562
|
+
|
|
1563
|
+
const read: JSONObject = findByCalls()[0]!;
|
|
1564
|
+
expect(read["query"]).toEqual({ _id: SETTING_ID, projectId: PROJECT_ID });
|
|
1565
|
+
expect(read["select"]).toEqual({
|
|
1566
|
+
_id: true,
|
|
1567
|
+
ruleType: true,
|
|
1568
|
+
options: true,
|
|
1569
|
+
incidentSeverities: { _id: true },
|
|
1570
|
+
alertSeverities: { _id: true },
|
|
1571
|
+
});
|
|
1572
|
+
expect(read["limit"]).toBe(LIMIT_PER_PROJECT);
|
|
1573
|
+
expect(read["props"]).toEqual({ isRoot: true });
|
|
1574
|
+
// The caller's own query object is left alone.
|
|
1575
|
+
expect(updateBy.query).toEqual({ _id: SETTING_ID });
|
|
1576
|
+
expect(incidentSeverityCountBy).not.toHaveBeenCalled();
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
test("a marked rule in another project is not read, so it does not block the caller", async () => {
|
|
1580
|
+
store({
|
|
1581
|
+
...EMPTIED_BY_DELETE,
|
|
1582
|
+
projectId: OTHER_PROJECT_ID,
|
|
1583
|
+
teamId: OTHER_TEAM_ID,
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
await expect(update({ enabled: true })).resolves.toBeDefined();
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
test("root is held to it too: nothing may turn such a rule into an every-severity rule", async () => {
|
|
1590
|
+
store({ ...EMPTIED_BY_DELETE });
|
|
1591
|
+
|
|
1592
|
+
await expect(
|
|
1593
|
+
onBeforeUpdate(
|
|
1594
|
+
updateByFor({ enabled: true }, { props: { isRoot: true } }),
|
|
1595
|
+
),
|
|
1596
|
+
).rejects.toThrow(SEVERITIES_DELETED_ENABLE_MESSAGE);
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
test("a bulk switch-on is refused when any row it matches was emptied", async () => {
|
|
1600
|
+
store(INCIDENT_CALL_CRITICAL, {
|
|
1601
|
+
...EMPTIED_BY_DELETE,
|
|
1602
|
+
id: SIBLING_SETTING_ID,
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
await expect(
|
|
1606
|
+
onBeforeUpdate(
|
|
1607
|
+
updateByFor(
|
|
1608
|
+
{ enabled: true },
|
|
1609
|
+
{ query: { teamId: TEAM_ID }, props: { isRoot: true } },
|
|
1610
|
+
),
|
|
1611
|
+
),
|
|
1612
|
+
).rejects.toThrow(SEVERITIES_DELETED_ENABLE_MESSAGE);
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
test.each<[string, StoredRuleInput]>([
|
|
1616
|
+
[
|
|
1617
|
+
"a stale mark on a rule that has severities again",
|
|
1618
|
+
{ ...EMPTIED_BY_DELETE, incidentSeverityIds: [MAJOR_INCIDENT] },
|
|
1619
|
+
],
|
|
1620
|
+
[
|
|
1621
|
+
"a mark on a method rule, which has no severities to lose",
|
|
1622
|
+
{
|
|
1623
|
+
id: SETTING_ID,
|
|
1624
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
1625
|
+
enabled: false,
|
|
1626
|
+
options: { [SEVERITIES_DELETED_OPTION]: true },
|
|
1627
|
+
},
|
|
1628
|
+
],
|
|
1629
|
+
[
|
|
1630
|
+
"a mark that is not `true`",
|
|
1631
|
+
{ ...EMPTIED_BY_DELETE, options: { [SEVERITIES_DELETED_OPTION]: "yes" } },
|
|
1632
|
+
],
|
|
1633
|
+
])(
|
|
1634
|
+
"%s does not block turning the rule on",
|
|
1635
|
+
async (_label: string, row: StoredRuleInput) => {
|
|
1636
|
+
store(row);
|
|
1637
|
+
|
|
1638
|
+
await expect(update({ enabled: true })).resolves.toBeDefined();
|
|
1639
|
+
},
|
|
1640
|
+
);
|
|
1641
|
+
|
|
1642
|
+
test("an alert rule emptied by an alert severity delete is held to it too", async () => {
|
|
1643
|
+
store({
|
|
1644
|
+
id: SETTING_ID,
|
|
1645
|
+
ruleType: ComplianceRuleType.HasAlertEpisodeOnCallRules,
|
|
1646
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1647
|
+
enabled: false,
|
|
1648
|
+
options: { [SEVERITIES_DELETED_OPTION]: true },
|
|
1649
|
+
// A stray severity of the other kind is not a scope.
|
|
1650
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
await expect(update({ enabled: true })).rejects.toThrow(
|
|
1654
|
+
SEVERITIES_DELETED_ENABLE_MESSAGE,
|
|
1655
|
+
);
|
|
1656
|
+
});
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
describe("TeamComplianceSettingService onBeforeUpdate - re-scoping a rule a severity delete emptied", () => {
|
|
1660
|
+
test("the edit form's save - new severities, and enabled - is accepted and clears the mark in the same write", async () => {
|
|
1661
|
+
store({ ...EMPTIED_BY_DELETE });
|
|
1662
|
+
|
|
1663
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
1664
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1665
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1666
|
+
incidentSeverities: [{ _id: MAJOR_INCIDENT }],
|
|
1667
|
+
alertSeverities: [],
|
|
1668
|
+
enabled: true,
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
await expect(onBeforeUpdate(updateBy)).resolves.toBeDefined();
|
|
1672
|
+
|
|
1673
|
+
expect(dataOf(updateBy)["enabled"]).toBe(true);
|
|
1674
|
+
expect(dataOf(updateBy)["options"]).toBeNull();
|
|
1675
|
+
expect(
|
|
1676
|
+
writtenIds(dataOf(updateBy)["incidentSeverities"], IncidentSeverity),
|
|
1677
|
+
).toEqual([MAJOR_INCIDENT]);
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1680
|
+
test.each<[string, Record<string, unknown>]>([
|
|
1681
|
+
["new severities", { incidentSeverities: [MAJOR_INCIDENT] }],
|
|
1682
|
+
[
|
|
1683
|
+
"deliberately every severity, from the edit form",
|
|
1684
|
+
{ incidentSeverities: [], enabled: true },
|
|
1685
|
+
],
|
|
1686
|
+
[
|
|
1687
|
+
"a new channel",
|
|
1688
|
+
{ notificationChannel: ComplianceNotificationChannel.Push },
|
|
1689
|
+
],
|
|
1690
|
+
[
|
|
1691
|
+
"a new rule type",
|
|
1692
|
+
{ ruleType: ComplianceRuleType.HasNotificationEmailMethod },
|
|
1693
|
+
],
|
|
1694
|
+
])(
|
|
1695
|
+
"%s clears the mark",
|
|
1696
|
+
async (_label: string, data: Record<string, unknown>) => {
|
|
1697
|
+
store({ ...EMPTIED_BY_DELETE });
|
|
1698
|
+
|
|
1699
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
1700
|
+
...data,
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
await onBeforeUpdate(updateBy);
|
|
1704
|
+
|
|
1705
|
+
expect(dataOf(updateBy)["options"]).toBeNull();
|
|
1706
|
+
},
|
|
1707
|
+
);
|
|
1708
|
+
|
|
1709
|
+
test("every other option the rule carries is kept", async () => {
|
|
1710
|
+
store({
|
|
1711
|
+
...EMPTIED_BY_DELETE,
|
|
1712
|
+
options: { [SEVERITIES_DELETED_OPTION]: true, note: "keep me" },
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
1716
|
+
incidentSeverities: [MAJOR_INCIDENT],
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
await onBeforeUpdate(updateBy);
|
|
1720
|
+
|
|
1721
|
+
expect(dataOf(updateBy)["options"]).toEqual({ note: "keep me" });
|
|
1722
|
+
});
|
|
1723
|
+
|
|
1724
|
+
test("options sent with the re-scope are written, without the mark", async () => {
|
|
1725
|
+
store({ ...EMPTIED_BY_DELETE });
|
|
1726
|
+
|
|
1727
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
1728
|
+
incidentSeverities: [MAJOR_INCIDENT],
|
|
1729
|
+
options: { [SEVERITIES_DELETED_OPTION]: true, note: "sent" },
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
await onBeforeUpdate(updateBy);
|
|
1733
|
+
|
|
1734
|
+
expect(dataOf(updateBy)["options"]).toEqual({ note: "sent" });
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
test("a re-scope of an unmarked rule leaves its options alone", async () => {
|
|
1738
|
+
store({ ...INCIDENT_CALL_CRITICAL, options: { note: "untouched" } });
|
|
1739
|
+
|
|
1740
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
1741
|
+
incidentSeverities: [MAJOR_INCIDENT],
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
await onBeforeUpdate(updateBy);
|
|
1745
|
+
|
|
1746
|
+
expect("options" in dataOf(updateBy)).toBe(false);
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
test("a re-scope over rows that disagree on their options is refused rather than overwrite one row's options with another's", async () => {
|
|
1750
|
+
store(
|
|
1751
|
+
{ ...EMPTIED_BY_DELETE },
|
|
1752
|
+
{
|
|
1753
|
+
...INCIDENT_CALL_CRITICAL,
|
|
1754
|
+
id: SIBLING_SETTING_ID,
|
|
1755
|
+
notificationChannel: ComplianceNotificationChannel.SMS,
|
|
1756
|
+
options: { note: "the sibling's own" },
|
|
1757
|
+
},
|
|
1758
|
+
);
|
|
1759
|
+
|
|
1760
|
+
await expect(
|
|
1761
|
+
onBeforeUpdate(
|
|
1762
|
+
updateByFor(
|
|
1763
|
+
{ incidentSeverities: [MINOR_INCIDENT] },
|
|
1764
|
+
{ query: { teamId: TEAM_ID }, props: { isRoot: true } },
|
|
1765
|
+
),
|
|
1766
|
+
),
|
|
1767
|
+
).rejects.toThrow(new BadDataException(OPTIONS_DIFFER_MESSAGE));
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
test("the re-scoped rule is still checked for duplicates, and refused like any other", async () => {
|
|
1771
|
+
store(
|
|
1772
|
+
{ ...EMPTIED_BY_DELETE },
|
|
1773
|
+
{
|
|
1774
|
+
...INCIDENT_CALL_CRITICAL,
|
|
1775
|
+
id: SIBLING_SETTING_ID,
|
|
1776
|
+
incidentSeverityIds: [MAJOR_INCIDENT],
|
|
1777
|
+
},
|
|
1778
|
+
);
|
|
1779
|
+
|
|
1780
|
+
await expect(
|
|
1781
|
+
update({ incidentSeverities: [MAJOR_INCIDENT], enabled: true }),
|
|
1782
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
1783
|
+
});
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1786
|
+
describe("TeamComplianceSettingService - a rule a severity delete emptied duplicates nothing", () => {
|
|
1787
|
+
test("the team's real 'every severity' rule of the same type and channel can still be saved from its edit form", async () => {
|
|
1788
|
+
// What the delete left: no severities, paused, marked.
|
|
1789
|
+
store(
|
|
1790
|
+
{ ...EMPTIED_BY_DELETE, id: SIBLING_SETTING_ID },
|
|
1791
|
+
{
|
|
1792
|
+
id: SETTING_ID,
|
|
1793
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1794
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1795
|
+
},
|
|
1796
|
+
);
|
|
1797
|
+
|
|
1798
|
+
await expect(
|
|
1799
|
+
update({
|
|
1800
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1801
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1802
|
+
incidentSeverities: [],
|
|
1803
|
+
alertSeverities: [],
|
|
1804
|
+
enabled: false,
|
|
1805
|
+
}),
|
|
1806
|
+
).resolves.toBeDefined();
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
test("a new 'every severity' rule of the same type and channel may be added beside it", async () => {
|
|
1810
|
+
store({ ...EMPTIED_BY_DELETE, id: SIBLING_SETTING_ID });
|
|
1811
|
+
|
|
1812
|
+
await expect(
|
|
1813
|
+
create({
|
|
1814
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1815
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1816
|
+
}),
|
|
1817
|
+
).resolves.toBeDefined();
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
test("an unmarked rule with no severities - a real 'every severity' rule, paused or not - is still a duplicate", async () => {
|
|
1821
|
+
store({
|
|
1822
|
+
...EMPTIED_BY_DELETE,
|
|
1823
|
+
id: SIBLING_SETTING_ID,
|
|
1824
|
+
options: undefined,
|
|
1825
|
+
});
|
|
1826
|
+
|
|
1827
|
+
await expect(
|
|
1828
|
+
create({
|
|
1829
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1830
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1831
|
+
}),
|
|
1832
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
test("a stale mark on a rule that has severities again does not hide it from the duplicate check", async () => {
|
|
1836
|
+
store({
|
|
1837
|
+
...EMPTIED_BY_DELETE,
|
|
1838
|
+
id: SIBLING_SETTING_ID,
|
|
1839
|
+
incidentSeverityIds: [MAJOR_INCIDENT],
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
await expect(
|
|
1843
|
+
create({
|
|
1844
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1845
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1846
|
+
incidentSeverities: [MAJOR_INCIDENT],
|
|
1847
|
+
}),
|
|
1848
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
1849
|
+
});
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
describe("TeamComplianceSettingService onBeforeUpdate - refusals before any read", () => {
|
|
1853
|
+
test.each([
|
|
1854
|
+
["an unknown rule type", { ruleType: "RequireTwoFactorAuth" }],
|
|
1855
|
+
["a null rule type", { ruleType: null }],
|
|
1856
|
+
["an unknown channel", { notificationChannel: "Pager" }],
|
|
1857
|
+
["a channel in the wrong case", { notificationChannel: "push" }],
|
|
1858
|
+
])("refuses %s", async (_label: string, data: Record<string, unknown>) => {
|
|
1859
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
1860
|
+
|
|
1861
|
+
await expect(update(data)).rejects.toThrow(BadDataException);
|
|
1862
|
+
expectNoReads();
|
|
1863
|
+
});
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
describe("TeamComplianceSettingService onBeforeUpdate - scope changes", () => {
|
|
1867
|
+
test("reads the rows being changed inside the caller's project, as root, with what their scope needs", async () => {
|
|
1868
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
1869
|
+
|
|
1870
|
+
await update({ notificationChannel: ComplianceNotificationChannel.Push });
|
|
1871
|
+
|
|
1872
|
+
const existingRead: JSONObject = findByCalls()[0]!;
|
|
1873
|
+
expect(existingRead["query"]).toEqual({
|
|
1874
|
+
_id: SETTING_ID,
|
|
1875
|
+
projectId: PROJECT_ID,
|
|
1876
|
+
});
|
|
1877
|
+
expect(existingRead["select"]).toEqual({
|
|
1878
|
+
_id: true,
|
|
1879
|
+
teamId: true,
|
|
1880
|
+
projectId: true,
|
|
1881
|
+
ruleType: true,
|
|
1882
|
+
notificationChannel: true,
|
|
1883
|
+
options: true,
|
|
1884
|
+
incidentSeverities: { _id: true },
|
|
1885
|
+
alertSeverities: { _id: true },
|
|
1886
|
+
});
|
|
1887
|
+
expect(existingRead["limit"]).toBe(LIMIT_PER_PROJECT);
|
|
1888
|
+
expect(existingRead["skip"]).toBe(0);
|
|
1889
|
+
expect(existingRead["props"]).toEqual({ isRoot: true });
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
test("does not copy the caller's own query object when adding the project", async () => {
|
|
1893
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
1894
|
+
|
|
1895
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
1896
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1897
|
+
});
|
|
1898
|
+
|
|
1899
|
+
await onBeforeUpdate(updateBy);
|
|
1900
|
+
|
|
1901
|
+
expect(updateBy.query).toEqual({ _id: SETTING_ID });
|
|
1902
|
+
});
|
|
1903
|
+
|
|
1904
|
+
test("a rule in another project is invisible: nothing is checked, nothing leaks", async () => {
|
|
1905
|
+
store({
|
|
1906
|
+
...INCIDENT_CALL_CRITICAL,
|
|
1907
|
+
id: OTHER_PROJECT_SETTING_ID,
|
|
1908
|
+
projectId: OTHER_PROJECT_ID,
|
|
1909
|
+
});
|
|
1910
|
+
// A rule in the caller's project that the other one would duplicate.
|
|
1911
|
+
store({
|
|
1912
|
+
...INCIDENT_CALL_CRITICAL,
|
|
1913
|
+
id: SIBLING_SETTING_ID,
|
|
1914
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
await expect(
|
|
1918
|
+
onBeforeUpdate(
|
|
1919
|
+
updateByFor(
|
|
1920
|
+
{ notificationChannel: ComplianceNotificationChannel.Push },
|
|
1921
|
+
{ query: { _id: OTHER_PROJECT_SETTING_ID } },
|
|
1922
|
+
),
|
|
1923
|
+
),
|
|
1924
|
+
).resolves.toBeDefined();
|
|
1925
|
+
|
|
1926
|
+
expect(findByCalls()[0]?.["query"]).toEqual({
|
|
1927
|
+
_id: OTHER_PROJECT_SETTING_ID,
|
|
1928
|
+
projectId: PROJECT_ID,
|
|
1929
|
+
});
|
|
1930
|
+
expect(siblingReads()).toEqual([]);
|
|
1931
|
+
expect(incidentSeverityCountBy).not.toHaveBeenCalled();
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
test("a root update without a tenant reads by the update's own query", async () => {
|
|
1935
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
1936
|
+
|
|
1937
|
+
await onBeforeUpdate(
|
|
1938
|
+
updateByFor(
|
|
1939
|
+
{ notificationChannel: ComplianceNotificationChannel.Push },
|
|
1940
|
+
{ props: { isRoot: true } },
|
|
1941
|
+
),
|
|
1942
|
+
);
|
|
1943
|
+
|
|
1944
|
+
expect(findByCalls()[0]?.["query"]).toEqual({ _id: SETTING_ID });
|
|
1945
|
+
// The row's own project scopes the checks.
|
|
1946
|
+
expect(siblingReads()[0]?.["query"]).toMatchObject({
|
|
1947
|
+
teamId: TEAM_ID,
|
|
1948
|
+
projectId: PROJECT_ID,
|
|
1949
|
+
});
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
test("compares against the team's other rules, never against the row itself", async () => {
|
|
1953
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
1954
|
+
|
|
1955
|
+
await update({ notificationChannel: ComplianceNotificationChannel.Push });
|
|
1956
|
+
|
|
1957
|
+
const siblings: Array<JSONObject> = siblingReads();
|
|
1958
|
+
expect(siblings).toHaveLength(1);
|
|
1959
|
+
|
|
1960
|
+
const query: JSONObject = siblings[0]!["query"] as JSONObject;
|
|
1961
|
+
expect(String(query["teamId"])).toBe(TEAM_ID.toString());
|
|
1962
|
+
expect(String(query["projectId"])).toBe(PROJECT_ID.toString());
|
|
1963
|
+
expect(query["ruleType"]).toBe(ComplianceRuleType.HasIncidentOnCallRules);
|
|
1964
|
+
|
|
1965
|
+
const excluded: unknown = query["_id"];
|
|
1966
|
+
expect(excluded).toBeInstanceOf(FindOperator);
|
|
1967
|
+
expect(
|
|
1968
|
+
Object.values(
|
|
1969
|
+
(excluded as FindOperator<unknown>).objectLiteralParameters || {},
|
|
1970
|
+
),
|
|
1971
|
+
).toEqual([SETTING_ID]);
|
|
1972
|
+
expect(siblings[0]!["props"]).toEqual({ isRoot: true });
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
test("re-saving a rule unchanged - what the edit form does - is not a duplicate of itself", async () => {
|
|
1976
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
1977
|
+
|
|
1978
|
+
await expect(
|
|
1979
|
+
update({
|
|
1980
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
1981
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
1982
|
+
incidentSeverities: [{ _id: CRITICAL_INCIDENT }],
|
|
1983
|
+
alertSeverities: [],
|
|
1984
|
+
enabled: true,
|
|
1985
|
+
}),
|
|
1986
|
+
).resolves.toBeDefined();
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
test("refuses a change that makes the rule identical to another rule of the team", async () => {
|
|
1990
|
+
store(INCIDENT_CALL_CRITICAL, {
|
|
1991
|
+
...INCIDENT_CALL_CRITICAL,
|
|
1992
|
+
id: SIBLING_SETTING_ID,
|
|
1993
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
await expect(
|
|
1997
|
+
update({ notificationChannel: ComplianceNotificationChannel.Push }),
|
|
1998
|
+
).rejects.toThrow(new BadDataException(DUPLICATE_COMPLIANCE_RULE_MESSAGE));
|
|
1999
|
+
});
|
|
2000
|
+
|
|
2001
|
+
test("the scope is the update merged over what is stored: a severity change keeps the stored channel", async () => {
|
|
2002
|
+
store(INCIDENT_CALL_CRITICAL, {
|
|
2003
|
+
...INCIDENT_CALL_CRITICAL,
|
|
2004
|
+
id: SIBLING_SETTING_ID,
|
|
2005
|
+
incidentSeverityIds: [MAJOR_INCIDENT],
|
|
2006
|
+
});
|
|
2007
|
+
|
|
2008
|
+
// Call + [Major] exists already; changing only the severities collides.
|
|
2009
|
+
await expect(
|
|
2010
|
+
update({ incidentSeverities: [MAJOR_INCIDENT] }),
|
|
2011
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
2012
|
+
|
|
2013
|
+
// Call + [Minor] does not.
|
|
2014
|
+
await expect(
|
|
2015
|
+
update({ incidentSeverities: [MINOR_INCIDENT] }),
|
|
2016
|
+
).resolves.toBeDefined();
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
test("a channel change keeps the stored severities", async () => {
|
|
2020
|
+
store(INCIDENT_CALL_CRITICAL, {
|
|
2021
|
+
id: SIBLING_SETTING_ID,
|
|
2022
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2023
|
+
notificationChannel: ComplianceNotificationChannel.SMS,
|
|
2024
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
await expect(
|
|
2028
|
+
update({ notificationChannel: ComplianceNotificationChannel.SMS }),
|
|
2029
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
test("clearing the channel back to 'any channel' is a scope change and is checked", async () => {
|
|
2033
|
+
store(INCIDENT_CALL_CRITICAL, {
|
|
2034
|
+
id: SIBLING_SETTING_ID,
|
|
2035
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2036
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
2037
|
+
});
|
|
2038
|
+
|
|
2039
|
+
await expect(update({ notificationChannel: null })).rejects.toThrow(
|
|
2040
|
+
DUPLICATE_COMPLIANCE_RULE_MESSAGE,
|
|
2041
|
+
);
|
|
2042
|
+
});
|
|
2043
|
+
|
|
2044
|
+
test("new severities are validated against the rule's project", async () => {
|
|
2045
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
2046
|
+
|
|
2047
|
+
await expect(
|
|
2048
|
+
update({ incidentSeverities: [FOREIGN_INCIDENT] }),
|
|
2049
|
+
).rejects.toThrow(new BadDataException(FOREIGN_INCIDENT_SEVERITY_MESSAGE));
|
|
2050
|
+
|
|
2051
|
+
expect(countCalls(incidentSeverityCountBy)[0]?.["query"]).toMatchObject({
|
|
2052
|
+
projectId: PROJECT_ID,
|
|
2053
|
+
});
|
|
2054
|
+
expect(siblingReads()).toEqual([]);
|
|
2055
|
+
});
|
|
2056
|
+
|
|
2057
|
+
test("an invalid severity id in an update is refused", async () => {
|
|
2058
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
2059
|
+
|
|
2060
|
+
await expect(
|
|
2061
|
+
update({ incidentSeverities: ["not-a-uuid"] }),
|
|
2062
|
+
).rejects.toThrow(/Invalid ID format/);
|
|
2063
|
+
});
|
|
2064
|
+
|
|
2065
|
+
test("a rule whose stored type is not recognised is skipped, not failed", async () => {
|
|
2066
|
+
store({
|
|
2067
|
+
id: SETTING_ID,
|
|
2068
|
+
ruleType: "RequireTwoFactorAuth",
|
|
2069
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
2070
|
+
});
|
|
2071
|
+
|
|
2072
|
+
await expect(
|
|
2073
|
+
update({ notificationChannel: ComplianceNotificationChannel.Push }),
|
|
2074
|
+
).resolves.toBeDefined();
|
|
2075
|
+
|
|
2076
|
+
expect(siblingReads()).toEqual([]);
|
|
2077
|
+
expect(incidentSeverityCountBy).not.toHaveBeenCalled();
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
/*
|
|
2081
|
+
* The relation save writes a sent list to the row whatever its type, so
|
|
2082
|
+
* the list is checked by its own kind against the row's project even when
|
|
2083
|
+
* the row's type is one this build does not recognise.
|
|
2084
|
+
*/
|
|
2085
|
+
test.each<[string, Record<string, unknown>, string]>([
|
|
2086
|
+
[
|
|
2087
|
+
"incident",
|
|
2088
|
+
{ incidentSeverities: [{ _id: FOREIGN_INCIDENT }] },
|
|
2089
|
+
FOREIGN_INCIDENT_SEVERITY_MESSAGE,
|
|
2090
|
+
],
|
|
2091
|
+
[
|
|
2092
|
+
"alert",
|
|
2093
|
+
{ alertSeverities: [{ _id: FOREIGN_ALERT }] },
|
|
2094
|
+
FOREIGN_ALERT_SEVERITY_MESSAGE,
|
|
2095
|
+
],
|
|
2096
|
+
])(
|
|
2097
|
+
"another project's %s severity sent to a rule whose stored type is not recognised is refused",
|
|
2098
|
+
async (_label: string, data: Record<string, unknown>, message: string) => {
|
|
2099
|
+
store({ id: SETTING_ID, ruleType: "LegacyRule" });
|
|
2100
|
+
|
|
2101
|
+
await expect(update({ ...data })).rejects.toThrow(
|
|
2102
|
+
new BadDataException(message),
|
|
2103
|
+
);
|
|
2104
|
+
expect(siblingReads()).toEqual([]);
|
|
2105
|
+
},
|
|
2106
|
+
);
|
|
2107
|
+
|
|
2108
|
+
test("this project's severities sent to such a rule are checked against the row's project and accepted", async () => {
|
|
2109
|
+
store({ id: SETTING_ID, ruleType: "LegacyRule" });
|
|
2110
|
+
|
|
2111
|
+
await expect(
|
|
2112
|
+
update({
|
|
2113
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
2114
|
+
alertSeverities: [CRITICAL_ALERT],
|
|
2115
|
+
}),
|
|
2116
|
+
).resolves.toBeDefined();
|
|
2117
|
+
|
|
2118
|
+
expect(
|
|
2119
|
+
countCalls(incidentSeverityCountBy).map((call: JSONObject) => {
|
|
2120
|
+
return [
|
|
2121
|
+
String((call["query"] as JSONObject)["projectId"]),
|
|
2122
|
+
includedIds(call["query"] as JSONObject),
|
|
2123
|
+
];
|
|
2124
|
+
}),
|
|
2125
|
+
).toEqual([[PROJECT_ID.toString(), [CRITICAL_INCIDENT]]]);
|
|
2126
|
+
expect(
|
|
2127
|
+
countCalls(alertSeverityCountBy).map((call: JSONObject) => {
|
|
2128
|
+
return includedIds(call["query"] as JSONObject);
|
|
2129
|
+
}),
|
|
2130
|
+
).toEqual([[CRITICAL_ALERT]]);
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
test("a root update over rows of two projects checks a sent list against each project, once per project", async () => {
|
|
2134
|
+
store(
|
|
2135
|
+
{ id: SETTING_ID, ruleType: "LegacyRule" },
|
|
2136
|
+
{
|
|
2137
|
+
id: OTHER_PROJECT_SETTING_ID,
|
|
2138
|
+
ruleType: "LegacyRule",
|
|
2139
|
+
projectId: OTHER_PROJECT_ID,
|
|
2140
|
+
teamId: OTHER_TEAM_ID,
|
|
2141
|
+
},
|
|
2142
|
+
{ id: SIBLING_SETTING_ID, ruleType: "LegacyRule" },
|
|
2143
|
+
);
|
|
2144
|
+
|
|
2145
|
+
await expect(
|
|
2146
|
+
onBeforeUpdate(
|
|
2147
|
+
updateByFor(
|
|
2148
|
+
{ incidentSeverities: [CRITICAL_INCIDENT] },
|
|
2149
|
+
{ query: { ruleType: "LegacyRule" }, props: { isRoot: true } },
|
|
2150
|
+
),
|
|
2151
|
+
),
|
|
2152
|
+
).rejects.toThrow(FOREIGN_INCIDENT_SEVERITY_MESSAGE);
|
|
2153
|
+
|
|
2154
|
+
expect(
|
|
2155
|
+
countCalls(incidentSeverityCountBy).map((call: JSONObject): string => {
|
|
2156
|
+
return String((call["query"] as JSONObject)["projectId"]);
|
|
2157
|
+
}),
|
|
2158
|
+
).toEqual([PROJECT_ID.toString(), OTHER_PROJECT_ID.toString()]);
|
|
2159
|
+
});
|
|
2160
|
+
|
|
2161
|
+
test("a list the update sends is checked once, not again as the row's scope", async () => {
|
|
2162
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
2163
|
+
|
|
2164
|
+
await update({ incidentSeverities: [MAJOR_INCIDENT] });
|
|
2165
|
+
|
|
2166
|
+
expect(incidentSeverityCountBy).toHaveBeenCalledTimes(1);
|
|
2167
|
+
});
|
|
2168
|
+
|
|
2169
|
+
test.each([
|
|
2170
|
+
["no team", { omitTeam: true }],
|
|
2171
|
+
["no project", { omitProject: true }],
|
|
2172
|
+
])(
|
|
2173
|
+
"a stored row with %s is skipped",
|
|
2174
|
+
async (_label: string, broken: Partial<StoredRuleInput>) => {
|
|
2175
|
+
store({ ...INCIDENT_CALL_CRITICAL, ...broken });
|
|
2176
|
+
|
|
2177
|
+
await expect(
|
|
2178
|
+
onBeforeUpdate(
|
|
2179
|
+
updateByFor(
|
|
2180
|
+
{ notificationChannel: ComplianceNotificationChannel.Push },
|
|
2181
|
+
{ props: { isRoot: true } },
|
|
2182
|
+
),
|
|
2183
|
+
),
|
|
2184
|
+
).resolves.toBeDefined();
|
|
2185
|
+
|
|
2186
|
+
expect(siblingReads()).toEqual([]);
|
|
2187
|
+
},
|
|
2188
|
+
);
|
|
2189
|
+
|
|
2190
|
+
test("every row an update matches is checked against its own team", async () => {
|
|
2191
|
+
store(INCIDENT_CALL_CRITICAL, {
|
|
2192
|
+
...INCIDENT_CALL_CRITICAL,
|
|
2193
|
+
id: SIBLING_SETTING_ID,
|
|
2194
|
+
teamId: OTHER_TEAM_ID,
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
await onBeforeUpdate(
|
|
2198
|
+
updateByFor(
|
|
2199
|
+
{ notificationChannel: ComplianceNotificationChannel.Push },
|
|
2200
|
+
{
|
|
2201
|
+
query: {
|
|
2202
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2203
|
+
},
|
|
2204
|
+
},
|
|
2205
|
+
),
|
|
2206
|
+
);
|
|
2207
|
+
|
|
2208
|
+
expect(
|
|
2209
|
+
siblingReads().map((read: JSONObject): string => {
|
|
2210
|
+
return String((read["query"] as JSONObject)["teamId"]);
|
|
2211
|
+
}),
|
|
2212
|
+
).toEqual([TEAM_ID.toString(), OTHER_TEAM_ID.toString()]);
|
|
2213
|
+
});
|
|
2214
|
+
});
|
|
2215
|
+
|
|
2216
|
+
describe("TeamComplianceSettingService onBeforeUpdate - a rule type change", () => {
|
|
2217
|
+
test("to a method rule clears the channel and every severity, including the ones already stored", async () => {
|
|
2218
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
2219
|
+
|
|
2220
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
2221
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
2222
|
+
});
|
|
2223
|
+
|
|
2224
|
+
await onBeforeUpdate(updateBy);
|
|
2225
|
+
|
|
2226
|
+
expect(dataOf(updateBy)).toEqual({
|
|
2227
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
2228
|
+
notificationChannel: null,
|
|
2229
|
+
incidentSeverities: [],
|
|
2230
|
+
alertSeverities: [],
|
|
2231
|
+
});
|
|
2232
|
+
// Nothing left to validate; the duplicate lookup is for the new type.
|
|
2233
|
+
expect(incidentSeverityCountBy).not.toHaveBeenCalled();
|
|
2234
|
+
expect(
|
|
2235
|
+
(siblingReads()[0]?.["query"] as JSONObject | undefined)?.["ruleType"],
|
|
2236
|
+
).toBe(ComplianceRuleType.HasNotificationEmailMethod);
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
test("to a method rule collides with an existing rule of that type", async () => {
|
|
2240
|
+
store(INCIDENT_CALL_CRITICAL, {
|
|
2241
|
+
id: SIBLING_SETTING_ID,
|
|
2242
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
await expect(
|
|
2246
|
+
update({ ruleType: ComplianceRuleType.HasNotificationEmailMethod }),
|
|
2247
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
2248
|
+
});
|
|
2249
|
+
|
|
2250
|
+
test("from incident to alert drops the incident severities and keeps the channel", async () => {
|
|
2251
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
2252
|
+
|
|
2253
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
2254
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
await onBeforeUpdate(updateBy);
|
|
2258
|
+
|
|
2259
|
+
expect(dataOf(updateBy)).toEqual({
|
|
2260
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
2261
|
+
incidentSeverities: [],
|
|
2262
|
+
});
|
|
2263
|
+
expect(incidentSeverityCountBy).not.toHaveBeenCalled();
|
|
2264
|
+
expect(alertSeverityCountBy).not.toHaveBeenCalled();
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
test("from incident to alert is judged as 'Call for every alert severity'", async () => {
|
|
2268
|
+
store(INCIDENT_CALL_CRITICAL, {
|
|
2269
|
+
id: SIBLING_SETTING_ID,
|
|
2270
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
2271
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
2272
|
+
});
|
|
2273
|
+
|
|
2274
|
+
await expect(
|
|
2275
|
+
update({ ruleType: ComplianceRuleType.HasAlertOnCallRules }),
|
|
2276
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
2277
|
+
});
|
|
2278
|
+
|
|
2279
|
+
test("from incident to alert with new alert severities validates those", async () => {
|
|
2280
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
2281
|
+
|
|
2282
|
+
await expect(
|
|
2283
|
+
update({
|
|
2284
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
2285
|
+
alertSeverities: [FOREIGN_ALERT],
|
|
2286
|
+
}),
|
|
2287
|
+
).rejects.toThrow(FOREIGN_ALERT_SEVERITY_MESSAGE);
|
|
2288
|
+
|
|
2289
|
+
await expect(
|
|
2290
|
+
update({
|
|
2291
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
2292
|
+
alertSeverities: [CRITICAL_ALERT],
|
|
2293
|
+
}),
|
|
2294
|
+
).resolves.toBeDefined();
|
|
2295
|
+
});
|
|
2296
|
+
|
|
2297
|
+
test("to the incident episode rule keeps the stored incident scope and re-validates it", async () => {
|
|
2298
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
2299
|
+
|
|
2300
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
2301
|
+
ruleType: ComplianceRuleType.HasIncidentEpisodeOnCallRules,
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
await onBeforeUpdate(updateBy);
|
|
2305
|
+
|
|
2306
|
+
expect(dataOf(updateBy)).toEqual({
|
|
2307
|
+
ruleType: ComplianceRuleType.HasIncidentEpisodeOnCallRules,
|
|
2308
|
+
alertSeverities: [],
|
|
2309
|
+
});
|
|
2310
|
+
expect(
|
|
2311
|
+
includedIds(
|
|
2312
|
+
countCalls(incidentSeverityCountBy)[0]!["query"] as JSONObject,
|
|
2313
|
+
),
|
|
2314
|
+
).toEqual([CRITICAL_INCIDENT]);
|
|
2315
|
+
expect(
|
|
2316
|
+
(siblingReads()[0]?.["query"] as JSONObject | undefined)?.["ruleType"],
|
|
2317
|
+
).toBe(ComplianceRuleType.HasIncidentEpisodeOnCallRules);
|
|
2318
|
+
});
|
|
2319
|
+
|
|
2320
|
+
test("an unknown stored type can be repaired by setting a real one", async () => {
|
|
2321
|
+
store({ id: SETTING_ID, ruleType: "RequireTwoFactorAuth" });
|
|
2322
|
+
|
|
2323
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
2324
|
+
ruleType: ComplianceRuleType.HasNotificationPushMethod,
|
|
2325
|
+
});
|
|
2326
|
+
|
|
2327
|
+
await expect(onBeforeUpdate(updateBy)).resolves.toBeDefined();
|
|
2328
|
+
expect(siblingReads()).toHaveLength(1);
|
|
2329
|
+
});
|
|
2330
|
+
});
|
|
2331
|
+
|
|
2332
|
+
describe("TeamComplianceSettingService onBeforeUpdate - options the stored type does not use", () => {
|
|
2333
|
+
test("alert severities sent for an incident rule are dropped, not stored unvalidated", async () => {
|
|
2334
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
2335
|
+
|
|
2336
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
2337
|
+
alertSeverities: [{ _id: FOREIGN_ALERT }],
|
|
2338
|
+
});
|
|
2339
|
+
|
|
2340
|
+
await expect(onBeforeUpdate(updateBy)).resolves.toBeDefined();
|
|
2341
|
+
|
|
2342
|
+
expect(dataOf(updateBy)["alertSeverities"]).toEqual([]);
|
|
2343
|
+
expect(alertSeverityCountBy).not.toHaveBeenCalled();
|
|
2344
|
+
});
|
|
2345
|
+
|
|
2346
|
+
test("a channel sent for a method rule is dropped", async () => {
|
|
2347
|
+
store({
|
|
2348
|
+
id: SETTING_ID,
|
|
2349
|
+
ruleType: ComplianceRuleType.HasNotificationSMSMethod,
|
|
2350
|
+
});
|
|
2351
|
+
|
|
2352
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
2353
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
2354
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
2355
|
+
});
|
|
2356
|
+
|
|
2357
|
+
await onBeforeUpdate(updateBy);
|
|
2358
|
+
|
|
2359
|
+
expect(dataOf(updateBy)).toEqual({
|
|
2360
|
+
notificationChannel: null,
|
|
2361
|
+
incidentSeverities: [],
|
|
2362
|
+
});
|
|
2363
|
+
});
|
|
2364
|
+
|
|
2365
|
+
test("options the type does use are kept, the severities as the ids they name", async () => {
|
|
2366
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
2367
|
+
|
|
2368
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
2369
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
2370
|
+
incidentSeverities: [{ _id: MAJOR_INCIDENT }],
|
|
2371
|
+
});
|
|
2372
|
+
|
|
2373
|
+
await onBeforeUpdate(updateBy);
|
|
2374
|
+
|
|
2375
|
+
expect(Object.keys(dataOf(updateBy)).sort()).toEqual([
|
|
2376
|
+
"incidentSeverities",
|
|
2377
|
+
"notificationChannel",
|
|
2378
|
+
]);
|
|
2379
|
+
expect(dataOf(updateBy)["notificationChannel"]).toBe(
|
|
2380
|
+
ComplianceNotificationChannel.Push,
|
|
2381
|
+
);
|
|
2382
|
+
expect(
|
|
2383
|
+
writtenIds(dataOf(updateBy)["incidentSeverities"], IncidentSeverity),
|
|
2384
|
+
).toEqual([MAJOR_INCIDENT]);
|
|
2385
|
+
});
|
|
2386
|
+
});
|
|
2387
|
+
|
|
2388
|
+
/*
|
|
2389
|
+
* ---------------------------------------------------------------------------
|
|
2390
|
+
* The severities that are checked are the severities that are written.
|
|
2391
|
+
* ---------------------------------------------------------------------------
|
|
2392
|
+
*
|
|
2393
|
+
* The relation save writes whatever DatabaseService.sanitizeCreateOrUpdate
|
|
2394
|
+
* reads out of each list item, and inserts one join row per item. The hooks
|
|
2395
|
+
* used to check their OWN reading of the list and hand the list on as sent,
|
|
2396
|
+
* so the two could disagree: an item the save reads by its `id` was checked
|
|
2397
|
+
* by its `_id` or not at all - which let another project's severity through -
|
|
2398
|
+
* and a repeat the check counted once was inserted twice, which the join
|
|
2399
|
+
* table's primary key refuses.
|
|
2400
|
+
*/
|
|
2401
|
+
|
|
2402
|
+
describe("TeamComplianceSettingService - the severities checked are the severities written", () => {
|
|
2403
|
+
test("create: repeats and case collapse to one lower-case id each, handed to the save as models", async () => {
|
|
2404
|
+
const result: OnCreate<TeamComplianceSetting> = await create({
|
|
2405
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2406
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
2407
|
+
incidentSeverities: [
|
|
2408
|
+
CRITICAL_INCIDENT.toUpperCase(),
|
|
2409
|
+
{ _id: CRITICAL_INCIDENT },
|
|
2410
|
+
new ObjectID(MAJOR_INCIDENT),
|
|
2411
|
+
CRITICAL_INCIDENT,
|
|
2412
|
+
],
|
|
2413
|
+
});
|
|
2414
|
+
|
|
2415
|
+
expect(
|
|
2416
|
+
writtenIds(result.createBy.data.incidentSeverities, IncidentSeverity),
|
|
2417
|
+
).toEqual([CRITICAL_INCIDENT, MAJOR_INCIDENT]);
|
|
2418
|
+
expect(
|
|
2419
|
+
includedIds(
|
|
2420
|
+
countCalls(incidentSeverityCountBy)[0]!["query"] as JSONObject,
|
|
2421
|
+
),
|
|
2422
|
+
).toEqual([CRITICAL_INCIDENT, MAJOR_INCIDENT]);
|
|
2423
|
+
});
|
|
2424
|
+
|
|
2425
|
+
test("create: an alert list is written as alert severities", async () => {
|
|
2426
|
+
const result: OnCreate<TeamComplianceSetting> = await create({
|
|
2427
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
2428
|
+
alertSeverities: [{ id: MAJOR_ALERT }, MAJOR_ALERT.toUpperCase()],
|
|
2429
|
+
});
|
|
2430
|
+
|
|
2431
|
+
expect(
|
|
2432
|
+
writtenIds(result.createBy.data.alertSeverities, AlertSeverity),
|
|
2433
|
+
).toEqual([MAJOR_ALERT]);
|
|
2434
|
+
});
|
|
2435
|
+
|
|
2436
|
+
test("update: the list sent is normalised once, and each row is checked against exactly that", async () => {
|
|
2437
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
2438
|
+
|
|
2439
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor({
|
|
2440
|
+
incidentSeverities: [
|
|
2441
|
+
{ _id: MAJOR_INCIDENT },
|
|
2442
|
+
{ _id: MAJOR_INCIDENT.toUpperCase() },
|
|
2443
|
+
],
|
|
2444
|
+
});
|
|
2445
|
+
|
|
2446
|
+
await onBeforeUpdate(updateBy);
|
|
2447
|
+
|
|
2448
|
+
expect(
|
|
2449
|
+
writtenIds(dataOf(updateBy)["incidentSeverities"], IncidentSeverity),
|
|
2450
|
+
).toEqual([MAJOR_INCIDENT]);
|
|
2451
|
+
expect(
|
|
2452
|
+
includedIds(
|
|
2453
|
+
countCalls(incidentSeverityCountBy)[0]!["query"] as JSONObject,
|
|
2454
|
+
),
|
|
2455
|
+
).toEqual([MAJOR_INCIDENT]);
|
|
2456
|
+
});
|
|
2457
|
+
|
|
2458
|
+
test("update: a multi-row update over rows of both kinds normalises both lists it sends", async () => {
|
|
2459
|
+
store(INCIDENT_CALL_CRITICAL, {
|
|
2460
|
+
id: SIBLING_SETTING_ID,
|
|
2461
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
2462
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
2463
|
+
alertSeverityIds: [CRITICAL_ALERT],
|
|
2464
|
+
});
|
|
2465
|
+
|
|
2466
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor(
|
|
2467
|
+
{
|
|
2468
|
+
incidentSeverities: [MAJOR_INCIDENT, MAJOR_INCIDENT],
|
|
2469
|
+
alertSeverities: [MAJOR_ALERT.toUpperCase(), { _id: MAJOR_ALERT }],
|
|
2470
|
+
},
|
|
2471
|
+
{ query: { teamId: TEAM_ID } },
|
|
2472
|
+
);
|
|
2473
|
+
|
|
2474
|
+
await onBeforeUpdate(updateBy);
|
|
2475
|
+
|
|
2476
|
+
expect(
|
|
2477
|
+
writtenIds(dataOf(updateBy)["incidentSeverities"], IncidentSeverity),
|
|
2478
|
+
).toEqual([MAJOR_INCIDENT]);
|
|
2479
|
+
expect(
|
|
2480
|
+
writtenIds(dataOf(updateBy)["alertSeverities"], AlertSeverity),
|
|
2481
|
+
).toEqual([MAJOR_ALERT]);
|
|
2482
|
+
});
|
|
2483
|
+
|
|
2484
|
+
const READ_BY_ID: Array<[string, JSONObject]> = [
|
|
2485
|
+
["a number _id", { _id: 1, id: FOREIGN_INCIDENT }],
|
|
2486
|
+
["a boolean _id", { _id: true, id: FOREIGN_INCIDENT }],
|
|
2487
|
+
[
|
|
2488
|
+
"an ObjectID _id naming this project's own severity",
|
|
2489
|
+
{
|
|
2490
|
+
_id: new ObjectID(CRITICAL_INCIDENT) as unknown as JSONObject,
|
|
2491
|
+
id: FOREIGN_INCIDENT,
|
|
2492
|
+
},
|
|
2493
|
+
],
|
|
2494
|
+
["an object _id", { _id: {}, id: FOREIGN_INCIDENT }],
|
|
2495
|
+
];
|
|
2496
|
+
|
|
2497
|
+
test.each(READ_BY_ID)(
|
|
2498
|
+
"update: an item with %s is read by its id, as the save reads it - so another project's severity is refused",
|
|
2499
|
+
async (_label: string, item: JSONObject) => {
|
|
2500
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
2501
|
+
|
|
2502
|
+
await expect(update({ incidentSeverities: [item] })).rejects.toThrow(
|
|
2503
|
+
FOREIGN_INCIDENT_SEVERITY_MESSAGE,
|
|
2504
|
+
);
|
|
2505
|
+
|
|
2506
|
+
expect(
|
|
2507
|
+
includedIds(
|
|
2508
|
+
countCalls(incidentSeverityCountBy)[0]!["query"] as JSONObject,
|
|
2509
|
+
),
|
|
2510
|
+
).toEqual([FOREIGN_INCIDENT]);
|
|
2511
|
+
},
|
|
2512
|
+
);
|
|
2513
|
+
|
|
2514
|
+
test.each(READ_BY_ID)(
|
|
2515
|
+
"create: an item with %s is read by its id too",
|
|
2516
|
+
async (_label: string, item: JSONObject) => {
|
|
2517
|
+
await expect(
|
|
2518
|
+
create({
|
|
2519
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2520
|
+
incidentSeverities: [item],
|
|
2521
|
+
}),
|
|
2522
|
+
).rejects.toThrow(FOREIGN_INCIDENT_SEVERITY_MESSAGE);
|
|
2523
|
+
},
|
|
2524
|
+
);
|
|
2525
|
+
|
|
2526
|
+
test("the same trick cannot dodge the duplicate check: a rule is compared by the id that would be written", async () => {
|
|
2527
|
+
store({ ...INCIDENT_CALL_CRITICAL, id: SIBLING_SETTING_ID });
|
|
2528
|
+
|
|
2529
|
+
await expect(
|
|
2530
|
+
create({
|
|
2531
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2532
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
2533
|
+
incidentSeverities: [{ _id: 1, id: CRITICAL_INCIDENT }],
|
|
2534
|
+
}),
|
|
2535
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
2536
|
+
});
|
|
2537
|
+
|
|
2538
|
+
const DROPPED_BY_THE_SAVE: Array<[string, unknown]> = [
|
|
2539
|
+
[
|
|
2540
|
+
"an ObjectID _id and no id (the save drops it: the rule would become 'every severity')",
|
|
2541
|
+
{ _id: new ObjectID(CRITICAL_INCIDENT) },
|
|
2542
|
+
],
|
|
2543
|
+
["a boolean _id and no id", { _id: true }],
|
|
2544
|
+
["an empty object", {}],
|
|
2545
|
+
["a name and no id", { name: "Critical" }],
|
|
2546
|
+
["a number", 7],
|
|
2547
|
+
["null", null],
|
|
2548
|
+
["a nested list", [CRITICAL_INCIDENT]],
|
|
2549
|
+
["a model with no id", new IncidentSeverity()],
|
|
2550
|
+
];
|
|
2551
|
+
|
|
2552
|
+
test.each(DROPPED_BY_THE_SAVE)(
|
|
2553
|
+
"create: an item that names no severity - %s - is refused before anything is read",
|
|
2554
|
+
async (_label: string, item: unknown) => {
|
|
2555
|
+
await expect(
|
|
2556
|
+
create({
|
|
2557
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2558
|
+
incidentSeverities: [CRITICAL_INCIDENT, item],
|
|
2559
|
+
}),
|
|
2560
|
+
).rejects.toThrow(
|
|
2561
|
+
new BadDataException(
|
|
2562
|
+
"Every incident severity of a compliance rule must be a severity id.",
|
|
2563
|
+
),
|
|
2564
|
+
);
|
|
2565
|
+
|
|
2566
|
+
expectNoReads();
|
|
2567
|
+
},
|
|
2568
|
+
);
|
|
2569
|
+
|
|
2570
|
+
test.each(DROPPED_BY_THE_SAVE)(
|
|
2571
|
+
"update: an item that names no severity - %s - is refused, and nothing is checked or written",
|
|
2572
|
+
async (_label: string, item: unknown) => {
|
|
2573
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
2574
|
+
|
|
2575
|
+
await expect(
|
|
2576
|
+
update({ incidentSeverities: [MAJOR_INCIDENT, item] }),
|
|
2577
|
+
).rejects.toThrow(
|
|
2578
|
+
"Every incident severity of a compliance rule must be a severity id.",
|
|
2579
|
+
);
|
|
2580
|
+
|
|
2581
|
+
expect(incidentSeverityCountBy).not.toHaveBeenCalled();
|
|
2582
|
+
expect(siblingReads()).toEqual([]);
|
|
2583
|
+
},
|
|
2584
|
+
);
|
|
2585
|
+
|
|
2586
|
+
test("the refusal names the list's own kind", async () => {
|
|
2587
|
+
await expect(
|
|
2588
|
+
create({
|
|
2589
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
2590
|
+
alertSeverities: [{ _id: true }],
|
|
2591
|
+
}),
|
|
2592
|
+
).rejects.toThrow(
|
|
2593
|
+
"Every alert severity of a compliance rule must be a severity id.",
|
|
2594
|
+
);
|
|
2595
|
+
});
|
|
2596
|
+
|
|
2597
|
+
test.each([
|
|
2598
|
+
["an id string", CRITICAL_INCIDENT],
|
|
2599
|
+
["a single {_id}", { _id: CRITICAL_INCIDENT }],
|
|
2600
|
+
["a number", 3],
|
|
2601
|
+
])(
|
|
2602
|
+
"a severity 'list' that is %s is refused - the save would ignore it, the check read it as every severity",
|
|
2603
|
+
async (_label: string, value: unknown) => {
|
|
2604
|
+
await expect(
|
|
2605
|
+
create({
|
|
2606
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2607
|
+
incidentSeverities: value,
|
|
2608
|
+
}),
|
|
2609
|
+
).rejects.toThrow(
|
|
2610
|
+
"The incident severities of a compliance rule must be a list of severity ids.",
|
|
2611
|
+
);
|
|
2612
|
+
|
|
2613
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
2614
|
+
|
|
2615
|
+
await expect(update({ incidentSeverities: value })).rejects.toThrow(
|
|
2616
|
+
"The incident severities of a compliance rule must be a list of severity ids.",
|
|
2617
|
+
);
|
|
2618
|
+
},
|
|
2619
|
+
);
|
|
2620
|
+
|
|
2621
|
+
test("null is an empty list, and is written as one", async () => {
|
|
2622
|
+
const result: OnCreate<TeamComplianceSetting> = await create({
|
|
2623
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2624
|
+
incidentSeverities: null,
|
|
2625
|
+
});
|
|
2626
|
+
|
|
2627
|
+
expect(result.createBy.data.incidentSeverities).toEqual([]);
|
|
2628
|
+
});
|
|
2629
|
+
|
|
2630
|
+
test("a list that is not sent stays unsent", async () => {
|
|
2631
|
+
const result: OnCreate<TeamComplianceSetting> = await create({
|
|
2632
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2633
|
+
});
|
|
2634
|
+
|
|
2635
|
+
expect(modelValue(result.createBy.data, "incidentSeverities")).toBe(
|
|
2636
|
+
undefined,
|
|
2637
|
+
);
|
|
2638
|
+
expect(modelValue(result.createBy.data, "alertSeverities")).toBe(undefined);
|
|
2639
|
+
});
|
|
2640
|
+
});
|
|
2641
|
+
|
|
2642
|
+
type SanitizeFunction = (
|
|
2643
|
+
data: JSONObject,
|
|
2644
|
+
props: DatabaseCommonInteractionProps,
|
|
2645
|
+
isUpdate: boolean,
|
|
2646
|
+
) => Promise<JSONObject>;
|
|
2647
|
+
|
|
2648
|
+
/*
|
|
2649
|
+
* The valid severity ids DatabaseService.sanitizeCreateOrUpdate - what the
|
|
2650
|
+
* relation save writes from - reads out of an incident severity list,
|
|
2651
|
+
* trimmed and lower-cased.
|
|
2652
|
+
*/
|
|
2653
|
+
const writtenBySave: (list: Array<unknown>) => Promise<Array<string>> = async (
|
|
2654
|
+
list: Array<unknown>,
|
|
2655
|
+
): Promise<Array<string>> => {
|
|
2656
|
+
const service: TeamComplianceSettingServiceClass =
|
|
2657
|
+
new TeamComplianceSettingServiceClass();
|
|
2658
|
+
|
|
2659
|
+
const sanitized: JSONObject = await (
|
|
2660
|
+
service as unknown as { sanitizeCreateOrUpdate: SanitizeFunction }
|
|
2661
|
+
).sanitizeCreateOrUpdate(
|
|
2662
|
+
{ incidentSeverities: list } as JSONObject,
|
|
2663
|
+
{ isRoot: true },
|
|
2664
|
+
true,
|
|
2665
|
+
);
|
|
2666
|
+
|
|
2667
|
+
const written: unknown = sanitized["incidentSeverities"];
|
|
2668
|
+
|
|
2669
|
+
return (Array.isArray(written) ? written : [])
|
|
2670
|
+
.map((item: unknown): string => {
|
|
2671
|
+
return String((item as { _id?: unknown })._id)
|
|
2672
|
+
.trim()
|
|
2673
|
+
.toLowerCase();
|
|
2674
|
+
})
|
|
2675
|
+
.filter((id: string): boolean => {
|
|
2676
|
+
return ObjectID.isValidUUID(id);
|
|
2677
|
+
});
|
|
2678
|
+
};
|
|
2679
|
+
|
|
2680
|
+
const resolveOrNull: (item: unknown) => Array<string> | null = (
|
|
2681
|
+
item: unknown,
|
|
2682
|
+
): Array<string> | null => {
|
|
2683
|
+
try {
|
|
2684
|
+
return TeamComplianceSettingServiceClass.resolveSentSeverityIds(
|
|
2685
|
+
[item],
|
|
2686
|
+
ComplianceSeverityKind.Incident,
|
|
2687
|
+
);
|
|
2688
|
+
} catch {
|
|
2689
|
+
return null;
|
|
2690
|
+
}
|
|
2691
|
+
};
|
|
2692
|
+
|
|
2693
|
+
const MAJOR_INCIDENT_MODEL: IncidentSeverity = new IncidentSeverity();
|
|
2694
|
+
MAJOR_INCIDENT_MODEL._id = MAJOR_INCIDENT;
|
|
2695
|
+
|
|
2696
|
+
describe("TeamComplianceSettingService.resolveSentSeverityIds", () => {
|
|
2697
|
+
test("reads an item exactly as the relation save does: id string or ObjectID, else a string _id, else a string id", () => {
|
|
2698
|
+
const model: IncidentSeverity = new IncidentSeverity();
|
|
2699
|
+
model._id = MINOR_INCIDENT;
|
|
2700
|
+
|
|
2701
|
+
expect(
|
|
2702
|
+
TeamComplianceSettingServiceClass.resolveSentSeverityIds(
|
|
2703
|
+
[
|
|
2704
|
+
CRITICAL_INCIDENT,
|
|
2705
|
+
new ObjectID(MAJOR_INCIDENT),
|
|
2706
|
+
model,
|
|
2707
|
+
{ _id: CRITICAL_ALERT, id: FOREIGN_INCIDENT },
|
|
2708
|
+
{ _id: 1, id: MAJOR_ALERT },
|
|
2709
|
+
{ _id: new ObjectID(FOREIGN_ALERT), id: FOREIGN_INCIDENT },
|
|
2710
|
+
],
|
|
2711
|
+
ComplianceSeverityKind.Incident,
|
|
2712
|
+
),
|
|
2713
|
+
).toEqual([
|
|
2714
|
+
CRITICAL_INCIDENT,
|
|
2715
|
+
MAJOR_INCIDENT,
|
|
2716
|
+
MINOR_INCIDENT,
|
|
2717
|
+
CRITICAL_ALERT,
|
|
2718
|
+
MAJOR_ALERT,
|
|
2719
|
+
FOREIGN_INCIDENT,
|
|
2720
|
+
]);
|
|
2721
|
+
});
|
|
2722
|
+
|
|
2723
|
+
test("trims, lower-cases and de-duplicates, keeping the first occurrence", () => {
|
|
2724
|
+
expect(
|
|
2725
|
+
TeamComplianceSettingServiceClass.resolveSentSeverityIds(
|
|
2726
|
+
[
|
|
2727
|
+
MINOR_INCIDENT,
|
|
2728
|
+
` ${CRITICAL_INCIDENT.toUpperCase()} `,
|
|
2729
|
+
CRITICAL_INCIDENT,
|
|
2730
|
+
{ _id: MINOR_INCIDENT },
|
|
2731
|
+
MAJOR_INCIDENT,
|
|
2732
|
+
],
|
|
2733
|
+
ComplianceSeverityKind.Incident,
|
|
2734
|
+
),
|
|
2735
|
+
).toEqual([MINOR_INCIDENT, CRITICAL_INCIDENT, MAJOR_INCIDENT]);
|
|
2736
|
+
});
|
|
2737
|
+
|
|
2738
|
+
test("null and an empty list are empty", () => {
|
|
2739
|
+
for (const value of [null, []]) {
|
|
2740
|
+
expect(
|
|
2741
|
+
TeamComplianceSettingServiceClass.resolveSentSeverityIds(
|
|
2742
|
+
value,
|
|
2743
|
+
ComplianceSeverityKind.Alert,
|
|
2744
|
+
),
|
|
2745
|
+
).toEqual([]);
|
|
2746
|
+
}
|
|
2747
|
+
});
|
|
2748
|
+
|
|
2749
|
+
test("an id that is not a uuid is refused by name", () => {
|
|
2750
|
+
expect(() => {
|
|
2751
|
+
TeamComplianceSettingServiceClass.resolveSentSeverityIds(
|
|
2752
|
+
["not-a-uuid"],
|
|
2753
|
+
ComplianceSeverityKind.Incident,
|
|
2754
|
+
);
|
|
2755
|
+
}).toThrow(/Invalid ID format: "not-a-uuid"/);
|
|
2756
|
+
|
|
2757
|
+
for (const blank of ["", " "]) {
|
|
2758
|
+
expect(() => {
|
|
2759
|
+
TeamComplianceSettingServiceClass.resolveSentSeverityIds(
|
|
2760
|
+
[blank],
|
|
2761
|
+
ComplianceSeverityKind.Incident,
|
|
2762
|
+
);
|
|
2763
|
+
}).toThrow(/Invalid ID format/);
|
|
2764
|
+
}
|
|
2765
|
+
});
|
|
2766
|
+
|
|
2767
|
+
/*
|
|
2768
|
+
* The property the hooks rely on, against the real sanitizer: whatever id
|
|
2769
|
+
* this reads from an item is the id the save would write for it, and an
|
|
2770
|
+
* item it refuses is one the save would write no severity for at all.
|
|
2771
|
+
*/
|
|
2772
|
+
test.each<[string, unknown]>([
|
|
2773
|
+
["an id string", CRITICAL_INCIDENT],
|
|
2774
|
+
["an upper-case id string", MAJOR_INCIDENT.toUpperCase()],
|
|
2775
|
+
["an ObjectID", new ObjectID(MINOR_INCIDENT)],
|
|
2776
|
+
["{_id}", { _id: CRITICAL_INCIDENT }],
|
|
2777
|
+
["{id}", { id: MAJOR_INCIDENT }],
|
|
2778
|
+
["{_id, id}", { _id: CRITICAL_INCIDENT, id: FOREIGN_INCIDENT }],
|
|
2779
|
+
["{_id: number, id}", { _id: 1, id: FOREIGN_INCIDENT }],
|
|
2780
|
+
["{_id: true, id}", { _id: true, id: FOREIGN_INCIDENT }],
|
|
2781
|
+
[
|
|
2782
|
+
"{_id: ObjectID, id}",
|
|
2783
|
+
{ _id: new ObjectID(CRITICAL_INCIDENT), id: FOREIGN_INCIDENT },
|
|
2784
|
+
],
|
|
2785
|
+
["{_id: {}, id}", { _id: {}, id: FOREIGN_INCIDENT }],
|
|
2786
|
+
["{_id: ObjectID} alone", { _id: new ObjectID(CRITICAL_INCIDENT) }],
|
|
2787
|
+
["{_id: true} alone", { _id: true }],
|
|
2788
|
+
["{_id: ''} and an id", { _id: "", id: MINOR_INCIDENT }],
|
|
2789
|
+
["{}", {}],
|
|
2790
|
+
["{name}", { name: "Critical" }],
|
|
2791
|
+
["a number", 7],
|
|
2792
|
+
["null", null],
|
|
2793
|
+
["a nested list", [CRITICAL_INCIDENT]],
|
|
2794
|
+
["a model with an id", MAJOR_INCIDENT_MODEL],
|
|
2795
|
+
["a model with no id", new IncidentSeverity()],
|
|
2796
|
+
["not a uuid", "not-a-uuid"],
|
|
2797
|
+
["an empty string", ""],
|
|
2798
|
+
])(
|
|
2799
|
+
"%s: the id it reads is the id the save writes",
|
|
2800
|
+
async (_label: string, item: unknown) => {
|
|
2801
|
+
const resolved: Array<string> | null = resolveOrNull(item);
|
|
2802
|
+
const written: Array<string> = await writtenBySave([item]);
|
|
2803
|
+
|
|
2804
|
+
expect(resolved === null ? [] : resolved).toEqual(written);
|
|
2805
|
+
},
|
|
2806
|
+
);
|
|
2807
|
+
|
|
2808
|
+
test("a list the hooks have normalised is written by the save exactly as it was checked", async () => {
|
|
2809
|
+
const createBy: CreateBy<TeamComplianceSetting> = createByFor(
|
|
2810
|
+
newRule({
|
|
2811
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2812
|
+
incidentSeverities: [
|
|
2813
|
+
{ _id: 1, id: MAJOR_INCIDENT },
|
|
2814
|
+
CRITICAL_INCIDENT.toUpperCase(),
|
|
2815
|
+
{ id: CRITICAL_INCIDENT },
|
|
2816
|
+
new ObjectID(MAJOR_INCIDENT),
|
|
2817
|
+
],
|
|
2818
|
+
}),
|
|
2819
|
+
);
|
|
2820
|
+
|
|
2821
|
+
await onBeforeCreate(createBy);
|
|
2822
|
+
|
|
2823
|
+
const checked: Array<string> = includedIds(
|
|
2824
|
+
countCalls(incidentSeverityCountBy)[0]!["query"] as JSONObject,
|
|
2825
|
+
);
|
|
2826
|
+
|
|
2827
|
+
expect(
|
|
2828
|
+
await writtenBySave(
|
|
2829
|
+
createBy.data.incidentSeverities as unknown as Array<unknown>,
|
|
2830
|
+
),
|
|
2831
|
+
).toEqual([MAJOR_INCIDENT, CRITICAL_INCIDENT]);
|
|
2832
|
+
expect([...checked].sort()).toEqual(
|
|
2833
|
+
[MAJOR_INCIDENT, CRITICAL_INCIDENT].sort(),
|
|
2834
|
+
);
|
|
2835
|
+
});
|
|
2836
|
+
});
|
|
2837
|
+
|
|
2838
|
+
/*
|
|
2839
|
+
* ---------------------------------------------------------------------------
|
|
2840
|
+
* Who may ask.
|
|
2841
|
+
* ---------------------------------------------------------------------------
|
|
2842
|
+
*
|
|
2843
|
+
* DatabaseService checks a caller's create and update permission only AFTER
|
|
2844
|
+
* these hooks, and the hooks read as root in whichever project the request
|
|
2845
|
+
* names. Their refusals - "that severity is not in this project", "that team
|
|
2846
|
+
* already has this rule" - described another project's severities and rules
|
|
2847
|
+
* to anyone signed in who named it. The hooks now make the same permission
|
|
2848
|
+
* check first.
|
|
2849
|
+
*/
|
|
2850
|
+
|
|
2851
|
+
const EDITORS_OF_EMPTIED_RULES: Array<
|
|
2852
|
+
[string, DatabaseCommonInteractionProps]
|
|
2853
|
+
> = [
|
|
2854
|
+
["a team editor", EDITOR_PROPS],
|
|
2855
|
+
["a project admin", memberProps([Permission.ProjectAdmin])],
|
|
2856
|
+
];
|
|
2857
|
+
|
|
2858
|
+
describe("TeamComplianceSettingService - the caller's permission is checked before anything is read", () => {
|
|
2859
|
+
const OUTSIDERS: Array<[string, DatabaseCommonInteractionProps]> = [
|
|
2860
|
+
["a signed-in user with no permission in the project", OUTSIDER_PROPS],
|
|
2861
|
+
[
|
|
2862
|
+
"a member who may only read the project's teams",
|
|
2863
|
+
memberProps([Permission.ProjectMember, Permission.ReadProjectTeam]),
|
|
2864
|
+
],
|
|
2865
|
+
[
|
|
2866
|
+
"a team editor of another project who names this one",
|
|
2867
|
+
{
|
|
2868
|
+
...memberProps(
|
|
2869
|
+
[Permission.ReadProjectTeam, Permission.EditProjectTeam],
|
|
2870
|
+
OTHER_PROJECT_ID,
|
|
2871
|
+
),
|
|
2872
|
+
tenantId: PROJECT_ID,
|
|
2873
|
+
},
|
|
2874
|
+
],
|
|
2875
|
+
];
|
|
2876
|
+
|
|
2877
|
+
test.each(OUTSIDERS)(
|
|
2878
|
+
"create: %s is refused before any severity or rule is read, so the refusal says nothing about the project",
|
|
2879
|
+
async (_label: string, props: DatabaseCommonInteractionProps) => {
|
|
2880
|
+
// A rule the probe below would duplicate.
|
|
2881
|
+
store({ ...INCIDENT_CALL_CRITICAL, id: SIBLING_SETTING_ID });
|
|
2882
|
+
|
|
2883
|
+
await expect(
|
|
2884
|
+
onBeforeCreate(
|
|
2885
|
+
createByFor(
|
|
2886
|
+
newRule({
|
|
2887
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2888
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
2889
|
+
incidentSeverities: [FOREIGN_INCIDENT],
|
|
2890
|
+
}),
|
|
2891
|
+
props,
|
|
2892
|
+
),
|
|
2893
|
+
),
|
|
2894
|
+
).rejects.toThrow(NotAuthorizedException);
|
|
2895
|
+
|
|
2896
|
+
await expect(
|
|
2897
|
+
onBeforeCreate(
|
|
2898
|
+
createByFor(
|
|
2899
|
+
newRule({
|
|
2900
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
2901
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
2902
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
2903
|
+
}),
|
|
2904
|
+
props,
|
|
2905
|
+
),
|
|
2906
|
+
),
|
|
2907
|
+
).rejects.toThrow(NotAuthorizedException);
|
|
2908
|
+
|
|
2909
|
+
expectNoReads();
|
|
2910
|
+
},
|
|
2911
|
+
);
|
|
2912
|
+
|
|
2913
|
+
test.each(OUTSIDERS)(
|
|
2914
|
+
"update: %s changing a rule's scope is refused before anything is read",
|
|
2915
|
+
async (_label: string, props: DatabaseCommonInteractionProps) => {
|
|
2916
|
+
store(INCIDENT_CALL_CRITICAL, {
|
|
2917
|
+
...INCIDENT_CALL_CRITICAL,
|
|
2918
|
+
id: SIBLING_SETTING_ID,
|
|
2919
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
2920
|
+
});
|
|
2921
|
+
|
|
2922
|
+
for (const data of [
|
|
2923
|
+
{ incidentSeverities: [FOREIGN_INCIDENT] },
|
|
2924
|
+
{ notificationChannel: ComplianceNotificationChannel.Push },
|
|
2925
|
+
]) {
|
|
2926
|
+
await expect(
|
|
2927
|
+
onBeforeUpdate(updateByFor({ ...data }, { props: props })),
|
|
2928
|
+
).rejects.toThrow(NotAuthorizedException);
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
expectNoReads();
|
|
2932
|
+
},
|
|
2933
|
+
);
|
|
2934
|
+
|
|
2935
|
+
test.each(OUTSIDERS)(
|
|
2936
|
+
"update: %s switching a rule off gets nothing from this hook - it reads nothing, and _updateBy's own check refuses the write",
|
|
2937
|
+
async (_label: string, props: DatabaseCommonInteractionProps) => {
|
|
2938
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
2939
|
+
|
|
2940
|
+
await expect(
|
|
2941
|
+
onBeforeUpdate(updateByFor({ enabled: false }, { props: props })),
|
|
2942
|
+
).resolves.toBeDefined();
|
|
2943
|
+
|
|
2944
|
+
expectNoReads();
|
|
2945
|
+
},
|
|
2946
|
+
);
|
|
2947
|
+
|
|
2948
|
+
test.each(OUTSIDERS)(
|
|
2949
|
+
"update: %s switching a rule on is refused before anything is read, so the refusal cannot tell an emptied rule from any other",
|
|
2950
|
+
async (_label: string, props: DatabaseCommonInteractionProps) => {
|
|
2951
|
+
store({ ...EMPTIED_BY_DELETE });
|
|
2952
|
+
|
|
2953
|
+
await expect(
|
|
2954
|
+
onBeforeUpdate(updateByFor({ enabled: true }, { props: props })),
|
|
2955
|
+
).rejects.toThrow(NotAuthorizedException);
|
|
2956
|
+
|
|
2957
|
+
expectNoReads();
|
|
2958
|
+
},
|
|
2959
|
+
);
|
|
2960
|
+
|
|
2961
|
+
test.each(EDITORS_OF_EMPTIED_RULES)(
|
|
2962
|
+
"update: %s re-scoping an emptied rule writes options the update check _updateBy repeats allows",
|
|
2963
|
+
async (_label: string, props: DatabaseCommonInteractionProps) => {
|
|
2964
|
+
store({ ...EMPTIED_BY_DELETE });
|
|
2965
|
+
|
|
2966
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor(
|
|
2967
|
+
{ incidentSeverities: [MAJOR_INCIDENT], enabled: true },
|
|
2968
|
+
{ props: props },
|
|
2969
|
+
);
|
|
2970
|
+
|
|
2971
|
+
await onBeforeUpdate(updateBy);
|
|
2972
|
+
|
|
2973
|
+
expect("options" in dataOf(updateBy)).toBe(true);
|
|
2974
|
+
|
|
2975
|
+
await expect(
|
|
2976
|
+
ModelPermission.checkUpdateQueryPermissions(
|
|
2977
|
+
TeamComplianceSetting,
|
|
2978
|
+
{ _id: SETTING_ID },
|
|
2979
|
+
updateBy.data,
|
|
2980
|
+
props,
|
|
2981
|
+
),
|
|
2982
|
+
).resolves.toBeDefined();
|
|
2983
|
+
},
|
|
2984
|
+
);
|
|
2985
|
+
|
|
2986
|
+
const EDITORS: Array<[string, DatabaseCommonInteractionProps]> = [
|
|
2987
|
+
["a team editor", EDITOR_PROPS],
|
|
2988
|
+
["a project admin", memberProps([Permission.ProjectAdmin])],
|
|
2989
|
+
["a project owner", memberProps([Permission.ProjectOwner])],
|
|
2990
|
+
];
|
|
2991
|
+
|
|
2992
|
+
test.each(EDITORS)(
|
|
2993
|
+
"create: %s passes, and what the hook leaves - cleared options included - passes the create check DatabaseService repeats",
|
|
2994
|
+
async (_label: string, props: DatabaseCommonInteractionProps) => {
|
|
2995
|
+
for (const input of [
|
|
2996
|
+
{
|
|
2997
|
+
ruleType: ComplianceRuleType.HasNotificationCallMethod,
|
|
2998
|
+
notificationChannel: ComplianceNotificationChannel.SMS,
|
|
2999
|
+
incidentSeverities: [FOREIGN_INCIDENT],
|
|
3000
|
+
alertSeverities: [FOREIGN_ALERT],
|
|
3001
|
+
},
|
|
3002
|
+
{
|
|
3003
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3004
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
3005
|
+
incidentSeverities: [CRITICAL_INCIDENT, CRITICAL_INCIDENT],
|
|
3006
|
+
alertSeverities: [FOREIGN_ALERT],
|
|
3007
|
+
},
|
|
3008
|
+
]) {
|
|
3009
|
+
const result: OnCreate<TeamComplianceSetting> = await onBeforeCreate(
|
|
3010
|
+
createByFor(newRule(input), props),
|
|
3011
|
+
);
|
|
3012
|
+
|
|
3013
|
+
expect((): void => {
|
|
3014
|
+
ModelPermission.checkCreatePermissions(
|
|
3015
|
+
TeamComplianceSetting,
|
|
3016
|
+
result.createBy.data,
|
|
3017
|
+
props,
|
|
3018
|
+
);
|
|
3019
|
+
}).not.toThrow();
|
|
3020
|
+
}
|
|
3021
|
+
},
|
|
3022
|
+
);
|
|
3023
|
+
|
|
3024
|
+
test("create: Edit Teams on its own is enough to create a rule", async () => {
|
|
3025
|
+
const props: DatabaseCommonInteractionProps = memberProps([
|
|
3026
|
+
Permission.EditProjectTeam,
|
|
3027
|
+
]);
|
|
3028
|
+
|
|
3029
|
+
const result: OnCreate<TeamComplianceSetting> = await onBeforeCreate(
|
|
3030
|
+
createByFor(
|
|
3031
|
+
newRule({
|
|
3032
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
3033
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
3034
|
+
alertSeverities: [CRITICAL_ALERT],
|
|
3035
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
3036
|
+
}),
|
|
3037
|
+
props,
|
|
3038
|
+
),
|
|
3039
|
+
);
|
|
3040
|
+
|
|
3041
|
+
expect((): void => {
|
|
3042
|
+
ModelPermission.checkCreatePermissions(
|
|
3043
|
+
TeamComplianceSetting,
|
|
3044
|
+
result.createBy.data,
|
|
3045
|
+
props,
|
|
3046
|
+
);
|
|
3047
|
+
}).not.toThrow();
|
|
3048
|
+
});
|
|
3049
|
+
|
|
3050
|
+
test.each(EDITORS)(
|
|
3051
|
+
"update: %s passes, and what the hook leaves passes the update check _updateBy repeats",
|
|
3052
|
+
async (_label: string, props: DatabaseCommonInteractionProps) => {
|
|
3053
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
3054
|
+
|
|
3055
|
+
for (const data of [
|
|
3056
|
+
{ ruleType: ComplianceRuleType.HasNotificationEmailMethod },
|
|
3057
|
+
{
|
|
3058
|
+
incidentSeverities: [{ _id: MAJOR_INCIDENT }, MAJOR_INCIDENT],
|
|
3059
|
+
alertSeverities: [FOREIGN_ALERT],
|
|
3060
|
+
},
|
|
3061
|
+
{ notificationChannel: null },
|
|
3062
|
+
]) {
|
|
3063
|
+
const updateBy: UpdateBy<TeamComplianceSetting> = updateByFor(
|
|
3064
|
+
{ ...data },
|
|
3065
|
+
{ props: props },
|
|
3066
|
+
);
|
|
3067
|
+
|
|
3068
|
+
await onBeforeUpdate(updateBy);
|
|
3069
|
+
|
|
3070
|
+
await expect(
|
|
3071
|
+
ModelPermission.checkUpdateQueryPermissions(
|
|
3072
|
+
TeamComplianceSetting,
|
|
3073
|
+
{ _id: SETTING_ID },
|
|
3074
|
+
updateBy.data,
|
|
3075
|
+
props,
|
|
3076
|
+
),
|
|
3077
|
+
).resolves.toBeDefined();
|
|
3078
|
+
}
|
|
3079
|
+
},
|
|
3080
|
+
);
|
|
3081
|
+
|
|
3082
|
+
test("root and master-admin callers are not put through it", async () => {
|
|
3083
|
+
const createCheck: jest.SpyInstance = jest.spyOn(
|
|
3084
|
+
ModelPermission,
|
|
3085
|
+
"checkCreatePermissions",
|
|
3086
|
+
);
|
|
3087
|
+
const updateCheck: jest.SpyInstance = jest.spyOn(
|
|
3088
|
+
ModelPermission,
|
|
3089
|
+
"checkUpdateQueryPermissions",
|
|
3090
|
+
);
|
|
3091
|
+
|
|
3092
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
3093
|
+
|
|
3094
|
+
await onBeforeCreate(
|
|
3095
|
+
createByFor(
|
|
3096
|
+
newRule({ ruleType: ComplianceRuleType.HasNotificationEmailMethod }),
|
|
3097
|
+
{ isRoot: true },
|
|
3098
|
+
),
|
|
3099
|
+
);
|
|
3100
|
+
await onBeforeCreate(
|
|
3101
|
+
createByFor(
|
|
3102
|
+
newRule({ ruleType: ComplianceRuleType.HasNotificationPushMethod }),
|
|
3103
|
+
{ isMasterAdmin: true, userId: USER_ID, tenantId: PROJECT_ID },
|
|
3104
|
+
),
|
|
3105
|
+
);
|
|
3106
|
+
await onBeforeUpdate(
|
|
3107
|
+
updateByFor(
|
|
3108
|
+
{ notificationChannel: ComplianceNotificationChannel.Push },
|
|
3109
|
+
{ props: { isRoot: true } },
|
|
3110
|
+
),
|
|
3111
|
+
);
|
|
3112
|
+
|
|
3113
|
+
expect(createCheck).not.toHaveBeenCalled();
|
|
3114
|
+
expect(updateCheck).not.toHaveBeenCalled();
|
|
3115
|
+
});
|
|
3116
|
+
});
|
|
3117
|
+
|
|
3118
|
+
/*
|
|
3119
|
+
* ---------------------------------------------------------------------------
|
|
3120
|
+
* Severity deletes.
|
|
3121
|
+
* ---------------------------------------------------------------------------
|
|
3122
|
+
*
|
|
3123
|
+
* A rule's severity scope lives only in join rows, which cascade away with
|
|
3124
|
+
* the severity, and a rule with no severity left reads as a rule for every
|
|
3125
|
+
* severity of its kind. IncidentSeverityService and AlertSeverityService ask
|
|
3126
|
+
* which rules reference what they are about to delete, and once the delete
|
|
3127
|
+
* has happened pause and mark those left with no severity.
|
|
3128
|
+
*/
|
|
3129
|
+
|
|
3130
|
+
const RULE_CRITICAL_ONLY: string = "77777777-7777-4777-8777-000000000001";
|
|
3131
|
+
const RULE_EPISODE_CRITICAL_ONLY: string =
|
|
3132
|
+
"77777777-7777-4777-8777-000000000002";
|
|
3133
|
+
const RULE_CRITICAL_AND_MAJOR: string = "77777777-7777-4777-8777-000000000003";
|
|
3134
|
+
const RULE_EVERY_SEVERITY: string = "77777777-7777-4777-8777-000000000004";
|
|
3135
|
+
const RULE_PAUSED: string = "77777777-7777-4777-8777-000000000005";
|
|
3136
|
+
const RULE_ALERT: string = "77777777-7777-4777-8777-000000000006";
|
|
3137
|
+
const RULE_METHOD: string = "77777777-7777-4777-8777-000000000007";
|
|
3138
|
+
const RULE_OTHER_PROJECT: string = "77777777-7777-4777-8777-000000000008";
|
|
3139
|
+
const RULE_MAJOR_ONLY: string = "77777777-7777-4777-8777-000000000009";
|
|
3140
|
+
|
|
3141
|
+
const deleting: (
|
|
3142
|
+
ids: Array<string>,
|
|
3143
|
+
project?: ObjectID,
|
|
3144
|
+
) => Array<DeletedSeverity> = (
|
|
3145
|
+
ids: Array<string>,
|
|
3146
|
+
project?: ObjectID,
|
|
3147
|
+
): Array<DeletedSeverity> => {
|
|
3148
|
+
return ids.map((id: string): DeletedSeverity => {
|
|
3149
|
+
return { _id: id, projectId: project || PROJECT_ID };
|
|
3150
|
+
});
|
|
3151
|
+
};
|
|
3152
|
+
|
|
3153
|
+
const MARKED: JSONObject = { [SEVERITIES_DELETED_OPTION]: true };
|
|
3154
|
+
|
|
3155
|
+
describe("TeamComplianceSettingService - a severity delete pauses and marks the rules it leaves with no severity", () => {
|
|
3156
|
+
let settingsUpdateBy: jest.SpyInstance;
|
|
3157
|
+
|
|
3158
|
+
beforeEach(() => {
|
|
3159
|
+
settingsUpdateBy = jest
|
|
3160
|
+
.spyOn(TeamComplianceSettingService, "updateBy")
|
|
3161
|
+
.mockResolvedValue(1 as never);
|
|
3162
|
+
|
|
3163
|
+
store(
|
|
3164
|
+
{
|
|
3165
|
+
id: RULE_CRITICAL_ONLY,
|
|
3166
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3167
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
3168
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
3169
|
+
},
|
|
3170
|
+
{
|
|
3171
|
+
id: RULE_EPISODE_CRITICAL_ONLY,
|
|
3172
|
+
ruleType: ComplianceRuleType.HasIncidentEpisodeOnCallRules,
|
|
3173
|
+
notificationChannel: ComplianceNotificationChannel.SMS,
|
|
3174
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
3175
|
+
},
|
|
3176
|
+
{
|
|
3177
|
+
id: RULE_CRITICAL_AND_MAJOR,
|
|
3178
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3179
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
3180
|
+
incidentSeverityIds: [CRITICAL_INCIDENT, MAJOR_INCIDENT],
|
|
3181
|
+
},
|
|
3182
|
+
{
|
|
3183
|
+
id: RULE_EVERY_SEVERITY,
|
|
3184
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3185
|
+
notificationChannel: ComplianceNotificationChannel.Email,
|
|
3186
|
+
},
|
|
3187
|
+
{
|
|
3188
|
+
id: RULE_PAUSED,
|
|
3189
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3190
|
+
notificationChannel: ComplianceNotificationChannel.Slack,
|
|
3191
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
3192
|
+
enabled: false,
|
|
3193
|
+
},
|
|
3194
|
+
{
|
|
3195
|
+
id: RULE_ALERT,
|
|
3196
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
3197
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
3198
|
+
alertSeverityIds: [CRITICAL_ALERT],
|
|
3199
|
+
},
|
|
3200
|
+
{
|
|
3201
|
+
// A stray severity on a method rule scopes nothing.
|
|
3202
|
+
id: RULE_METHOD,
|
|
3203
|
+
ruleType: ComplianceRuleType.HasNotificationEmailMethod,
|
|
3204
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
3205
|
+
},
|
|
3206
|
+
{
|
|
3207
|
+
id: RULE_OTHER_PROJECT,
|
|
3208
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3209
|
+
projectId: OTHER_PROJECT_ID,
|
|
3210
|
+
teamId: OTHER_TEAM_ID,
|
|
3211
|
+
incidentSeverityIds: [FOREIGN_INCIDENT],
|
|
3212
|
+
},
|
|
3213
|
+
{
|
|
3214
|
+
id: RULE_MAJOR_ONLY,
|
|
3215
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3216
|
+
notificationChannel: ComplianceNotificationChannel.WhatsApp,
|
|
3217
|
+
incidentSeverityIds: [MAJOR_INCIDENT],
|
|
3218
|
+
},
|
|
3219
|
+
);
|
|
3220
|
+
});
|
|
3221
|
+
|
|
3222
|
+
/*
|
|
3223
|
+
* ANY reference, not only rules scoped to nothing but what this delete
|
|
3224
|
+
* removes: two deletes side by side each still see the other's severity
|
|
3225
|
+
* attached, and a rule neither carried would be left empty and enabled.
|
|
3226
|
+
*/
|
|
3227
|
+
test("finds every rule of the severity's kind that references a severity being deleted - paused ones too", async () => {
|
|
3228
|
+
expect(
|
|
3229
|
+
await TeamComplianceSettingService.getRulesScopedToAnyOf({
|
|
3230
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3231
|
+
severities: deleting([CRITICAL_INCIDENT]),
|
|
3232
|
+
}),
|
|
3233
|
+
).toEqual([
|
|
3234
|
+
RULE_CRITICAL_ONLY,
|
|
3235
|
+
RULE_EPISODE_CRITICAL_ONLY,
|
|
3236
|
+
RULE_CRITICAL_AND_MAJOR,
|
|
3237
|
+
RULE_PAUSED,
|
|
3238
|
+
]);
|
|
3239
|
+
|
|
3240
|
+
expect(
|
|
3241
|
+
await TeamComplianceSettingService.getRulesScopedToAnyOf({
|
|
3242
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3243
|
+
severities: deleting([MAJOR_INCIDENT.toUpperCase()]),
|
|
3244
|
+
}),
|
|
3245
|
+
).toEqual([RULE_CRITICAL_AND_MAJOR, RULE_MAJOR_ONLY]);
|
|
3246
|
+
|
|
3247
|
+
expect(
|
|
3248
|
+
await TeamComplianceSettingService.getRulesScopedToAnyOf({
|
|
3249
|
+
severityKind: ComplianceSeverityKind.Alert,
|
|
3250
|
+
severities: deleting([CRITICAL_ALERT]),
|
|
3251
|
+
}),
|
|
3252
|
+
).toEqual([RULE_ALERT]);
|
|
3253
|
+
|
|
3254
|
+
// A severity no rule names carries nothing.
|
|
3255
|
+
expect(
|
|
3256
|
+
await TeamComplianceSettingService.getRulesScopedToAnyOf({
|
|
3257
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3258
|
+
severities: deleting([MINOR_INCIDENT]),
|
|
3259
|
+
}),
|
|
3260
|
+
).toEqual([]);
|
|
3261
|
+
});
|
|
3262
|
+
|
|
3263
|
+
test("reads the project's rules of that kind's types, enabled or not, as root, with their severities", async () => {
|
|
3264
|
+
await TeamComplianceSettingService.getRulesScopedToAnyOf({
|
|
3265
|
+
severityKind: ComplianceSeverityKind.Alert,
|
|
3266
|
+
severities: deleting([CRITICAL_ALERT]),
|
|
3267
|
+
});
|
|
3268
|
+
|
|
3269
|
+
expect(settingsFindBy).toHaveBeenCalledTimes(1);
|
|
3270
|
+
|
|
3271
|
+
const read: JSONObject = findByCalls()[0]!;
|
|
3272
|
+
const query: JSONObject = read["query"] as JSONObject;
|
|
3273
|
+
|
|
3274
|
+
expect(Object.keys(query).sort()).toEqual(["projectId", "ruleType"]);
|
|
3275
|
+
expect(String(query["projectId"])).toBe(PROJECT_ID.toString());
|
|
3276
|
+
expect((query["ruleType"] as Includes).values).toEqual([
|
|
3277
|
+
ComplianceRuleType.HasAlertOnCallRules,
|
|
3278
|
+
ComplianceRuleType.HasAlertEpisodeOnCallRules,
|
|
3279
|
+
]);
|
|
3280
|
+
expect(read["select"]).toEqual({
|
|
3281
|
+
_id: true,
|
|
3282
|
+
ruleType: true,
|
|
3283
|
+
incidentSeverities: { _id: true },
|
|
3284
|
+
alertSeverities: { _id: true },
|
|
3285
|
+
});
|
|
3286
|
+
expect(read["limit"]).toBe(LIMIT_PER_PROJECT);
|
|
3287
|
+
expect(read["props"]).toEqual({ isRoot: true });
|
|
3288
|
+
});
|
|
3289
|
+
|
|
3290
|
+
test("a delete across projects checks each project's rules against its own severities", async () => {
|
|
3291
|
+
expect(
|
|
3292
|
+
await TeamComplianceSettingService.getRulesScopedToAnyOf({
|
|
3293
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3294
|
+
severities: [
|
|
3295
|
+
...deleting([FOREIGN_INCIDENT]),
|
|
3296
|
+
...deleting([FOREIGN_INCIDENT], OTHER_PROJECT_ID),
|
|
3297
|
+
],
|
|
3298
|
+
}),
|
|
3299
|
+
).toEqual([RULE_OTHER_PROJECT]);
|
|
3300
|
+
|
|
3301
|
+
expect(settingsFindBy).toHaveBeenCalledTimes(2);
|
|
3302
|
+
});
|
|
3303
|
+
|
|
3304
|
+
test("nothing being deleted reads nothing", async () => {
|
|
3305
|
+
expect(
|
|
3306
|
+
await TeamComplianceSettingService.getRulesScopedToAnyOf({
|
|
3307
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3308
|
+
severities: [{ _id: CRITICAL_INCIDENT }, { projectId: PROJECT_ID }],
|
|
3309
|
+
}),
|
|
3310
|
+
).toEqual([]);
|
|
3311
|
+
|
|
3312
|
+
expect(settingsFindBy).not.toHaveBeenCalled();
|
|
3313
|
+
});
|
|
3314
|
+
|
|
3315
|
+
test("after the delete: pauses and marks, as root, the named rules left with no severity of their kind", async () => {
|
|
3316
|
+
// What the cascade leaves behind.
|
|
3317
|
+
storedSettings = [];
|
|
3318
|
+
store(
|
|
3319
|
+
{
|
|
3320
|
+
id: RULE_CRITICAL_ONLY,
|
|
3321
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3322
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
3323
|
+
},
|
|
3324
|
+
{
|
|
3325
|
+
id: RULE_EPISODE_CRITICAL_ONLY,
|
|
3326
|
+
ruleType: ComplianceRuleType.HasIncidentEpisodeOnCallRules,
|
|
3327
|
+
},
|
|
3328
|
+
{
|
|
3329
|
+
// Another of its severities is still there.
|
|
3330
|
+
id: RULE_CRITICAL_AND_MAJOR,
|
|
3331
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3332
|
+
incidentSeverityIds: [MAJOR_INCIDENT],
|
|
3333
|
+
},
|
|
3334
|
+
{
|
|
3335
|
+
// Paused by an admin before the delete: marked all the same.
|
|
3336
|
+
id: RULE_PAUSED,
|
|
3337
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3338
|
+
enabled: false,
|
|
3339
|
+
},
|
|
3340
|
+
{
|
|
3341
|
+
// Not of the deleted kind.
|
|
3342
|
+
id: RULE_ALERT,
|
|
3343
|
+
ruleType: ComplianceRuleType.HasAlertOnCallRules,
|
|
3344
|
+
},
|
|
3345
|
+
);
|
|
3346
|
+
|
|
3347
|
+
expect(
|
|
3348
|
+
await TeamComplianceSettingService.pauseRulesLeftWithoutSeverities({
|
|
3349
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3350
|
+
settingIds: [
|
|
3351
|
+
RULE_CRITICAL_ONLY,
|
|
3352
|
+
RULE_EPISODE_CRITICAL_ONLY,
|
|
3353
|
+
RULE_CRITICAL_AND_MAJOR,
|
|
3354
|
+
RULE_PAUSED,
|
|
3355
|
+
RULE_ALERT,
|
|
3356
|
+
RULE_EVERY_SEVERITY,
|
|
3357
|
+
],
|
|
3358
|
+
}),
|
|
3359
|
+
).toEqual([RULE_CRITICAL_ONLY, RULE_EPISODE_CRITICAL_ONLY, RULE_PAUSED]);
|
|
3360
|
+
|
|
3361
|
+
const read: JSONObject = findByCalls()[0]!;
|
|
3362
|
+
expect((read["query"] as JSONObject)["_id"]).toBeInstanceOf(Includes);
|
|
3363
|
+
expect(read["select"]).toEqual({
|
|
3364
|
+
_id: true,
|
|
3365
|
+
ruleType: true,
|
|
3366
|
+
enabled: true,
|
|
3367
|
+
options: true,
|
|
3368
|
+
incidentSeverities: { _id: true },
|
|
3369
|
+
alertSeverities: { _id: true },
|
|
3370
|
+
});
|
|
3371
|
+
expect(read["props"]).toEqual({ isRoot: true });
|
|
3372
|
+
|
|
3373
|
+
// Rules with the same options are paused in one write.
|
|
3374
|
+
expect(settingsUpdateBy).toHaveBeenCalledTimes(1);
|
|
3375
|
+
|
|
3376
|
+
const write: JSONObject = settingsUpdateBy.mock.calls[0]![0] as JSONObject;
|
|
3377
|
+
expect(((write["query"] as JSONObject)["_id"] as Includes).values).toEqual([
|
|
3378
|
+
RULE_CRITICAL_ONLY,
|
|
3379
|
+
RULE_EPISODE_CRITICAL_ONLY,
|
|
3380
|
+
RULE_PAUSED,
|
|
3381
|
+
]);
|
|
3382
|
+
expect(write["data"]).toEqual({ enabled: false, options: MARKED });
|
|
3383
|
+
expect(write["limit"]).toBe(3);
|
|
3384
|
+
expect(write["props"]).toEqual({ isRoot: true });
|
|
3385
|
+
});
|
|
3386
|
+
|
|
3387
|
+
test("the mark is added to whatever else a rule's options hold, one write per set of options", async () => {
|
|
3388
|
+
storedSettings = [];
|
|
3389
|
+
store(
|
|
3390
|
+
{
|
|
3391
|
+
id: RULE_CRITICAL_ONLY,
|
|
3392
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3393
|
+
options: { note: "first" },
|
|
3394
|
+
},
|
|
3395
|
+
{
|
|
3396
|
+
id: RULE_EPISODE_CRITICAL_ONLY,
|
|
3397
|
+
ruleType: ComplianceRuleType.HasIncidentEpisodeOnCallRules,
|
|
3398
|
+
options: { note: "second" },
|
|
3399
|
+
},
|
|
3400
|
+
);
|
|
3401
|
+
|
|
3402
|
+
await TeamComplianceSettingService.pauseRulesLeftWithoutSeverities({
|
|
3403
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3404
|
+
settingIds: [RULE_CRITICAL_ONLY, RULE_EPISODE_CRITICAL_ONLY],
|
|
3405
|
+
});
|
|
3406
|
+
|
|
3407
|
+
expect(
|
|
3408
|
+
settingsUpdateBy.mock.calls.map((call: Array<unknown>) => {
|
|
3409
|
+
const write: JSONObject = call[0] as JSONObject;
|
|
3410
|
+
return [
|
|
3411
|
+
((write["query"] as JSONObject)["_id"] as Includes).values,
|
|
3412
|
+
write["data"],
|
|
3413
|
+
];
|
|
3414
|
+
}),
|
|
3415
|
+
).toEqual([
|
|
3416
|
+
[
|
|
3417
|
+
[RULE_CRITICAL_ONLY],
|
|
3418
|
+
{ enabled: false, options: { note: "first", ...MARKED } },
|
|
3419
|
+
],
|
|
3420
|
+
[
|
|
3421
|
+
[RULE_EPISODE_CRITICAL_ONLY],
|
|
3422
|
+
{ enabled: false, options: { note: "second", ...MARKED } },
|
|
3423
|
+
],
|
|
3424
|
+
]);
|
|
3425
|
+
});
|
|
3426
|
+
|
|
3427
|
+
test("a rule already paused and marked is not written again", async () => {
|
|
3428
|
+
storedSettings = [];
|
|
3429
|
+
store({
|
|
3430
|
+
id: RULE_CRITICAL_ONLY,
|
|
3431
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3432
|
+
enabled: false,
|
|
3433
|
+
options: MARKED,
|
|
3434
|
+
});
|
|
3435
|
+
|
|
3436
|
+
expect(
|
|
3437
|
+
await TeamComplianceSettingService.pauseRulesLeftWithoutSeverities({
|
|
3438
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3439
|
+
settingIds: [RULE_CRITICAL_ONLY],
|
|
3440
|
+
}),
|
|
3441
|
+
).toEqual([]);
|
|
3442
|
+
expect(settingsUpdateBy).not.toHaveBeenCalled();
|
|
3443
|
+
});
|
|
3444
|
+
|
|
3445
|
+
test("the pause write reads nothing through the update hook, so it cannot fail on the rule's configuration", async () => {
|
|
3446
|
+
storedSettings = [];
|
|
3447
|
+
store({
|
|
3448
|
+
id: RULE_CRITICAL_ONLY,
|
|
3449
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3450
|
+
});
|
|
3451
|
+
|
|
3452
|
+
const pauseWrite: UpdateBy<TeamComplianceSetting> = {
|
|
3453
|
+
query: { _id: new Includes([RULE_CRITICAL_ONLY]) },
|
|
3454
|
+
data: { enabled: false, options: MARKED },
|
|
3455
|
+
limit: 1,
|
|
3456
|
+
skip: 0,
|
|
3457
|
+
props: { isRoot: true },
|
|
3458
|
+
} as unknown as UpdateBy<TeamComplianceSetting>;
|
|
3459
|
+
|
|
3460
|
+
settingsFindBy.mockClear();
|
|
3461
|
+
|
|
3462
|
+
await onBeforeUpdate(pauseWrite);
|
|
3463
|
+
|
|
3464
|
+
expect(settingsFindBy).not.toHaveBeenCalled();
|
|
3465
|
+
expect(dataOf(pauseWrite)).toEqual({ enabled: false, options: MARKED });
|
|
3466
|
+
});
|
|
3467
|
+
|
|
3468
|
+
/*
|
|
3469
|
+
* Two deletes side by side, of the two severities of a "Critical and
|
|
3470
|
+
* Major" rule: both look the rule up before either deletes, so each sees
|
|
3471
|
+
* the other's severity still attached. Only the delete that finishes last
|
|
3472
|
+
* sees the rule empty - and it pauses it.
|
|
3473
|
+
*/
|
|
3474
|
+
test("two deletes that both look up before either deletes: the rule is paused exactly once, by the last", async () => {
|
|
3475
|
+
const lookups: Array<Array<string>> = [
|
|
3476
|
+
await TeamComplianceSettingService.getRulesScopedToAnyOf({
|
|
3477
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3478
|
+
severities: deleting([CRITICAL_INCIDENT]),
|
|
3479
|
+
}),
|
|
3480
|
+
await TeamComplianceSettingService.getRulesScopedToAnyOf({
|
|
3481
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3482
|
+
severities: deleting([MAJOR_INCIDENT]),
|
|
3483
|
+
}),
|
|
3484
|
+
];
|
|
3485
|
+
|
|
3486
|
+
for (const lookup of lookups) {
|
|
3487
|
+
expect(lookup).toContain(RULE_CRITICAL_AND_MAJOR);
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
// Critical's delete commits first: Major is still attached.
|
|
3491
|
+
storedSettings = [];
|
|
3492
|
+
store({
|
|
3493
|
+
id: RULE_CRITICAL_AND_MAJOR,
|
|
3494
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3495
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
3496
|
+
incidentSeverityIds: [MAJOR_INCIDENT],
|
|
3497
|
+
});
|
|
3498
|
+
|
|
3499
|
+
expect(
|
|
3500
|
+
await TeamComplianceSettingService.pauseRulesLeftWithoutSeverities({
|
|
3501
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3502
|
+
settingIds: lookups[0]!,
|
|
3503
|
+
}),
|
|
3504
|
+
).toEqual([]);
|
|
3505
|
+
|
|
3506
|
+
// Then Major's: nothing is left.
|
|
3507
|
+
storedSettings = [];
|
|
3508
|
+
store({
|
|
3509
|
+
id: RULE_CRITICAL_AND_MAJOR,
|
|
3510
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3511
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
3512
|
+
});
|
|
3513
|
+
|
|
3514
|
+
expect(
|
|
3515
|
+
await TeamComplianceSettingService.pauseRulesLeftWithoutSeverities({
|
|
3516
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3517
|
+
settingIds: lookups[1]!,
|
|
3518
|
+
}),
|
|
3519
|
+
).toEqual([RULE_CRITICAL_AND_MAJOR]);
|
|
3520
|
+
|
|
3521
|
+
expect(settingsUpdateBy).toHaveBeenCalledTimes(1);
|
|
3522
|
+
});
|
|
3523
|
+
|
|
3524
|
+
test("nothing named reads nothing; nothing left empty writes nothing", async () => {
|
|
3525
|
+
await TeamComplianceSettingService.pauseRulesLeftWithoutSeverities({
|
|
3526
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3527
|
+
settingIds: [],
|
|
3528
|
+
});
|
|
3529
|
+
|
|
3530
|
+
expect(settingsFindBy).not.toHaveBeenCalled();
|
|
3531
|
+
|
|
3532
|
+
expect(
|
|
3533
|
+
await TeamComplianceSettingService.pauseRulesLeftWithoutSeverities({
|
|
3534
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3535
|
+
settingIds: [RULE_CRITICAL_AND_MAJOR],
|
|
3536
|
+
}),
|
|
3537
|
+
).toEqual([]);
|
|
3538
|
+
expect(settingsUpdateBy).not.toHaveBeenCalled();
|
|
3539
|
+
});
|
|
3540
|
+
});
|
|
3541
|
+
|
|
3542
|
+
describe("TeamComplianceSettingService.hasSeveritiesDeletedMark / isLeftWithoutSeverities", () => {
|
|
3543
|
+
test("only a `true` under the key is the mark", () => {
|
|
3544
|
+
expect(SEVERITIES_DELETED_OPTION).toBe("severitiesDeleted");
|
|
3545
|
+
|
|
3546
|
+
for (const options of [
|
|
3547
|
+
undefined,
|
|
3548
|
+
null,
|
|
3549
|
+
{},
|
|
3550
|
+
{ severitiesDeleted: false },
|
|
3551
|
+
{ severitiesDeleted: "true" },
|
|
3552
|
+
[true],
|
|
3553
|
+
"severitiesDeleted",
|
|
3554
|
+
]) {
|
|
3555
|
+
expect(
|
|
3556
|
+
TeamComplianceSettingServiceClass.hasSeveritiesDeletedMark(options),
|
|
3557
|
+
).toBe(false);
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
expect(
|
|
3561
|
+
TeamComplianceSettingServiceClass.hasSeveritiesDeletedMark({
|
|
3562
|
+
severitiesDeleted: true,
|
|
3563
|
+
note: "x",
|
|
3564
|
+
}),
|
|
3565
|
+
).toBe(true);
|
|
3566
|
+
});
|
|
3567
|
+
|
|
3568
|
+
test("a rule is left without severities only when marked, scoped by severity, and empty of its own kind", () => {
|
|
3569
|
+
const isLeft: (input: StoredRuleInput) => boolean = (
|
|
3570
|
+
input: StoredRuleInput,
|
|
3571
|
+
): boolean => {
|
|
3572
|
+
return TeamComplianceSettingServiceClass.isLeftWithoutSeverities(
|
|
3573
|
+
storedRule(input),
|
|
3574
|
+
);
|
|
3575
|
+
};
|
|
3576
|
+
|
|
3577
|
+
expect(isLeft({ ...EMPTIED_BY_DELETE })).toBe(true);
|
|
3578
|
+
// Enabled or not: the mark is about the scope, not the switch.
|
|
3579
|
+
expect(isLeft({ ...EMPTIED_BY_DELETE, enabled: true })).toBe(true);
|
|
3580
|
+
// A severity of the other kind is not a scope.
|
|
3581
|
+
expect(
|
|
3582
|
+
isLeft({ ...EMPTIED_BY_DELETE, alertSeverityIds: [CRITICAL_ALERT] }),
|
|
3583
|
+
).toBe(true);
|
|
3584
|
+
expect(
|
|
3585
|
+
isLeft({ ...EMPTIED_BY_DELETE, incidentSeverityIds: [MAJOR_INCIDENT] }),
|
|
3586
|
+
).toBe(false);
|
|
3587
|
+
expect(isLeft({ ...EMPTIED_BY_DELETE, options: undefined })).toBe(false);
|
|
3588
|
+
expect(
|
|
3589
|
+
isLeft({
|
|
3590
|
+
...EMPTIED_BY_DELETE,
|
|
3591
|
+
ruleType: ComplianceRuleType.HasNotificationCallMethod,
|
|
3592
|
+
}),
|
|
3593
|
+
).toBe(false);
|
|
3594
|
+
expect(isLeft({ ...EMPTIED_BY_DELETE, ruleType: "LegacyRule" })).toBe(
|
|
3595
|
+
false,
|
|
3596
|
+
);
|
|
3597
|
+
});
|
|
3598
|
+
});
|
|
3599
|
+
|
|
3600
|
+
/*
|
|
3601
|
+
* ---------------------------------------------------------------------------
|
|
3602
|
+
* The static scope helpers.
|
|
3603
|
+
* ---------------------------------------------------------------------------
|
|
3604
|
+
*/
|
|
3605
|
+
|
|
3606
|
+
describe("TeamComplianceSettingService.getIds", () => {
|
|
3607
|
+
test.each([
|
|
3608
|
+
["undefined", undefined],
|
|
3609
|
+
["null", null],
|
|
3610
|
+
["a bare id string", CRITICAL_INCIDENT],
|
|
3611
|
+
["an object", { _id: CRITICAL_INCIDENT }],
|
|
3612
|
+
["a number", 7],
|
|
3613
|
+
])(
|
|
3614
|
+
"is empty for %s, which is not a list",
|
|
3615
|
+
(_label: string, value: unknown) => {
|
|
3616
|
+
expect(TeamComplianceSettingServiceClass.getIds(value)).toEqual([]);
|
|
3617
|
+
},
|
|
3618
|
+
);
|
|
3619
|
+
|
|
3620
|
+
test("reads ids from strings, ObjectIDs, models and {_id} / {id} JSON", () => {
|
|
3621
|
+
const model: AlertSeverity = new AlertSeverity();
|
|
3622
|
+
model._id = MAJOR_ALERT;
|
|
3623
|
+
|
|
3624
|
+
expect(
|
|
3625
|
+
TeamComplianceSettingServiceClass.getIds([
|
|
3626
|
+
CRITICAL_INCIDENT,
|
|
3627
|
+
new ObjectID(MAJOR_INCIDENT),
|
|
3628
|
+
{ _id: MINOR_INCIDENT },
|
|
3629
|
+
{ id: CRITICAL_ALERT },
|
|
3630
|
+
{ _id: new ObjectID(FOREIGN_ALERT) },
|
|
3631
|
+
{ id: new ObjectID(FOREIGN_INCIDENT) },
|
|
3632
|
+
model,
|
|
3633
|
+
]),
|
|
3634
|
+
).toEqual(
|
|
3635
|
+
[
|
|
3636
|
+
CRITICAL_INCIDENT,
|
|
3637
|
+
MAJOR_INCIDENT,
|
|
3638
|
+
MINOR_INCIDENT,
|
|
3639
|
+
CRITICAL_ALERT,
|
|
3640
|
+
FOREIGN_ALERT,
|
|
3641
|
+
FOREIGN_INCIDENT,
|
|
3642
|
+
MAJOR_ALERT,
|
|
3643
|
+
].sort(),
|
|
3644
|
+
);
|
|
3645
|
+
});
|
|
3646
|
+
|
|
3647
|
+
test("prefers _id over id when an item carries both", () => {
|
|
3648
|
+
expect(
|
|
3649
|
+
TeamComplianceSettingServiceClass.getIds([
|
|
3650
|
+
{ _id: CRITICAL_INCIDENT, id: MAJOR_INCIDENT },
|
|
3651
|
+
]),
|
|
3652
|
+
).toEqual([CRITICAL_INCIDENT]);
|
|
3653
|
+
});
|
|
3654
|
+
|
|
3655
|
+
test("skips items with no usable id", () => {
|
|
3656
|
+
expect(
|
|
3657
|
+
TeamComplianceSettingServiceClass.getIds([
|
|
3658
|
+
null,
|
|
3659
|
+
undefined,
|
|
3660
|
+
"",
|
|
3661
|
+
" ",
|
|
3662
|
+
12,
|
|
3663
|
+
true,
|
|
3664
|
+
{},
|
|
3665
|
+
{ _id: 5 },
|
|
3666
|
+
{ name: "Critical" },
|
|
3667
|
+
[CRITICAL_INCIDENT],
|
|
3668
|
+
CRITICAL_INCIDENT,
|
|
3669
|
+
]),
|
|
3670
|
+
).toEqual([CRITICAL_INCIDENT]);
|
|
3671
|
+
});
|
|
3672
|
+
|
|
3673
|
+
test("trims, de-duplicates and sorts, so two lists compare by content", () => {
|
|
3674
|
+
expect(
|
|
3675
|
+
TeamComplianceSettingServiceClass.getIds([
|
|
3676
|
+
MINOR_INCIDENT,
|
|
3677
|
+
` ${CRITICAL_INCIDENT} `,
|
|
3678
|
+
MAJOR_INCIDENT,
|
|
3679
|
+
CRITICAL_INCIDENT,
|
|
3680
|
+
]),
|
|
3681
|
+
).toEqual([CRITICAL_INCIDENT, MAJOR_INCIDENT, MINOR_INCIDENT].sort());
|
|
3682
|
+
});
|
|
3683
|
+
|
|
3684
|
+
test("lower-cases, because Postgres stores and compares uuids without case", () => {
|
|
3685
|
+
expect(
|
|
3686
|
+
TeamComplianceSettingServiceClass.getIds([
|
|
3687
|
+
CRITICAL_INCIDENT.toUpperCase(),
|
|
3688
|
+
CRITICAL_INCIDENT,
|
|
3689
|
+
{ _id: MAJOR_INCIDENT.toUpperCase() },
|
|
3690
|
+
]),
|
|
3691
|
+
).toEqual([CRITICAL_INCIDENT, MAJOR_INCIDENT]);
|
|
3692
|
+
});
|
|
3693
|
+
});
|
|
3694
|
+
|
|
3695
|
+
describe("TeamComplianceSettingService.getScope", () => {
|
|
3696
|
+
test.each(METHOD_RULE_TYPES)(
|
|
3697
|
+
"%s has no channel, no severity kind and no severities, whatever is passed",
|
|
3698
|
+
(ruleType: ComplianceRuleType) => {
|
|
3699
|
+
expect(
|
|
3700
|
+
TeamComplianceSettingServiceClass.getScope({
|
|
3701
|
+
ruleType: ruleType,
|
|
3702
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
3703
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
3704
|
+
alertSeverities: [CRITICAL_ALERT],
|
|
3705
|
+
}),
|
|
3706
|
+
).toEqual({
|
|
3707
|
+
ruleType: ruleType,
|
|
3708
|
+
notificationChannel: null,
|
|
3709
|
+
severityKind: null,
|
|
3710
|
+
severityIds: [],
|
|
3711
|
+
});
|
|
3712
|
+
},
|
|
3713
|
+
);
|
|
3714
|
+
|
|
3715
|
+
test.each(INCIDENT_RULE_TYPES)(
|
|
3716
|
+
"%s is scoped by its incident severities only",
|
|
3717
|
+
(ruleType: ComplianceRuleType) => {
|
|
3718
|
+
expect(
|
|
3719
|
+
TeamComplianceSettingServiceClass.getScope({
|
|
3720
|
+
ruleType: ruleType,
|
|
3721
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
3722
|
+
incidentSeverities: [MAJOR_INCIDENT, CRITICAL_INCIDENT],
|
|
3723
|
+
alertSeverities: [CRITICAL_ALERT],
|
|
3724
|
+
}),
|
|
3725
|
+
).toEqual({
|
|
3726
|
+
ruleType: ruleType,
|
|
3727
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
3728
|
+
severityKind: ComplianceSeverityKind.Incident,
|
|
3729
|
+
severityIds: [CRITICAL_INCIDENT, MAJOR_INCIDENT].sort(),
|
|
3730
|
+
});
|
|
3731
|
+
},
|
|
3732
|
+
);
|
|
3733
|
+
|
|
3734
|
+
test.each(ALERT_RULE_TYPES)(
|
|
3735
|
+
"%s is scoped by its alert severities only",
|
|
3736
|
+
(ruleType: ComplianceRuleType) => {
|
|
3737
|
+
expect(
|
|
3738
|
+
TeamComplianceSettingServiceClass.getScope({
|
|
3739
|
+
ruleType: ruleType,
|
|
3740
|
+
notificationChannel: null,
|
|
3741
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
3742
|
+
alertSeverities: [MAJOR_ALERT],
|
|
3743
|
+
}),
|
|
3744
|
+
).toEqual({
|
|
3745
|
+
ruleType: ruleType,
|
|
3746
|
+
notificationChannel: null,
|
|
3747
|
+
severityKind: ComplianceSeverityKind.Alert,
|
|
3748
|
+
severityIds: [MAJOR_ALERT],
|
|
3749
|
+
});
|
|
3750
|
+
},
|
|
3751
|
+
);
|
|
3752
|
+
|
|
3753
|
+
test.each([
|
|
3754
|
+
["undefined", undefined],
|
|
3755
|
+
["null", null],
|
|
3756
|
+
["an unknown channel", "Pager"],
|
|
3757
|
+
["a channel in the wrong case", "call"],
|
|
3758
|
+
["a number", 3],
|
|
3759
|
+
])("reads %s as 'any channel'", (_label: string, channel: unknown) => {
|
|
3760
|
+
expect(
|
|
3761
|
+
TeamComplianceSettingServiceClass.getScope({
|
|
3762
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3763
|
+
notificationChannel: channel,
|
|
3764
|
+
incidentSeverities: undefined,
|
|
3765
|
+
alertSeverities: undefined,
|
|
3766
|
+
}).notificationChannel,
|
|
3767
|
+
).toBeNull();
|
|
3768
|
+
});
|
|
3769
|
+
|
|
3770
|
+
test.each(
|
|
3771
|
+
COMPLIANCE_RULE_DEFINITIONS.map(
|
|
3772
|
+
(definition: ComplianceRuleDefinition): [ComplianceRuleType] => {
|
|
3773
|
+
return [definition.ruleType];
|
|
3774
|
+
},
|
|
3775
|
+
),
|
|
3776
|
+
)(
|
|
3777
|
+
"%s: the scope agrees with the rule catalog",
|
|
3778
|
+
(ruleType: ComplianceRuleType) => {
|
|
3779
|
+
const scope: ComplianceRuleScope =
|
|
3780
|
+
TeamComplianceSettingServiceClass.getScope({
|
|
3781
|
+
ruleType: ruleType,
|
|
3782
|
+
notificationChannel: ComplianceNotificationChannel.Slack,
|
|
3783
|
+
incidentSeverities: [CRITICAL_INCIDENT],
|
|
3784
|
+
alertSeverities: [CRITICAL_ALERT],
|
|
3785
|
+
});
|
|
3786
|
+
|
|
3787
|
+
expect(scope.severityKind).toBe(
|
|
3788
|
+
ComplianceRule.getSeverityKind(ruleType) || null,
|
|
3789
|
+
);
|
|
3790
|
+
expect(scope.notificationChannel).toBe(
|
|
3791
|
+
ComplianceRule.supportsChannel(ruleType)
|
|
3792
|
+
? ComplianceNotificationChannel.Slack
|
|
3793
|
+
: null,
|
|
3794
|
+
);
|
|
3795
|
+
expect(scope.severityIds).toHaveLength(
|
|
3796
|
+
ComplianceRule.supportsSeverityScope(ruleType) ? 1 : 0,
|
|
3797
|
+
);
|
|
3798
|
+
},
|
|
3799
|
+
);
|
|
3800
|
+
});
|
|
3801
|
+
|
|
3802
|
+
describe("TeamComplianceSettingService.isSameScope", () => {
|
|
3803
|
+
const base: ComplianceRuleScope = TeamComplianceSettingServiceClass.getScope({
|
|
3804
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3805
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
3806
|
+
incidentSeverities: [CRITICAL_INCIDENT, MAJOR_INCIDENT],
|
|
3807
|
+
alertSeverities: undefined,
|
|
3808
|
+
});
|
|
3809
|
+
|
|
3810
|
+
test("a scope is the same as itself and as an identical copy", () => {
|
|
3811
|
+
expect(TeamComplianceSettingServiceClass.isSameScope(base, base)).toBe(
|
|
3812
|
+
true,
|
|
3813
|
+
);
|
|
3814
|
+
expect(
|
|
3815
|
+
TeamComplianceSettingServiceClass.isSameScope(base, {
|
|
3816
|
+
...base,
|
|
3817
|
+
severityIds: [...base.severityIds],
|
|
3818
|
+
}),
|
|
3819
|
+
).toBe(true);
|
|
3820
|
+
});
|
|
3821
|
+
|
|
3822
|
+
test("severity order, repeats and case do not matter once read through getScope", () => {
|
|
3823
|
+
const reordered: ComplianceRuleScope =
|
|
3824
|
+
TeamComplianceSettingServiceClass.getScope({
|
|
3825
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3826
|
+
notificationChannel: ComplianceNotificationChannel.Call,
|
|
3827
|
+
incidentSeverities: [
|
|
3828
|
+
MAJOR_INCIDENT.toUpperCase(),
|
|
3829
|
+
{ _id: CRITICAL_INCIDENT },
|
|
3830
|
+
MAJOR_INCIDENT,
|
|
3831
|
+
],
|
|
3832
|
+
alertSeverities: [CRITICAL_ALERT],
|
|
3833
|
+
});
|
|
3834
|
+
|
|
3835
|
+
expect(TeamComplianceSettingServiceClass.isSameScope(base, reordered)).toBe(
|
|
3836
|
+
true,
|
|
3837
|
+
);
|
|
3838
|
+
expect(TeamComplianceSettingServiceClass.isSameScope(reordered, base)).toBe(
|
|
3839
|
+
true,
|
|
3840
|
+
);
|
|
3841
|
+
});
|
|
3842
|
+
|
|
3843
|
+
test.each<[string, Partial<ComplianceRuleScope>]>([
|
|
3844
|
+
[
|
|
3845
|
+
"the rule type",
|
|
3846
|
+
{ ruleType: ComplianceRuleType.HasIncidentEpisodeOnCallRules },
|
|
3847
|
+
],
|
|
3848
|
+
[
|
|
3849
|
+
"the channel",
|
|
3850
|
+
{ notificationChannel: ComplianceNotificationChannel.Push },
|
|
3851
|
+
],
|
|
3852
|
+
["'any channel' against one channel", { notificationChannel: null }],
|
|
3853
|
+
["a narrower severity set", { severityIds: [CRITICAL_INCIDENT] }],
|
|
3854
|
+
["every severity against some", { severityIds: [] }],
|
|
3855
|
+
[
|
|
3856
|
+
"a wider severity set",
|
|
3857
|
+
{
|
|
3858
|
+
severityIds: [CRITICAL_INCIDENT, MAJOR_INCIDENT, MINOR_INCIDENT].sort(),
|
|
3859
|
+
},
|
|
3860
|
+
],
|
|
3861
|
+
])(
|
|
3862
|
+
"differs by %s",
|
|
3863
|
+
(_label: string, change: Partial<ComplianceRuleScope>) => {
|
|
3864
|
+
const other: ComplianceRuleScope = { ...base, ...change };
|
|
3865
|
+
|
|
3866
|
+
expect(TeamComplianceSettingServiceClass.isSameScope(base, other)).toBe(
|
|
3867
|
+
false,
|
|
3868
|
+
);
|
|
3869
|
+
expect(TeamComplianceSettingServiceClass.isSameScope(other, base)).toBe(
|
|
3870
|
+
false,
|
|
3871
|
+
);
|
|
3872
|
+
},
|
|
3873
|
+
);
|
|
3874
|
+
});
|
|
3875
|
+
|
|
3876
|
+
/*
|
|
3877
|
+
* ---------------------------------------------------------------------------
|
|
3878
|
+
* Through updateOneById, the path BaseAPI takes.
|
|
3879
|
+
* ---------------------------------------------------------------------------
|
|
3880
|
+
*/
|
|
3881
|
+
|
|
3882
|
+
type InternalFindFunction = (
|
|
3883
|
+
findBy: JSONObject,
|
|
3884
|
+
) => Promise<Array<TeamComplianceSetting>>;
|
|
3885
|
+
|
|
3886
|
+
interface ServiceInternals {
|
|
3887
|
+
_findBy: InternalFindFunction;
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
describe("TeamComplianceSettingService updates reach the write normalised", () => {
|
|
3891
|
+
let service: TeamComplianceSettingServiceClass;
|
|
3892
|
+
let save: jest.Mock;
|
|
3893
|
+
let repositoryUpdate: jest.Mock;
|
|
3894
|
+
|
|
3895
|
+
beforeEach(() => {
|
|
3896
|
+
service = new TeamComplianceSettingServiceClass();
|
|
3897
|
+
|
|
3898
|
+
store(INCIDENT_CALL_CRITICAL);
|
|
3899
|
+
|
|
3900
|
+
// The hook's reads, from the in-memory team.
|
|
3901
|
+
jest.spyOn(service, "findBy").mockImplementation(((findBy: {
|
|
3902
|
+
query: JSONObject;
|
|
3903
|
+
}) => {
|
|
3904
|
+
return Promise.resolve(
|
|
3905
|
+
storedSettings.filter((row: TeamComplianceSetting): boolean => {
|
|
3906
|
+
return matchesQuery(row, findBy.query);
|
|
3907
|
+
}),
|
|
3908
|
+
);
|
|
3909
|
+
}) as never);
|
|
3910
|
+
|
|
3911
|
+
// _updateBy's own internal find of the row it is about to write.
|
|
3912
|
+
jest
|
|
3913
|
+
.spyOn(service as unknown as ServiceInternals, "_findBy")
|
|
3914
|
+
.mockImplementation((): Promise<Array<TeamComplianceSetting>> => {
|
|
3915
|
+
const row: TeamComplianceSetting = new TeamComplianceSetting();
|
|
3916
|
+
row._id = SETTING_ID;
|
|
3917
|
+
row.projectId = PROJECT_ID;
|
|
3918
|
+
return Promise.resolve([row]);
|
|
3919
|
+
});
|
|
3920
|
+
|
|
3921
|
+
save = jest.fn((item: unknown) => {
|
|
3922
|
+
return Promise.resolve(item);
|
|
3923
|
+
});
|
|
3924
|
+
repositoryUpdate = jest.fn(() => {
|
|
3925
|
+
return Promise.resolve({ affected: 1 });
|
|
3926
|
+
});
|
|
3927
|
+
jest.spyOn(service, "getRepository").mockReturnValue({
|
|
3928
|
+
save: save,
|
|
3929
|
+
update: repositoryUpdate,
|
|
3930
|
+
} as never);
|
|
3931
|
+
|
|
3932
|
+
// The permission layer needs a database and is not what this is about.
|
|
3933
|
+
jest
|
|
3934
|
+
.spyOn(ModelPermission, "checkUpdateQueryPermissions")
|
|
3935
|
+
.mockImplementation(((_modelType: unknown, query: unknown) => {
|
|
3936
|
+
return Promise.resolve(query);
|
|
3937
|
+
}) as never);
|
|
3938
|
+
jest
|
|
3939
|
+
.spyOn(ModelPermission, "checkUpdatePermissionByModel")
|
|
3940
|
+
.mockResolvedValue(undefined as never);
|
|
3941
|
+
});
|
|
3942
|
+
|
|
3943
|
+
test("a toggle is a plain column update: no hook reads, no relation write", async () => {
|
|
3944
|
+
await service.updateOneById({
|
|
3945
|
+
id: new ObjectID(SETTING_ID),
|
|
3946
|
+
data: { enabled: false },
|
|
3947
|
+
props: { isRoot: true },
|
|
3948
|
+
});
|
|
3949
|
+
|
|
3950
|
+
expect(service.findBy).not.toHaveBeenCalled();
|
|
3951
|
+
expect(save).not.toHaveBeenCalled();
|
|
3952
|
+
expect(repositoryUpdate).toHaveBeenCalledTimes(1);
|
|
3953
|
+
expect(repositoryUpdate.mock.calls[0]?.[1]).toMatchObject({
|
|
3954
|
+
enabled: false,
|
|
3955
|
+
});
|
|
3956
|
+
});
|
|
3957
|
+
|
|
3958
|
+
test("a rule type change writes the cleared channel and severities with the new type", async () => {
|
|
3959
|
+
await service.updateOneById({
|
|
3960
|
+
id: new ObjectID(SETTING_ID),
|
|
3961
|
+
data: { ruleType: ComplianceRuleType.HasNotificationEmailMethod },
|
|
3962
|
+
props: { isRoot: true },
|
|
3963
|
+
});
|
|
3964
|
+
|
|
3965
|
+
// The severity lists are relations, so the row goes through save().
|
|
3966
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
3967
|
+
|
|
3968
|
+
const written: Record<string, unknown> = save.mock.calls[0]?.[0] as Record<
|
|
3969
|
+
string,
|
|
3970
|
+
unknown
|
|
3971
|
+
>;
|
|
3972
|
+
expect(written["_id"]).toBe(SETTING_ID);
|
|
3973
|
+
expect(written["ruleType"]).toBe(
|
|
3974
|
+
ComplianceRuleType.HasNotificationEmailMethod,
|
|
3975
|
+
);
|
|
3976
|
+
expect(written["notificationChannel"]).toBeNull();
|
|
3977
|
+
expect(written["incidentSeverities"]).toEqual([]);
|
|
3978
|
+
expect(written["alertSeverities"]).toEqual([]);
|
|
3979
|
+
});
|
|
3980
|
+
|
|
3981
|
+
test("a duplicate is refused before anything is written", async () => {
|
|
3982
|
+
store({
|
|
3983
|
+
id: SIBLING_SETTING_ID,
|
|
3984
|
+
ruleType: ComplianceRuleType.HasIncidentOnCallRules,
|
|
3985
|
+
notificationChannel: ComplianceNotificationChannel.Push,
|
|
3986
|
+
incidentSeverityIds: [CRITICAL_INCIDENT],
|
|
3987
|
+
});
|
|
3988
|
+
|
|
3989
|
+
await expect(
|
|
3990
|
+
service.updateOneById({
|
|
3991
|
+
id: new ObjectID(SETTING_ID),
|
|
3992
|
+
data: { notificationChannel: ComplianceNotificationChannel.Push },
|
|
3993
|
+
props: { isRoot: true },
|
|
3994
|
+
}),
|
|
3995
|
+
).rejects.toThrow(DUPLICATE_COMPLIANCE_RULE_MESSAGE);
|
|
3996
|
+
|
|
3997
|
+
expect(save).not.toHaveBeenCalled();
|
|
3998
|
+
expect(repositoryUpdate).not.toHaveBeenCalled();
|
|
3999
|
+
});
|
|
4000
|
+
test("a repeated severity, in any case, reaches the relation save once and lower-cased", async () => {
|
|
4001
|
+
await service.updateOneById({
|
|
4002
|
+
id: new ObjectID(SETTING_ID),
|
|
4003
|
+
data: {
|
|
4004
|
+
incidentSeverities: [
|
|
4005
|
+
{ _id: MAJOR_INCIDENT.toUpperCase() },
|
|
4006
|
+
{ _id: MAJOR_INCIDENT },
|
|
4007
|
+
],
|
|
4008
|
+
} as never,
|
|
4009
|
+
props: { isRoot: true },
|
|
4010
|
+
});
|
|
4011
|
+
|
|
4012
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
4013
|
+
|
|
4014
|
+
const written: Record<string, unknown> = save.mock.calls[0]?.[0] as Record<
|
|
4015
|
+
string,
|
|
4016
|
+
unknown
|
|
4017
|
+
>;
|
|
4018
|
+
expect(
|
|
4019
|
+
(written["incidentSeverities"] as Array<{ _id?: string }>).map(
|
|
4020
|
+
(item: { _id?: string }): string | undefined => {
|
|
4021
|
+
return item._id;
|
|
4022
|
+
},
|
|
4023
|
+
),
|
|
4024
|
+
).toEqual([MAJOR_INCIDENT]);
|
|
4025
|
+
});
|
|
4026
|
+
|
|
4027
|
+
test("another project's severity named by `id` behind a non-string `_id` is refused before anything is written", async () => {
|
|
4028
|
+
await expect(
|
|
4029
|
+
service.updateOneById({
|
|
4030
|
+
id: new ObjectID(SETTING_ID),
|
|
4031
|
+
data: {
|
|
4032
|
+
incidentSeverities: [{ _id: 1, id: FOREIGN_INCIDENT }],
|
|
4033
|
+
} as never,
|
|
4034
|
+
props: EDITOR_PROPS,
|
|
4035
|
+
}),
|
|
4036
|
+
).rejects.toThrow(FOREIGN_INCIDENT_SEVERITY_MESSAGE);
|
|
4037
|
+
|
|
4038
|
+
expect(save).not.toHaveBeenCalled();
|
|
4039
|
+
expect(repositoryUpdate).not.toHaveBeenCalled();
|
|
190
4040
|
});
|
|
191
4041
|
});
|