@oneuptime/common 14.0.6 → 14.0.8
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/InventoryItem.ts +23 -12
- package/Models/DatabaseModels/StatusPage.ts +55 -0
- package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
- package/Server/API/IncidentAPI.ts +162 -0
- package/Server/API/StatusPageAPI.ts +487 -171
- package/Server/API/StatusPageSubscriberAPI.ts +63 -15
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -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/Index.ts +16 -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/Services/AlertService.ts +32 -36
- package/Server/Services/DatabaseService.ts +93 -10
- package/Server/Services/IncidentAlertService.ts +609 -2
- 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 +1663 -103
- package/Server/Services/IncidentStateTimelineService.ts +13 -0
- package/Server/Services/IncidentTemplateService.ts +109 -0
- package/Server/Services/InventoryItemService.ts +38 -0
- package/Server/Services/IoTDeviceCredentialService.ts +13 -4
- package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
- package/Server/Services/ScheduledMaintenanceService.ts +118 -64
- 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/UserNotificationRuleService.ts +55 -53
- package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
- package/Server/Types/Markdown.ts +153 -6
- package/Server/Types/Workflow/Components/Email.ts +61 -2
- package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
- package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
- package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
- package/Server/Utils/DataSource/EgressGuard.ts +66 -21
- package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
- package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
- package/Server/Utils/Response.ts +31 -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/Topology/TopologyConcurrencyLimiter.ts +229 -0
- package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
- package/Server/Utils/Topology/TopologyRequest.ts +463 -0
- package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
- package/Server/Utils/Topology/TopologySql.ts +1024 -0
- 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/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/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
- package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
- package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
- package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
- package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
- package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
- package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
- package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
- package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
- package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
- package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
- package/Tests/App/Dashboard/EventNotes.test.tsx +776 -0
- 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 +534 -6
- package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
- package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
- package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
- package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
- package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
- 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/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/InfrastructureExplorer.test.tsx +1365 -57
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
- package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
- package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
- package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
- 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/TopologyDataLifecycle.test.tsx +583 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
- package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
- package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -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/ExpiredSessionRouteGuards.test.ts +7 -0
- package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -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/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
- package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
- package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
- 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/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
- package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
- package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
- package/Tests/Server/Services/DomainService.test.ts +525 -0
- package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -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/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -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/InventoryItemServiceOverviewCounts.test.ts +345 -0
- package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
- package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
- package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
- package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
- package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
- package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
- package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
- 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/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/AffectedResources/LinkedAffectedResources.test.ts +858 -0
- package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -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/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
- package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
- package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
- package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
- package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
- package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
- package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
- package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
- package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
- package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
- package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -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/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/Topology/TopologyTypeRules.test.ts +176 -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/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
- package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
- package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
- 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/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/Incident/IncidentAlertLink.ts +19 -0
- 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/Telemetry/InventoryLiveness.ts +26 -0
- package/Types/Topology/TopologyApi.ts +428 -0
- package/Types/Topology/TopologyTypeRules.ts +191 -0
- 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/Forms/ModelForm.tsx +70 -8
- package/UI/Components/Forms/Validation.ts +1 -1
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
- package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
- package/UI/Components/ModelTable/BaseModelTable.tsx +9 -8
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
- package/UI/Components/ModelTable/ModelTable.tsx +6 -7
- package/UI/Utils/SensitiveUrlToken.ts +10 -5
- package/Utils/API.ts +247 -2
- package/Utils/Incident/IncidentFromAlerts.ts +78 -0
- 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/InventoryItem.js +20 -11
- package/build/dist/Models/DatabaseModels/InventoryItem.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/Server/API/IncidentAPI.js +100 -1
- package/build/dist/Server/API/IncidentAPI.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/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
- 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/Index.js +16 -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/Services/AlertService.js +21 -33
- package/build/dist/Server/Services/AlertService.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/IncidentAlertService.js +457 -3
- package/build/dist/Server/Services/IncidentAlertService.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 +1148 -42
- package/build/dist/Server/Services/IncidentService.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/InventoryItemService.js +31 -0
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
- package/build/dist/Server/Services/IoTDeviceCredentialService.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 +84 -57
- package/build/dist/Server/Services/ScheduledMaintenanceService.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/UserNotificationRuleService.js +55 -51
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.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/AffectedResources/LinkedAffectedResources.js +405 -0
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
- 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/Inventory/InventoryOverviewAggregation.js +81 -0
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
- 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/Response.js +30 -0
- package/build/dist/Server/Utils/Response.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/Topology/TopologyConcurrencyLimiter.js +178 -0
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
- 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/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/Incident/IncidentAlertLink.js +17 -0
- package/build/dist/Types/Incident/IncidentAlertLink.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/Telemetry/InventoryLiveness.js +23 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
- package/build/dist/Types/Topology/TopologyApi.js +55 -0
- package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
- 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/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/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 +2 -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/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/Incident/IncidentFromAlerts.js +38 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.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,19 +1,51 @@
|
|
|
1
1
|
import Entities from "../../../Models/DatabaseModels/Index";
|
|
2
|
+
import Incident from "../../../Models/DatabaseModels/Incident";
|
|
2
3
|
import IncidentAlert from "../../../Models/DatabaseModels/IncidentAlert";
|
|
4
|
+
import { AlertFeedEventType } from "../../../Models/DatabaseModels/AlertFeed";
|
|
3
5
|
import PostgresAppInstance from "../../../Server/Infrastructure/PostgresDatabase";
|
|
6
|
+
import Semaphore, {
|
|
7
|
+
SemaphoreMutex,
|
|
8
|
+
} from "../../../Server/Infrastructure/Semaphore";
|
|
4
9
|
import AlertFeedService from "../../../Server/Services/AlertFeedService";
|
|
10
|
+
import AlertMeasurementValueService from "../../../Server/Services/AlertMeasurementValueService";
|
|
5
11
|
import AlertService from "../../../Server/Services/AlertService";
|
|
12
|
+
import AlertStateTimelineService from "../../../Server/Services/AlertStateTimelineService";
|
|
13
|
+
import AutoRemediationRuleEngineService from "../../../Server/Services/AutoRemediationRuleEngineService";
|
|
14
|
+
import CustomFieldMappingService from "../../../Server/Services/CustomFieldMappingService";
|
|
6
15
|
import IncidentAlertService, {
|
|
16
|
+
AcknowledgeDeclaredAlertsResult,
|
|
17
|
+
AlertsToAcknowledgeOnDeclare,
|
|
7
18
|
LinkAlertsToIncidentResult,
|
|
8
19
|
} from "../../../Server/Services/IncidentAlertService";
|
|
9
20
|
import IncidentFeedService from "../../../Server/Services/IncidentFeedService";
|
|
21
|
+
import IncidentGroupingEngineService from "../../../Server/Services/IncidentGroupingEngineService";
|
|
22
|
+
import IncidentLabelRuleEngineService from "../../../Server/Services/IncidentLabelRuleEngineService";
|
|
23
|
+
import IncidentOnCallRuleEngineService from "../../../Server/Services/IncidentOnCallRuleEngineService";
|
|
24
|
+
import IncidentOwnerRuleEngineService from "../../../Server/Services/IncidentOwnerRuleEngineService";
|
|
25
|
+
import IncidentOwnerUserService from "../../../Server/Services/IncidentOwnerUserService";
|
|
26
|
+
import IncidentPrivacyRuleEngineService from "../../../Server/Services/IncidentPrivacyRuleEngineService";
|
|
10
27
|
import IncidentService from "../../../Server/Services/IncidentService";
|
|
28
|
+
import IncidentSlaService from "../../../Server/Services/IncidentSlaService";
|
|
29
|
+
import RunbookRuleEngineService from "../../../Server/Services/RunbookRuleEngineService";
|
|
30
|
+
import WorkspaceNotificationRuleService from "../../../Server/Services/WorkspaceNotificationRuleService";
|
|
31
|
+
import AIIncidentInvestigationRunner from "../../../Server/Utils/AI/SRE/IncidentInvestigationRunner";
|
|
32
|
+
import AlertStateChangeAuthorization from "../../../Server/Utils/Alert/AlertStateChangeAuthorization";
|
|
11
33
|
import PostgresErrorTranslator from "../../../Server/Utils/Database/PostgresErrorTranslator";
|
|
34
|
+
import ProductAnalytics from "../../../Server/Utils/ProductAnalytics";
|
|
12
35
|
import URL from "../../../Types/API/URL";
|
|
13
36
|
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
14
|
-
import
|
|
37
|
+
import PermissionScope from "../../../Types/Database/AccessControl/PermissionScope";
|
|
38
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
39
|
+
import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
|
|
40
|
+
import {
|
|
41
|
+
INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY,
|
|
42
|
+
INCIDENT_ALERT_ALREADY_LINKED_MESSAGE,
|
|
43
|
+
INCIDENT_ALERT_IDS_TO_LINK_KEY,
|
|
44
|
+
} from "../../../Types/Incident/IncidentAlertLink";
|
|
45
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
15
46
|
import ObjectID from "../../../Types/ObjectID";
|
|
16
47
|
import Permission, {
|
|
48
|
+
UserPermission,
|
|
17
49
|
UserTenantAccessPermission,
|
|
18
50
|
} from "../../../Types/Permission";
|
|
19
51
|
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
@@ -900,4 +932,1596 @@ describePostgres("IncidentAlert against a migrated Postgres", () => {
|
|
|
900
932
|
expect(await linkCount()).toBe(0);
|
|
901
933
|
});
|
|
902
934
|
});
|
|
935
|
+
|
|
936
|
+
/*
|
|
937
|
+
* Declaring an incident from alerts and asking, in the same request, for
|
|
938
|
+
* the alerts to be acknowledged (miscDataProps.acknowledgeAlertsToLink):
|
|
939
|
+
* the real IncidentService.create, the real links, and the real
|
|
940
|
+
* acknowledgement - AlertService.changeAlertState writing an
|
|
941
|
+
* AlertStateTimeline row and moving the alert's current state - against
|
|
942
|
+
* the clones. This block adds clones of the tables that path reads and
|
|
943
|
+
* writes: the alert and incident states, the incident severities, the
|
|
944
|
+
* alert state timeline, and the label / monitor / SLO relations the new
|
|
945
|
+
* incident is re-read with (and the alerts' labels, which the permission
|
|
946
|
+
* check reads). They live in the same schema and are dropped with it.
|
|
947
|
+
*
|
|
948
|
+
* Stubbed, beyond the stubs above: the Redis locks (there is no Redis
|
|
949
|
+
* here), and what surrounds the declaration without being about the
|
|
950
|
+
* alerts' state - the un-awaited onCreateSuccess chain (workspace
|
|
951
|
+
* channels, feeds, rules, SLA, reminders, AI), metrics, custom field
|
|
952
|
+
* mappings, auto-ownership, workflows and realtime.
|
|
953
|
+
*/
|
|
954
|
+
describe("declaring an incident from alerts and acknowledging them, through IncidentService", () => {
|
|
955
|
+
const DECLARE_TABLES: Array<string> = [
|
|
956
|
+
"AlertState",
|
|
957
|
+
"AlertStateTimeline",
|
|
958
|
+
"IncidentState",
|
|
959
|
+
"IncidentSeverity",
|
|
960
|
+
"Label",
|
|
961
|
+
"AlertLabel",
|
|
962
|
+
"IncidentLabel",
|
|
963
|
+
"Monitor",
|
|
964
|
+
"IncidentMonitor",
|
|
965
|
+
"ServiceLevelObjective",
|
|
966
|
+
"IncidentServiceLevelObjective",
|
|
967
|
+
];
|
|
968
|
+
|
|
969
|
+
// The create does not wait for the acknowledgement: how long a test does.
|
|
970
|
+
const ACKNOWLEDGEMENT_TIMEOUT_MS: number = 15000;
|
|
971
|
+
|
|
972
|
+
const NO_ACKNOWLEDGED_STATE_MESSAGE: string =
|
|
973
|
+
"This project has no Acknowledged alert state, so the alerts cannot be acknowledged. Declare the incident without acknowledging them, or add an Acknowledged state in the alert settings.";
|
|
974
|
+
|
|
975
|
+
const NO_PERMISSION_TO_ACKNOWLEDGE_MESSAGE: string =
|
|
976
|
+
"You do not have permission to acknowledge one or more of these alerts. Declare the incident without acknowledging them, or ask a project admin for permission.";
|
|
977
|
+
|
|
978
|
+
const PRIVATE_INCIDENT_CAUSE: string =
|
|
979
|
+
"Acknowledged because a private incident was declared from this alert.";
|
|
980
|
+
|
|
981
|
+
const NOT_VISIBLE_ALERTS_MESSAGE: string =
|
|
982
|
+
"One or more of the selected alerts do not exist in this project, or you do not have access to them.";
|
|
983
|
+
|
|
984
|
+
type AcknowledgeArguments = Parameters<
|
|
985
|
+
typeof IncidentAlertService.acknowledgeAlertsDeclaredWithIncident
|
|
986
|
+
>[0];
|
|
987
|
+
|
|
988
|
+
type SyncArguments = Parameters<
|
|
989
|
+
typeof IncidentAlertService.syncAlertWithLinkedIncidentState
|
|
990
|
+
>[0];
|
|
991
|
+
|
|
992
|
+
type AlertFeedItem = Parameters<
|
|
993
|
+
typeof AlertFeedService.createAlertFeedItem
|
|
994
|
+
>[0];
|
|
995
|
+
|
|
996
|
+
type StateFlag =
|
|
997
|
+
| "isCreatedState"
|
|
998
|
+
| "isAcknowledgedState"
|
|
999
|
+
| "isResolvedState";
|
|
1000
|
+
|
|
1001
|
+
interface ProjectStates {
|
|
1002
|
+
created: ObjectID;
|
|
1003
|
+
acknowledged: ObjectID;
|
|
1004
|
+
resolved: ObjectID;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
interface TimelineRow {
|
|
1008
|
+
alertStateId: string;
|
|
1009
|
+
createdByUserId: string | null;
|
|
1010
|
+
rootCause: string | null;
|
|
1011
|
+
isOwnerNotified: boolean;
|
|
1012
|
+
startsAt: Date;
|
|
1013
|
+
endsAt: Date | null;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
interface IncidentRow {
|
|
1017
|
+
_id: string;
|
|
1018
|
+
incidentNumber: number | null;
|
|
1019
|
+
currentIncidentStateId: string;
|
|
1020
|
+
isPrivate: boolean;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// What validateAcknowledgeAlertsForNewIncident settled, as strings.
|
|
1024
|
+
interface CheckedAcknowledgement {
|
|
1025
|
+
acknowledgedAlertStateId: string;
|
|
1026
|
+
alertIdsToAcknowledge: Array<string>;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const ownSpies: Array<jest.SpyInstance> = [];
|
|
1030
|
+
|
|
1031
|
+
let alertFeed: jest.SpyInstance;
|
|
1032
|
+
let acknowledge: jest.SpyInstance;
|
|
1033
|
+
let linksWhenAcknowledging: Array<number> = [];
|
|
1034
|
+
|
|
1035
|
+
let alertStates: ProjectStates;
|
|
1036
|
+
let incidentStates: ProjectStates;
|
|
1037
|
+
let incidentSeverityId: ObjectID;
|
|
1038
|
+
|
|
1039
|
+
beforeAll(async () => {
|
|
1040
|
+
for (const table of DECLARE_TABLES) {
|
|
1041
|
+
await database.query(
|
|
1042
|
+
`CREATE TABLE "${schema}"."${table}" (LIKE public."${table}" INCLUDING ALL)`,
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const own: (spy: jest.SpyInstance) => void = (
|
|
1047
|
+
spy: jest.SpyInstance,
|
|
1048
|
+
): void => {
|
|
1049
|
+
ownSpies.push(spy);
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
// No Redis here, and one request at a time.
|
|
1053
|
+
own(
|
|
1054
|
+
jest.spyOn(Semaphore, "lock").mockResolvedValue({} as SemaphoreMutex),
|
|
1055
|
+
);
|
|
1056
|
+
own(jest.spyOn(Semaphore, "release").mockResolvedValue(undefined));
|
|
1057
|
+
|
|
1058
|
+
own(
|
|
1059
|
+
jest
|
|
1060
|
+
.spyOn(ProductAnalytics, "captureForUser")
|
|
1061
|
+
.mockImplementation((): void => {
|
|
1062
|
+
// No analytics in tests.
|
|
1063
|
+
}),
|
|
1064
|
+
);
|
|
1065
|
+
own(
|
|
1066
|
+
jest
|
|
1067
|
+
.spyOn(CustomFieldMappingService, "applyMappingsToCreate")
|
|
1068
|
+
.mockResolvedValue(undefined),
|
|
1069
|
+
);
|
|
1070
|
+
own(
|
|
1071
|
+
jest
|
|
1072
|
+
.spyOn(CustomFieldMappingService, "applyMappingsToUpdate")
|
|
1073
|
+
.mockResolvedValue(undefined),
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
// Workflows and realtime of every model the declaration writes.
|
|
1077
|
+
own(
|
|
1078
|
+
jest
|
|
1079
|
+
.spyOn(IncidentService, "onTriggerWorkflow")
|
|
1080
|
+
.mockResolvedValue(undefined),
|
|
1081
|
+
);
|
|
1082
|
+
own(
|
|
1083
|
+
jest
|
|
1084
|
+
.spyOn(IncidentService, "onTriggerRealtime")
|
|
1085
|
+
.mockResolvedValue(undefined),
|
|
1086
|
+
);
|
|
1087
|
+
own(
|
|
1088
|
+
jest
|
|
1089
|
+
.spyOn(AlertService, "onTriggerWorkflow")
|
|
1090
|
+
.mockResolvedValue(undefined),
|
|
1091
|
+
);
|
|
1092
|
+
own(
|
|
1093
|
+
jest
|
|
1094
|
+
.spyOn(AlertService, "onTriggerRealtime")
|
|
1095
|
+
.mockResolvedValue(undefined),
|
|
1096
|
+
);
|
|
1097
|
+
own(
|
|
1098
|
+
jest
|
|
1099
|
+
.spyOn(AlertStateTimelineService, "onTriggerWorkflow")
|
|
1100
|
+
.mockResolvedValue(undefined),
|
|
1101
|
+
);
|
|
1102
|
+
own(
|
|
1103
|
+
jest
|
|
1104
|
+
.spyOn(AlertStateTimelineService, "onTriggerRealtime")
|
|
1105
|
+
.mockResolvedValue(undefined),
|
|
1106
|
+
);
|
|
1107
|
+
|
|
1108
|
+
// The un-awaited onCreateSuccess chain: none of it moves an alert.
|
|
1109
|
+
const incidentInternals: Record<string, () => Promise<void>> =
|
|
1110
|
+
IncidentService as unknown as Record<string, () => Promise<void>>;
|
|
1111
|
+
for (const method of [
|
|
1112
|
+
"handleIncidentWorkspaceOperationsAsync",
|
|
1113
|
+
"createIncidentFeedAsync",
|
|
1114
|
+
"handleIncidentStateChangeAsync",
|
|
1115
|
+
"disableActiveMonitoringIfManualIncident",
|
|
1116
|
+
"refreshReminderSchedule",
|
|
1117
|
+
]) {
|
|
1118
|
+
own(jest.spyOn(incidentInternals, method).mockResolvedValue(undefined));
|
|
1119
|
+
}
|
|
1120
|
+
own(
|
|
1121
|
+
jest
|
|
1122
|
+
.spyOn(IncidentPrivacyRuleEngineService, "applyRulesToIncident")
|
|
1123
|
+
.mockResolvedValue(false),
|
|
1124
|
+
);
|
|
1125
|
+
own(
|
|
1126
|
+
jest
|
|
1127
|
+
.spyOn(IncidentOwnerRuleEngineService, "applyRulesToIncident")
|
|
1128
|
+
.mockResolvedValue(undefined),
|
|
1129
|
+
);
|
|
1130
|
+
own(
|
|
1131
|
+
jest
|
|
1132
|
+
.spyOn(IncidentLabelRuleEngineService, "applyRulesToIncident")
|
|
1133
|
+
.mockResolvedValue(undefined),
|
|
1134
|
+
);
|
|
1135
|
+
own(
|
|
1136
|
+
jest
|
|
1137
|
+
.spyOn(IncidentOnCallRuleEngineService, "applyRulesToIncident")
|
|
1138
|
+
.mockResolvedValue(undefined),
|
|
1139
|
+
);
|
|
1140
|
+
own(
|
|
1141
|
+
jest
|
|
1142
|
+
.spyOn(RunbookRuleEngineService, "applyRulesToIncident")
|
|
1143
|
+
.mockResolvedValue(undefined),
|
|
1144
|
+
);
|
|
1145
|
+
own(
|
|
1146
|
+
jest
|
|
1147
|
+
.spyOn(AutoRemediationRuleEngineService, "applyRulesToIncident")
|
|
1148
|
+
.mockResolvedValue(undefined),
|
|
1149
|
+
);
|
|
1150
|
+
own(
|
|
1151
|
+
jest
|
|
1152
|
+
.spyOn(IncidentGroupingEngineService, "processIncident")
|
|
1153
|
+
.mockResolvedValue({ grouped: false }),
|
|
1154
|
+
);
|
|
1155
|
+
own(
|
|
1156
|
+
jest
|
|
1157
|
+
.spyOn(IncidentSlaService, "createSlaForIncident")
|
|
1158
|
+
.mockResolvedValue(null),
|
|
1159
|
+
);
|
|
1160
|
+
own(
|
|
1161
|
+
jest
|
|
1162
|
+
.spyOn(AIIncidentInvestigationRunner, "investigateNewIncident")
|
|
1163
|
+
.mockResolvedValue(false),
|
|
1164
|
+
);
|
|
1165
|
+
own(
|
|
1166
|
+
jest
|
|
1167
|
+
.spyOn(IncidentAlertService, "createDeclaredFromAlertsFeedItem")
|
|
1168
|
+
.mockResolvedValue(undefined),
|
|
1169
|
+
);
|
|
1170
|
+
own(
|
|
1171
|
+
jest
|
|
1172
|
+
.spyOn(IncidentAlertService, "copyAlertOwnersToIncident")
|
|
1173
|
+
.mockResolvedValue({ userIds: [], teamIds: [] }),
|
|
1174
|
+
);
|
|
1175
|
+
// A member who declares becomes its owner: owner tables are not cloned.
|
|
1176
|
+
own(
|
|
1177
|
+
jest
|
|
1178
|
+
.spyOn(IncidentOwnerUserService, "create")
|
|
1179
|
+
.mockResolvedValue(undefined as never),
|
|
1180
|
+
);
|
|
1181
|
+
|
|
1182
|
+
// What an alert state change sets off besides the change itself.
|
|
1183
|
+
own(
|
|
1184
|
+
jest
|
|
1185
|
+
.spyOn(AlertService, "refreshAlertMetrics")
|
|
1186
|
+
.mockResolvedValue(undefined),
|
|
1187
|
+
);
|
|
1188
|
+
own(
|
|
1189
|
+
jest
|
|
1190
|
+
.spyOn(AlertMeasurementValueService, "recomputeForAlert")
|
|
1191
|
+
.mockResolvedValue(undefined),
|
|
1192
|
+
);
|
|
1193
|
+
own(
|
|
1194
|
+
jest
|
|
1195
|
+
.spyOn(WorkspaceNotificationRuleService, "archiveWorkspaceChannels")
|
|
1196
|
+
.mockResolvedValue(undefined),
|
|
1197
|
+
);
|
|
1198
|
+
|
|
1199
|
+
// The stub set up for the whole suite: the alerts' feed entries.
|
|
1200
|
+
alertFeed = jest.spyOn(AlertFeedService, "createAlertFeedItem");
|
|
1201
|
+
|
|
1202
|
+
/*
|
|
1203
|
+
* The real acknowledgement, recording how many links the table held
|
|
1204
|
+
* when it started. Its promise is kept by the spy (mock.results), so a
|
|
1205
|
+
* test can wait for the un-awaited work to finish.
|
|
1206
|
+
*/
|
|
1207
|
+
const acknowledgeForReal: (
|
|
1208
|
+
data: AcknowledgeArguments,
|
|
1209
|
+
) => Promise<AcknowledgeDeclaredAlertsResult> =
|
|
1210
|
+
IncidentAlertService.acknowledgeAlertsDeclaredWithIncident.bind(
|
|
1211
|
+
IncidentAlertService,
|
|
1212
|
+
);
|
|
1213
|
+
acknowledge = jest
|
|
1214
|
+
.spyOn(IncidentAlertService, "acknowledgeAlertsDeclaredWithIncident")
|
|
1215
|
+
.mockImplementation(
|
|
1216
|
+
async (
|
|
1217
|
+
data: AcknowledgeArguments,
|
|
1218
|
+
): Promise<AcknowledgeDeclaredAlertsResult> => {
|
|
1219
|
+
linksWhenAcknowledging.push(await linkCount());
|
|
1220
|
+
return acknowledgeForReal(data);
|
|
1221
|
+
},
|
|
1222
|
+
);
|
|
1223
|
+
own(acknowledge);
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
beforeEach(async () => {
|
|
1227
|
+
await database.query(
|
|
1228
|
+
DECLARE_TABLES.map((table: string): string => {
|
|
1229
|
+
return `DELETE FROM "${schema}"."${table}"`;
|
|
1230
|
+
}).join("; "),
|
|
1231
|
+
);
|
|
1232
|
+
|
|
1233
|
+
alertFeed.mockClear();
|
|
1234
|
+
acknowledge.mockClear();
|
|
1235
|
+
linksWhenAcknowledging = [];
|
|
1236
|
+
|
|
1237
|
+
alertStates = {
|
|
1238
|
+
created: await seedAlertState("Created", 1, "isCreatedState"),
|
|
1239
|
+
acknowledged: await seedAlertState(
|
|
1240
|
+
"Acknowledged",
|
|
1241
|
+
2,
|
|
1242
|
+
"isAcknowledgedState",
|
|
1243
|
+
),
|
|
1244
|
+
resolved: await seedAlertState("Resolved", 3, "isResolvedState"),
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
incidentStates = {
|
|
1248
|
+
created: await seedIncidentState("Identified", 1, "isCreatedState"),
|
|
1249
|
+
acknowledged: await seedIncidentState(
|
|
1250
|
+
"Acknowledged",
|
|
1251
|
+
2,
|
|
1252
|
+
"isAcknowledgedState",
|
|
1253
|
+
),
|
|
1254
|
+
resolved: await seedIncidentState("Resolved", 3, "isResolvedState"),
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
incidentSeverityId = await seedIncidentSeverity();
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
afterAll(() => {
|
|
1261
|
+
for (const spy of ownSpies) {
|
|
1262
|
+
spy.mockRestore();
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
async function seedAlertState(
|
|
1267
|
+
name: string,
|
|
1268
|
+
order: number,
|
|
1269
|
+
flag: StateFlag,
|
|
1270
|
+
): Promise<ObjectID> {
|
|
1271
|
+
const id: ObjectID = ObjectID.generate();
|
|
1272
|
+
await database.query(
|
|
1273
|
+
`INSERT INTO "${schema}"."AlertState"
|
|
1274
|
+
("_id", "projectId", "name", "color", "order", "isCreatedState", "isAcknowledgedState", "isResolvedState", "version")
|
|
1275
|
+
VALUES ($1, $2, $3, '#6b7280', $4, $5, $6, $7, 1)`,
|
|
1276
|
+
[
|
|
1277
|
+
id.toString(),
|
|
1278
|
+
projectId.toString(),
|
|
1279
|
+
name,
|
|
1280
|
+
order,
|
|
1281
|
+
flag === "isCreatedState",
|
|
1282
|
+
flag === "isAcknowledgedState",
|
|
1283
|
+
flag === "isResolvedState",
|
|
1284
|
+
],
|
|
1285
|
+
);
|
|
1286
|
+
return id;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
async function seedIncidentState(
|
|
1290
|
+
name: string,
|
|
1291
|
+
order: number,
|
|
1292
|
+
flag: StateFlag,
|
|
1293
|
+
): Promise<ObjectID> {
|
|
1294
|
+
const id: ObjectID = ObjectID.generate();
|
|
1295
|
+
await database.query(
|
|
1296
|
+
`INSERT INTO "${schema}"."IncidentState"
|
|
1297
|
+
("_id", "projectId", "name", "slug", "color", "order", "isCreatedState", "isAcknowledgedState", "isResolvedState", "version")
|
|
1298
|
+
VALUES ($1, $2, $3, $4, '#6b7280', $5, $6, $7, $8, 1)`,
|
|
1299
|
+
[
|
|
1300
|
+
id.toString(),
|
|
1301
|
+
projectId.toString(),
|
|
1302
|
+
name,
|
|
1303
|
+
`${name.toLowerCase()}-${id.toString()}`,
|
|
1304
|
+
order,
|
|
1305
|
+
flag === "isCreatedState",
|
|
1306
|
+
flag === "isAcknowledgedState",
|
|
1307
|
+
flag === "isResolvedState",
|
|
1308
|
+
],
|
|
1309
|
+
);
|
|
1310
|
+
return id;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
async function seedIncidentSeverity(): Promise<ObjectID> {
|
|
1314
|
+
const id: ObjectID = ObjectID.generate();
|
|
1315
|
+
await database.query(
|
|
1316
|
+
`INSERT INTO "${schema}"."IncidentSeverity"
|
|
1317
|
+
("_id", "projectId", "name", "slug", "color", "order", "version")
|
|
1318
|
+
VALUES ($1, $2, 'Critical', $3, '#dc2626', 1, 1)`,
|
|
1319
|
+
[id.toString(), projectId.toString(), `critical-${id.toString()}`],
|
|
1320
|
+
);
|
|
1321
|
+
return id;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/*
|
|
1325
|
+
* An alert that went through the given states, the last one current:
|
|
1326
|
+
* ten minutes apart, the last one starting ten minutes ago, each row
|
|
1327
|
+
* ending where the next begins - as AlertStateTimelineService writes
|
|
1328
|
+
* them.
|
|
1329
|
+
*/
|
|
1330
|
+
async function seedAlertThrough(
|
|
1331
|
+
states: Array<ObjectID>,
|
|
1332
|
+
options: { project?: ObjectID; isPrivate?: boolean } = {},
|
|
1333
|
+
): Promise<ObjectID> {
|
|
1334
|
+
const id: ObjectID = ObjectID.generate();
|
|
1335
|
+
const alertProjectId: ObjectID = options.project || projectId;
|
|
1336
|
+
|
|
1337
|
+
await database.query(
|
|
1338
|
+
`INSERT INTO "${schema}"."Alert"
|
|
1339
|
+
("_id", "projectId", "title", "currentAlertStateId", "alertSeverityId", "isPrivate", "version")
|
|
1340
|
+
VALUES ($1, $2, 'Checkout p95 latency is high', $3, $4, $5, 1)`,
|
|
1341
|
+
[
|
|
1342
|
+
id.toString(),
|
|
1343
|
+
alertProjectId.toString(),
|
|
1344
|
+
states[states.length - 1]!.toString(),
|
|
1345
|
+
ObjectID.generate().toString(),
|
|
1346
|
+
Boolean(options.isPrivate),
|
|
1347
|
+
],
|
|
1348
|
+
);
|
|
1349
|
+
|
|
1350
|
+
const tenMinutes: number = 10 * 60 * 1000;
|
|
1351
|
+
const now: number = Date.now();
|
|
1352
|
+
|
|
1353
|
+
for (let index: number = 0; index < states.length; index++) {
|
|
1354
|
+
const startsAt: Date = new Date(
|
|
1355
|
+
now - (states.length - index) * tenMinutes,
|
|
1356
|
+
);
|
|
1357
|
+
const endsAt: Date | null =
|
|
1358
|
+
index < states.length - 1
|
|
1359
|
+
? new Date(startsAt.getTime() + tenMinutes)
|
|
1360
|
+
: null;
|
|
1361
|
+
|
|
1362
|
+
await database.query(
|
|
1363
|
+
`INSERT INTO "${schema}"."AlertStateTimeline"
|
|
1364
|
+
("_id", "projectId", "alertId", "alertStateId", "startsAt", "endsAt", "isOwnerNotified", "version")
|
|
1365
|
+
VALUES ($1, $2, $3, $4, $5, $6, true, 1)`,
|
|
1366
|
+
[
|
|
1367
|
+
ObjectID.generate().toString(),
|
|
1368
|
+
alertProjectId.toString(),
|
|
1369
|
+
id.toString(),
|
|
1370
|
+
states[index]!.toString(),
|
|
1371
|
+
startsAt,
|
|
1372
|
+
endsAt,
|
|
1373
|
+
],
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
return id;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
async function currentAlertStateOf(alertId: ObjectID): Promise<string> {
|
|
1381
|
+
const rows: Array<{ currentAlertStateId: string }> = await database.query(
|
|
1382
|
+
`SELECT "currentAlertStateId" FROM "${schema}"."Alert" WHERE "_id" = $1`,
|
|
1383
|
+
[alertId.toString()],
|
|
1384
|
+
);
|
|
1385
|
+
return rows[0]!.currentAlertStateId;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
async function timelineOf(alertId: ObjectID): Promise<Array<TimelineRow>> {
|
|
1389
|
+
return database.query(
|
|
1390
|
+
`SELECT "alertStateId", "createdByUserId", "rootCause", "isOwnerNotified", "startsAt", "endsAt"
|
|
1391
|
+
FROM "${schema}"."AlertStateTimeline"
|
|
1392
|
+
WHERE "alertId" = $1
|
|
1393
|
+
ORDER BY "startsAt"`,
|
|
1394
|
+
[alertId.toString()],
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
async function incidentRows(): Promise<Array<IncidentRow>> {
|
|
1399
|
+
return database.query(
|
|
1400
|
+
`SELECT "_id", "incidentNumber", "currentIncidentStateId", "isPrivate"
|
|
1401
|
+
FROM "${schema}"."Incident"
|
|
1402
|
+
ORDER BY "incidentNumber"`,
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
async function incidentCounter(): Promise<number> {
|
|
1407
|
+
const rows: Array<{ incidentCounter: number | string }> =
|
|
1408
|
+
await database.query(
|
|
1409
|
+
`SELECT "incidentCounter" FROM "${schema}"."Project" WHERE "_id" = $1`,
|
|
1410
|
+
[projectId.toString()],
|
|
1411
|
+
);
|
|
1412
|
+
return Number(rows[0]!.incidentCounter);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Polls, because the create does not wait for the acknowledgement.
|
|
1416
|
+
async function waitForAlertState(
|
|
1417
|
+
alertId: ObjectID,
|
|
1418
|
+
stateId: ObjectID,
|
|
1419
|
+
): Promise<void> {
|
|
1420
|
+
const deadline: number = Date.now() + ACKNOWLEDGEMENT_TIMEOUT_MS;
|
|
1421
|
+
let current: string = await currentAlertStateOf(alertId);
|
|
1422
|
+
|
|
1423
|
+
while (current !== stateId.toString()) {
|
|
1424
|
+
if (Date.now() > deadline) {
|
|
1425
|
+
throw new Error(
|
|
1426
|
+
`alert ${alertId.toString()} is in state ${current}, not ${stateId.toString()}, ${ACKNOWLEDGEMENT_TIMEOUT_MS}ms after the incident was declared`,
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
await new Promise<void>((resolve: () => void) => {
|
|
1431
|
+
setTimeout(resolve, 50);
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
current = await currentAlertStateOf(alertId);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// The one acknowledgement the declaration started, once it has finished.
|
|
1439
|
+
async function acknowledgement(): Promise<AcknowledgeDeclaredAlertsResult> {
|
|
1440
|
+
expect(acknowledge).toHaveBeenCalledTimes(1);
|
|
1441
|
+
return (await acknowledge.mock.results[0]!
|
|
1442
|
+
.value) as AcknowledgeDeclaredAlertsResult;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function acknowledgementArguments(): AcknowledgeArguments {
|
|
1446
|
+
return acknowledge.mock.calls[0]![0] as AcknowledgeArguments;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function ids(values: Array<ObjectID>): Array<string> {
|
|
1450
|
+
return values.map((value: ObjectID): string => {
|
|
1451
|
+
return value.toString();
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// The alert feed entries of state changes (links write their own).
|
|
1456
|
+
function stateChangeFeedItems(): Array<AlertFeedItem> {
|
|
1457
|
+
return alertFeed.mock.calls
|
|
1458
|
+
.map((call: Array<unknown>): AlertFeedItem => {
|
|
1459
|
+
return call[0] as AlertFeedItem;
|
|
1460
|
+
})
|
|
1461
|
+
.filter((item: AlertFeedItem): boolean => {
|
|
1462
|
+
return (
|
|
1463
|
+
item.alertFeedEventType === AlertFeedEventType.AlertStateChanged
|
|
1464
|
+
);
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
function newIncident(
|
|
1469
|
+
options: {
|
|
1470
|
+
createdByUserId?: ObjectID;
|
|
1471
|
+
isPrivate?: boolean;
|
|
1472
|
+
currentIncidentStateId?: ObjectID;
|
|
1473
|
+
} = {},
|
|
1474
|
+
): Incident {
|
|
1475
|
+
const incident: Incident = new Incident();
|
|
1476
|
+
incident.projectId = projectId;
|
|
1477
|
+
incident.title = "Checkout is failing";
|
|
1478
|
+
incident.incidentSeverityId = incidentSeverityId;
|
|
1479
|
+
|
|
1480
|
+
if (options.createdByUserId) {
|
|
1481
|
+
incident.createdByUserId = options.createdByUserId;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
if (options.isPrivate) {
|
|
1485
|
+
incident.isPrivate = true;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
if (options.currentIncidentStateId) {
|
|
1489
|
+
incident.currentIncidentStateId = options.currentIncidentStateId;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
return incident;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// miscDataProps of a declaration from these alerts.
|
|
1496
|
+
function declaredFrom(
|
|
1497
|
+
alertIds: Array<ObjectID>,
|
|
1498
|
+
acknowledgeAlerts?: boolean,
|
|
1499
|
+
): JSONObject {
|
|
1500
|
+
const miscDataProps: JSONObject = {
|
|
1501
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: ids(alertIds),
|
|
1502
|
+
};
|
|
1503
|
+
|
|
1504
|
+
if (acknowledgeAlerts !== undefined) {
|
|
1505
|
+
miscDataProps[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY] =
|
|
1506
|
+
acknowledgeAlerts;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
return miscDataProps;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// A user of the test project holding exactly these permissions.
|
|
1513
|
+
function userProps(
|
|
1514
|
+
userId: ObjectID,
|
|
1515
|
+
permissions: Array<Permission>,
|
|
1516
|
+
): DatabaseCommonInteractionProps {
|
|
1517
|
+
const tenantPermission: UserTenantAccessPermission = {
|
|
1518
|
+
projectId: projectId,
|
|
1519
|
+
_type: "UserTenantAccessPermission",
|
|
1520
|
+
permissions: permissions.map(
|
|
1521
|
+
(permission: Permission): UserPermission => {
|
|
1522
|
+
return {
|
|
1523
|
+
_type: "UserPermission",
|
|
1524
|
+
permission: permission,
|
|
1525
|
+
labelIds: [],
|
|
1526
|
+
isBlockPermission: false,
|
|
1527
|
+
};
|
|
1528
|
+
},
|
|
1529
|
+
),
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
return {
|
|
1533
|
+
tenantId: projectId,
|
|
1534
|
+
userId: userId,
|
|
1535
|
+
userType: UserType.User,
|
|
1536
|
+
userTenantAccessPermission: {
|
|
1537
|
+
[projectId.toString()]: tenantPermission,
|
|
1538
|
+
},
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/*
|
|
1543
|
+
* A responder who can read every alert (Viewer) and declare incidents
|
|
1544
|
+
* and link alerts to them (IncidentMember), but may change only the
|
|
1545
|
+
* alerts carrying one label (AlertMember scoped to that label).
|
|
1546
|
+
*/
|
|
1547
|
+
function labelScopedMemberProps(
|
|
1548
|
+
userId: ObjectID,
|
|
1549
|
+
alertLabelId: ObjectID,
|
|
1550
|
+
): DatabaseCommonInteractionProps {
|
|
1551
|
+
const props: DatabaseCommonInteractionProps = userProps(userId, [
|
|
1552
|
+
Permission.Viewer,
|
|
1553
|
+
Permission.IncidentMember,
|
|
1554
|
+
]);
|
|
1555
|
+
|
|
1556
|
+
const tenantPermission: UserTenantAccessPermission =
|
|
1557
|
+
props.userTenantAccessPermission![projectId.toString()]!;
|
|
1558
|
+
|
|
1559
|
+
tenantPermission.permissions.push({
|
|
1560
|
+
_type: "UserPermission",
|
|
1561
|
+
permission: Permission.AlertMember,
|
|
1562
|
+
labelIds: [alertLabelId],
|
|
1563
|
+
isBlockPermission: false,
|
|
1564
|
+
scope: PermissionScope.Labels,
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1567
|
+
return props;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
async function seedLabel(name: string): Promise<ObjectID> {
|
|
1571
|
+
const id: ObjectID = ObjectID.generate();
|
|
1572
|
+
await database.query(
|
|
1573
|
+
`INSERT INTO "${schema}"."Label" ("_id", "projectId", "name", "slug", "color", "version")
|
|
1574
|
+
VALUES ($1, $2, $3, $4, '#2563eb', 1)`,
|
|
1575
|
+
[
|
|
1576
|
+
id.toString(),
|
|
1577
|
+
projectId.toString(),
|
|
1578
|
+
name,
|
|
1579
|
+
`${name.toLowerCase()}-${id.toString()}`,
|
|
1580
|
+
],
|
|
1581
|
+
);
|
|
1582
|
+
return id;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
async function labelAlert(
|
|
1586
|
+
alertId: ObjectID,
|
|
1587
|
+
labelId: ObjectID,
|
|
1588
|
+
): Promise<ObjectID> {
|
|
1589
|
+
await database.query(
|
|
1590
|
+
`INSERT INTO "${schema}"."AlertLabel" ("alertId", "labelId") VALUES ($1, $2)`,
|
|
1591
|
+
[alertId.toString(), labelId.toString()],
|
|
1592
|
+
);
|
|
1593
|
+
return alertId;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
async function validateAcknowledge(
|
|
1597
|
+
alertIds: Array<ObjectID>,
|
|
1598
|
+
props: DatabaseCommonInteractionProps,
|
|
1599
|
+
): Promise<CheckedAcknowledgement | null> {
|
|
1600
|
+
const checked: AlertsToAcknowledgeOnDeclare | null =
|
|
1601
|
+
await IncidentAlertService.validateAcknowledgeAlertsForNewIncident({
|
|
1602
|
+
projectId: projectId,
|
|
1603
|
+
acknowledgeAlerts: true,
|
|
1604
|
+
alertIds: alertIds,
|
|
1605
|
+
props: props,
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
if (!checked) {
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
return {
|
|
1613
|
+
acknowledgedAlertStateId: checked.acknowledgedAlertStateId.toString(),
|
|
1614
|
+
alertIdsToAcknowledge: ids(checked.alertIdsToAcknowledge),
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function timelineStates(timeline: Array<TimelineRow>): Array<string> {
|
|
1619
|
+
return timeline.map((row: TimelineRow): string => {
|
|
1620
|
+
return row.alertStateId;
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
test("root declares with acknowledgeAlertsToLink: once linked, the alert is acknowledged as the declaring user", async () => {
|
|
1625
|
+
const userId: ObjectID = await seedUser();
|
|
1626
|
+
const alertId: ObjectID = await seedAlertThrough([alertStates.created]);
|
|
1627
|
+
|
|
1628
|
+
const incident: Incident = await IncidentService.create({
|
|
1629
|
+
data: newIncident({ createdByUserId: userId }),
|
|
1630
|
+
miscDataProps: declaredFrom([alertId], true),
|
|
1631
|
+
props: { isRoot: true },
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
expect(incident.incidentNumber).toBe(1);
|
|
1635
|
+
expect(incident.incidentNumberWithPrefix).toBe("#1");
|
|
1636
|
+
|
|
1637
|
+
await waitForAlertState(alertId, alertStates.acknowledged);
|
|
1638
|
+
|
|
1639
|
+
const result: AcknowledgeDeclaredAlertsResult = await acknowledgement();
|
|
1640
|
+
|
|
1641
|
+
expect(ids(result.acknowledgedAlertIds)).toEqual([alertId.toString()]);
|
|
1642
|
+
expect(result.alreadyAcknowledgedAlertIds).toEqual([]);
|
|
1643
|
+
expect(result.leftToLinkedAlertSyncAlertIds).toEqual([]);
|
|
1644
|
+
expect(result.failed).toEqual([]);
|
|
1645
|
+
|
|
1646
|
+
const args: AcknowledgeArguments = acknowledgementArguments();
|
|
1647
|
+
expect(args.projectId.toString()).toBe(projectId.toString());
|
|
1648
|
+
expect(args.incidentId.toString()).toBe(incident.id!.toString());
|
|
1649
|
+
expect(ids(args.alertIds)).toEqual([alertId.toString()]);
|
|
1650
|
+
expect(ids(args.linkedAlertIds)).toEqual([alertId.toString()]);
|
|
1651
|
+
expect(String(args.acknowledgedByUserId)).toBe(userId.toString());
|
|
1652
|
+
|
|
1653
|
+
// It started once the link was in the table, which names the user too.
|
|
1654
|
+
expect(linksWhenAcknowledging).toEqual([1]);
|
|
1655
|
+
expect(await linkRows()).toEqual([
|
|
1656
|
+
{ _id: expect.any(String), createdByUserId: userId.toString() },
|
|
1657
|
+
]);
|
|
1658
|
+
|
|
1659
|
+
const timeline: Array<TimelineRow> = await timelineOf(alertId);
|
|
1660
|
+
|
|
1661
|
+
expect(
|
|
1662
|
+
timeline.map((row: TimelineRow): string => {
|
|
1663
|
+
return row.alertStateId;
|
|
1664
|
+
}),
|
|
1665
|
+
).toEqual([
|
|
1666
|
+
alertStates.created.toString(),
|
|
1667
|
+
alertStates.acknowledged.toString(),
|
|
1668
|
+
]);
|
|
1669
|
+
expect(timeline[1]).toMatchObject({
|
|
1670
|
+
createdByUserId: userId.toString(),
|
|
1671
|
+
rootCause:
|
|
1672
|
+
"Acknowledged because Incident #1 was declared from this alert.",
|
|
1673
|
+
// The owners are told, as when Acknowledge is pressed on the alert.
|
|
1674
|
+
isOwnerNotified: false,
|
|
1675
|
+
endsAt: null,
|
|
1676
|
+
});
|
|
1677
|
+
// The Created row now ends where the acknowledgement begins.
|
|
1678
|
+
expect(timeline[0]!.endsAt?.getTime()).toBe(
|
|
1679
|
+
timeline[1]!.startsAt.getTime(),
|
|
1680
|
+
);
|
|
1681
|
+
|
|
1682
|
+
// The alert's feed says who and why.
|
|
1683
|
+
const feedItems: Array<AlertFeedItem> = stateChangeFeedItems();
|
|
1684
|
+
expect(feedItems).toHaveLength(1);
|
|
1685
|
+
expect(feedItems[0]!.alertId.toString()).toBe(alertId.toString());
|
|
1686
|
+
expect(String(feedItems[0]!.userId)).toBe(userId.toString());
|
|
1687
|
+
expect(String(feedItems[0]!.workspaceNotification?.notifyUserId)).toBe(
|
|
1688
|
+
userId.toString(),
|
|
1689
|
+
);
|
|
1690
|
+
expect(feedItems[0]!.moreInformationInMarkdown).toContain(
|
|
1691
|
+
"Acknowledged because Incident #1 was declared from this alert.",
|
|
1692
|
+
);
|
|
1693
|
+
|
|
1694
|
+
// The incident itself is left in the state it was declared in.
|
|
1695
|
+
expect(await incidentRows()).toEqual([
|
|
1696
|
+
{
|
|
1697
|
+
_id: incident.id!.toString(),
|
|
1698
|
+
incidentNumber: 1,
|
|
1699
|
+
currentIncidentStateId: incidentStates.created.toString(),
|
|
1700
|
+
isPrivate: false,
|
|
1701
|
+
},
|
|
1702
|
+
]);
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
test("alerts already acknowledged or resolved are left as they are: only the one still open is handed to the acknowledgement, and acknowledged", async () => {
|
|
1706
|
+
await database.query(
|
|
1707
|
+
`UPDATE "${schema}"."Project" SET "incidentNumberPrefix" = 'INC-' WHERE "_id" = $1`,
|
|
1708
|
+
[projectId.toString()],
|
|
1709
|
+
);
|
|
1710
|
+
|
|
1711
|
+
const userId: ObjectID = await seedUser();
|
|
1712
|
+
const openAlertId: ObjectID = await seedAlertThrough([
|
|
1713
|
+
alertStates.created,
|
|
1714
|
+
]);
|
|
1715
|
+
const acknowledgedAlertId: ObjectID = await seedAlertThrough([
|
|
1716
|
+
alertStates.created,
|
|
1717
|
+
alertStates.acknowledged,
|
|
1718
|
+
]);
|
|
1719
|
+
const resolvedAlertId: ObjectID = await seedAlertThrough([
|
|
1720
|
+
alertStates.created,
|
|
1721
|
+
alertStates.resolved,
|
|
1722
|
+
]);
|
|
1723
|
+
|
|
1724
|
+
const acknowledgedTimeline: Array<TimelineRow> =
|
|
1725
|
+
await timelineOf(acknowledgedAlertId);
|
|
1726
|
+
const resolvedTimeline: Array<TimelineRow> =
|
|
1727
|
+
await timelineOf(resolvedAlertId);
|
|
1728
|
+
|
|
1729
|
+
const incident: Incident = await IncidentService.create({
|
|
1730
|
+
data: newIncident({ createdByUserId: userId }),
|
|
1731
|
+
miscDataProps: declaredFrom(
|
|
1732
|
+
[acknowledgedAlertId, openAlertId, resolvedAlertId],
|
|
1733
|
+
true,
|
|
1734
|
+
),
|
|
1735
|
+
props: { isRoot: true },
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
expect(incident.incidentNumberWithPrefix).toBe("INC-1");
|
|
1739
|
+
|
|
1740
|
+
await waitForAlertState(openAlertId, alertStates.acknowledged);
|
|
1741
|
+
|
|
1742
|
+
const result: AcknowledgeDeclaredAlertsResult = await acknowledgement();
|
|
1743
|
+
|
|
1744
|
+
/*
|
|
1745
|
+
* The two already past Created were settled when the declaration was
|
|
1746
|
+
* checked: only the open alert is handed on to be written, while all
|
|
1747
|
+
* three are passed as linked (the sync decides on those).
|
|
1748
|
+
*/
|
|
1749
|
+
const args: AcknowledgeArguments = acknowledgementArguments();
|
|
1750
|
+
expect(ids(args.alertIds)).toEqual([openAlertId.toString()]);
|
|
1751
|
+
expect(ids(args.linkedAlertIds)).toEqual([
|
|
1752
|
+
acknowledgedAlertId.toString(),
|
|
1753
|
+
openAlertId.toString(),
|
|
1754
|
+
resolvedAlertId.toString(),
|
|
1755
|
+
]);
|
|
1756
|
+
|
|
1757
|
+
expect(ids(result.acknowledgedAlertIds)).toEqual([
|
|
1758
|
+
openAlertId.toString(),
|
|
1759
|
+
]);
|
|
1760
|
+
expect(result.alreadyAcknowledgedAlertIds).toEqual([]);
|
|
1761
|
+
expect(result.leftToLinkedAlertSyncAlertIds).toEqual([]);
|
|
1762
|
+
expect(result.failed).toEqual([]);
|
|
1763
|
+
|
|
1764
|
+
// All three are linked; nothing is moved backwards, or written twice.
|
|
1765
|
+
expect(await linkCount()).toBe(3);
|
|
1766
|
+
expect(await currentAlertStateOf(acknowledgedAlertId)).toBe(
|
|
1767
|
+
alertStates.acknowledged.toString(),
|
|
1768
|
+
);
|
|
1769
|
+
expect(await currentAlertStateOf(resolvedAlertId)).toBe(
|
|
1770
|
+
alertStates.resolved.toString(),
|
|
1771
|
+
);
|
|
1772
|
+
expect(await timelineOf(acknowledgedAlertId)).toEqual(
|
|
1773
|
+
acknowledgedTimeline,
|
|
1774
|
+
);
|
|
1775
|
+
expect(await timelineOf(resolvedAlertId)).toEqual(resolvedTimeline);
|
|
1776
|
+
|
|
1777
|
+
const openTimeline: Array<TimelineRow> = await timelineOf(openAlertId);
|
|
1778
|
+
expect(openTimeline).toHaveLength(2);
|
|
1779
|
+
expect(openTimeline[1]).toMatchObject({
|
|
1780
|
+
alertStateId: alertStates.acknowledged.toString(),
|
|
1781
|
+
createdByUserId: userId.toString(),
|
|
1782
|
+
rootCause:
|
|
1783
|
+
"Acknowledged because Incident INC-1 was declared from this alert.",
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1786
|
+
const feedItems: Array<AlertFeedItem> = stateChangeFeedItems();
|
|
1787
|
+
expect(
|
|
1788
|
+
feedItems.map((item: AlertFeedItem): string => {
|
|
1789
|
+
return item.alertId.toString();
|
|
1790
|
+
}),
|
|
1791
|
+
).toEqual([openAlertId.toString()]);
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
test("without acknowledgeAlertsToLink (absent or false) the alerts are linked and left in their state", async () => {
|
|
1795
|
+
const userId: ObjectID = await seedUser();
|
|
1796
|
+
const alertId: ObjectID = await seedAlertThrough([alertStates.created]);
|
|
1797
|
+
|
|
1798
|
+
const first: Incident = await IncidentService.create({
|
|
1799
|
+
data: newIncident({ createdByUserId: userId }),
|
|
1800
|
+
miscDataProps: declaredFrom([alertId]),
|
|
1801
|
+
props: { isRoot: true },
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
const second: Incident = await IncidentService.create({
|
|
1805
|
+
data: newIncident({ createdByUserId: userId }),
|
|
1806
|
+
miscDataProps: declaredFrom([alertId], false),
|
|
1807
|
+
props: { isRoot: true },
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
expect(first.incidentNumber).toBe(1);
|
|
1811
|
+
expect(second.incidentNumber).toBe(2);
|
|
1812
|
+
|
|
1813
|
+
/*
|
|
1814
|
+
* The acknowledgement is started before create() returns (only its
|
|
1815
|
+
* result is not waited for), so not having started by now is final.
|
|
1816
|
+
*/
|
|
1817
|
+
expect(acknowledge).not.toHaveBeenCalled();
|
|
1818
|
+
expect(await linkCount()).toBe(2);
|
|
1819
|
+
expect(await currentAlertStateOf(alertId)).toBe(
|
|
1820
|
+
alertStates.created.toString(),
|
|
1821
|
+
);
|
|
1822
|
+
expect(await timelineOf(alertId)).toHaveLength(1);
|
|
1823
|
+
expect(stateChangeFeedItems()).toEqual([]);
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
test("a project without an Acknowledged alert state: the declaration is refused before the incident, its number or its links exist", async () => {
|
|
1827
|
+
await database.query(
|
|
1828
|
+
`DELETE FROM "${schema}"."AlertState" WHERE "_id" = $1`,
|
|
1829
|
+
[alertStates.acknowledged.toString()],
|
|
1830
|
+
);
|
|
1831
|
+
|
|
1832
|
+
const userId: ObjectID = await seedUser();
|
|
1833
|
+
const alertId: ObjectID = await seedAlertThrough([alertStates.created]);
|
|
1834
|
+
|
|
1835
|
+
const refusal: unknown = await failureOf(() => {
|
|
1836
|
+
return IncidentService.create({
|
|
1837
|
+
data: newIncident({ createdByUserId: userId }),
|
|
1838
|
+
miscDataProps: declaredFrom([alertId], true),
|
|
1839
|
+
props: { isRoot: true },
|
|
1840
|
+
});
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
expect(refusal).toBeInstanceOf(BadDataException);
|
|
1844
|
+
expect((refusal as Error).message).toBe(NO_ACKNOWLEDGED_STATE_MESSAGE);
|
|
1845
|
+
|
|
1846
|
+
expect(await incidentRows()).toEqual([]);
|
|
1847
|
+
expect(await incidentCounter()).toBe(0);
|
|
1848
|
+
expect(await linkCount()).toBe(0);
|
|
1849
|
+
expect(acknowledge).not.toHaveBeenCalled();
|
|
1850
|
+
expect(await currentAlertStateOf(alertId)).toBe(
|
|
1851
|
+
alertStates.created.toString(),
|
|
1852
|
+
);
|
|
1853
|
+
|
|
1854
|
+
// Declared without asking, it goes through - and takes number 1.
|
|
1855
|
+
const incident: Incident = await IncidentService.create({
|
|
1856
|
+
data: newIncident({ createdByUserId: userId }),
|
|
1857
|
+
miscDataProps: declaredFrom([alertId]),
|
|
1858
|
+
props: { isRoot: true },
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1861
|
+
expect(incident.incidentNumber).toBe(1);
|
|
1862
|
+
expect(await linkCount()).toBe(1);
|
|
1863
|
+
expect(await currentAlertStateOf(alertId)).toBe(
|
|
1864
|
+
alertStates.created.toString(),
|
|
1865
|
+
);
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
test("a private incident is not named in the acknowledgement's cause", async () => {
|
|
1869
|
+
const userId: ObjectID = await seedUser();
|
|
1870
|
+
const alertId: ObjectID = await seedAlertThrough([alertStates.created]);
|
|
1871
|
+
|
|
1872
|
+
const incident: Incident = await IncidentService.create({
|
|
1873
|
+
data: newIncident({ createdByUserId: userId, isPrivate: true }),
|
|
1874
|
+
miscDataProps: declaredFrom([alertId], true),
|
|
1875
|
+
props: { isRoot: true },
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
expect(incident.incidentNumberWithPrefix).toBe("#1");
|
|
1879
|
+
|
|
1880
|
+
await waitForAlertState(alertId, alertStates.acknowledged);
|
|
1881
|
+
|
|
1882
|
+
const result: AcknowledgeDeclaredAlertsResult = await acknowledgement();
|
|
1883
|
+
expect(ids(result.acknowledgedAlertIds)).toEqual([alertId.toString()]);
|
|
1884
|
+
|
|
1885
|
+
expect((await incidentRows())[0]!.isPrivate).toBe(true);
|
|
1886
|
+
|
|
1887
|
+
const timeline: Array<TimelineRow> = await timelineOf(alertId);
|
|
1888
|
+
expect(timeline).toHaveLength(2);
|
|
1889
|
+
expect(timeline[1]).toMatchObject({
|
|
1890
|
+
alertStateId: alertStates.acknowledged.toString(),
|
|
1891
|
+
createdByUserId: userId.toString(),
|
|
1892
|
+
rootCause: PRIVATE_INCIDENT_CAUSE,
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
// The feed entry goes to the alert's channels: no incident number.
|
|
1896
|
+
const feedItems: Array<AlertFeedItem> = stateChangeFeedItems();
|
|
1897
|
+
expect(feedItems).toHaveLength(1);
|
|
1898
|
+
expect(feedItems[0]!.moreInformationInMarkdown).toContain(
|
|
1899
|
+
PRIVATE_INCIDENT_CAUSE,
|
|
1900
|
+
);
|
|
1901
|
+
expect(feedItems[0]!.moreInformationInMarkdown).not.toContain("#1");
|
|
1902
|
+
});
|
|
1903
|
+
|
|
1904
|
+
test("a member who may acknowledge alerts declares, and the acknowledgement is credited to them", async () => {
|
|
1905
|
+
const userId: ObjectID = await seedUser();
|
|
1906
|
+
const alertId: ObjectID = await seedAlertThrough([alertStates.created]);
|
|
1907
|
+
|
|
1908
|
+
const incident: Incident = await IncidentService.create({
|
|
1909
|
+
data: newIncident(),
|
|
1910
|
+
miscDataProps: declaredFrom([alertId], true),
|
|
1911
|
+
props: userProps(userId, [
|
|
1912
|
+
Permission.IncidentMember,
|
|
1913
|
+
Permission.AlertMember,
|
|
1914
|
+
]),
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
expect(incident.incidentNumber).toBe(1);
|
|
1918
|
+
|
|
1919
|
+
await waitForAlertState(alertId, alertStates.acknowledged);
|
|
1920
|
+
|
|
1921
|
+
const result: AcknowledgeDeclaredAlertsResult = await acknowledgement();
|
|
1922
|
+
expect(ids(result.acknowledgedAlertIds)).toEqual([alertId.toString()]);
|
|
1923
|
+
expect(result.failed).toEqual([]);
|
|
1924
|
+
|
|
1925
|
+
expect(String(acknowledgementArguments().acknowledgedByUserId)).toBe(
|
|
1926
|
+
userId.toString(),
|
|
1927
|
+
);
|
|
1928
|
+
|
|
1929
|
+
const timeline: Array<TimelineRow> = await timelineOf(alertId);
|
|
1930
|
+
expect(timeline).toHaveLength(2);
|
|
1931
|
+
expect(timeline[1]).toMatchObject({
|
|
1932
|
+
alertStateId: alertStates.acknowledged.toString(),
|
|
1933
|
+
createdByUserId: userId.toString(),
|
|
1934
|
+
rootCause:
|
|
1935
|
+
"Acknowledged because Incident #1 was declared from this alert.",
|
|
1936
|
+
});
|
|
1937
|
+
expect(await linkRows()).toEqual([
|
|
1938
|
+
{ _id: expect.any(String), createdByUserId: userId.toString() },
|
|
1939
|
+
]);
|
|
1940
|
+
});
|
|
1941
|
+
|
|
1942
|
+
test("a member who may not change alert states is refused before the incident exists", async () => {
|
|
1943
|
+
const userId: ObjectID = await seedUser();
|
|
1944
|
+
const alertId: ObjectID = await seedAlertThrough([alertStates.created]);
|
|
1945
|
+
|
|
1946
|
+
// May declare incidents and link alerts, and only read alerts.
|
|
1947
|
+
const viewer: DatabaseCommonInteractionProps = userProps(userId, [
|
|
1948
|
+
Permission.IncidentMember,
|
|
1949
|
+
Permission.AlertViewer,
|
|
1950
|
+
]);
|
|
1951
|
+
|
|
1952
|
+
const refusal: unknown = await failureOf(() => {
|
|
1953
|
+
return IncidentService.create({
|
|
1954
|
+
data: newIncident(),
|
|
1955
|
+
miscDataProps: declaredFrom([alertId], true),
|
|
1956
|
+
props: viewer,
|
|
1957
|
+
});
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
expect(refusal).toBeInstanceOf(BadDataException);
|
|
1961
|
+
expect((refusal as Error).message).toBe(
|
|
1962
|
+
NO_PERMISSION_TO_ACKNOWLEDGE_MESSAGE,
|
|
1963
|
+
);
|
|
1964
|
+
|
|
1965
|
+
expect(await incidentRows()).toEqual([]);
|
|
1966
|
+
expect(await incidentCounter()).toBe(0);
|
|
1967
|
+
expect(await linkCount()).toBe(0);
|
|
1968
|
+
expect(acknowledge).not.toHaveBeenCalled();
|
|
1969
|
+
expect(await currentAlertStateOf(alertId)).toBe(
|
|
1970
|
+
alertStates.created.toString(),
|
|
1971
|
+
);
|
|
1972
|
+
expect(await timelineOf(alertId)).toHaveLength(1);
|
|
1973
|
+
|
|
1974
|
+
// The same declaration without asking to acknowledge is theirs to make.
|
|
1975
|
+
const incident: Incident = await IncidentService.create({
|
|
1976
|
+
data: newIncident(),
|
|
1977
|
+
miscDataProps: declaredFrom([alertId]),
|
|
1978
|
+
props: viewer,
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
expect(incident.incidentNumber).toBe(1);
|
|
1982
|
+
expect(await linkRows()).toEqual([
|
|
1983
|
+
{ _id: expect.any(String), createdByUserId: userId.toString() },
|
|
1984
|
+
]);
|
|
1985
|
+
expect(await currentAlertStateOf(alertId)).toBe(
|
|
1986
|
+
alertStates.created.toString(),
|
|
1987
|
+
);
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
test("the permission check runs on the real tables: a member's update scope leaves out a private alert they do not own, and another project's alert is never one to acknowledge", async () => {
|
|
1991
|
+
const userId: ObjectID = await seedUser();
|
|
1992
|
+
const alertId: ObjectID = await seedAlertThrough([alertStates.created]);
|
|
1993
|
+
const privateAlertId: ObjectID = await seedAlertThrough(
|
|
1994
|
+
[alertStates.created],
|
|
1995
|
+
{ isPrivate: true },
|
|
1996
|
+
);
|
|
1997
|
+
const foreignAlertId: ObjectID = await seedAlertThrough(
|
|
1998
|
+
[alertStates.created],
|
|
1999
|
+
{ project: otherProjectId },
|
|
2000
|
+
);
|
|
2001
|
+
|
|
2002
|
+
const member: DatabaseCommonInteractionProps = userProps(userId, [
|
|
2003
|
+
Permission.IncidentMember,
|
|
2004
|
+
Permission.AlertMember,
|
|
2005
|
+
]);
|
|
2006
|
+
|
|
2007
|
+
expect(await validateAcknowledge([alertId], member)).toEqual({
|
|
2008
|
+
acknowledgedAlertStateId: alertStates.acknowledged.toString(),
|
|
2009
|
+
alertIdsToAcknowledge: [alertId.toString()],
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2012
|
+
await expect(
|
|
2013
|
+
validateAcknowledge([alertId, privateAlertId], member),
|
|
2014
|
+
).rejects.toThrow(NO_PERMISSION_TO_ACKNOWLEDGE_MESSAGE);
|
|
2015
|
+
|
|
2016
|
+
/*
|
|
2017
|
+
* Read for this project only, another project's alert is not found, so
|
|
2018
|
+
* it is not one of the alerts to acknowledge: it can never be written.
|
|
2019
|
+
*/
|
|
2020
|
+
expect(
|
|
2021
|
+
await validateAcknowledge([alertId, foreignAlertId], member),
|
|
2022
|
+
).toEqual({
|
|
2023
|
+
acknowledgedAlertStateId: alertStates.acknowledged.toString(),
|
|
2024
|
+
alertIdsToAcknowledge: [alertId.toString()],
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
// Declaring from it is refused earlier, while the ids are checked.
|
|
2028
|
+
const refusal: unknown = await failureOf(() => {
|
|
2029
|
+
return IncidentService.create({
|
|
2030
|
+
data: newIncident(),
|
|
2031
|
+
miscDataProps: declaredFrom([alertId, foreignAlertId], true),
|
|
2032
|
+
props: member,
|
|
2033
|
+
});
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
expect(refusal).toBeInstanceOf(BadDataException);
|
|
2037
|
+
expect((refusal as Error).message).toBe(NOT_VISIBLE_ALERTS_MESSAGE);
|
|
2038
|
+
expect(await incidentRows()).toEqual([]);
|
|
2039
|
+
expect(await incidentCounter()).toBe(0);
|
|
2040
|
+
expect(await linkCount()).toBe(0);
|
|
2041
|
+
expect(acknowledge).not.toHaveBeenCalled();
|
|
2042
|
+
|
|
2043
|
+
// Root is not asked: the ids were validated before this, as root.
|
|
2044
|
+
expect(
|
|
2045
|
+
await validateAcknowledge([alertId, privateAlertId], { isRoot: true }),
|
|
2046
|
+
).toEqual({
|
|
2047
|
+
acknowledgedAlertStateId: alertStates.acknowledged.toString(),
|
|
2048
|
+
alertIdsToAcknowledge: [alertId.toString(), privateAlertId.toString()],
|
|
2049
|
+
});
|
|
2050
|
+
|
|
2051
|
+
// Checking writes nothing.
|
|
2052
|
+
expect(await timelineOf(privateAlertId)).toHaveLength(1);
|
|
2053
|
+
expect(await timelineOf(foreignAlertId)).toHaveLength(1);
|
|
2054
|
+
expect(await currentAlertStateOf(alertId)).toBe(
|
|
2055
|
+
alertStates.created.toString(),
|
|
2056
|
+
);
|
|
2057
|
+
});
|
|
2058
|
+
|
|
2059
|
+
test("an incident declared straight into Acknowledged with the project's switch on: the linked-alert sync acknowledges the alert, and the declaration leaves it to the sync", async () => {
|
|
2060
|
+
await database.query(
|
|
2061
|
+
`UPDATE "${schema}"."Project" SET "acknowledgeLinkedAlertsWhenIncidentAcknowledged" = true WHERE "_id" = $1`,
|
|
2062
|
+
[projectId.toString()],
|
|
2063
|
+
);
|
|
2064
|
+
|
|
2065
|
+
const userId: ObjectID = await seedUser();
|
|
2066
|
+
const alertId: ObjectID = await seedAlertThrough([alertStates.created]);
|
|
2067
|
+
|
|
2068
|
+
/*
|
|
2069
|
+
* The suite stubs the sync; this test runs the real one, held until
|
|
2070
|
+
* the declaration's own acknowledgement has decided, so the order the
|
|
2071
|
+
* two would race in is fixed: the sync always moves the alert last.
|
|
2072
|
+
*/
|
|
2073
|
+
const sync: jest.SpyInstance = jest.spyOn(
|
|
2074
|
+
IncidentAlertService,
|
|
2075
|
+
"syncAlertWithLinkedIncidentState",
|
|
2076
|
+
);
|
|
2077
|
+
const syncForReal: (data: SyncArguments) => Promise<void> =
|
|
2078
|
+
Object.getPrototypeOf(
|
|
2079
|
+
IncidentAlertService,
|
|
2080
|
+
).syncAlertWithLinkedIncidentState.bind(IncidentAlertService);
|
|
2081
|
+
|
|
2082
|
+
// Assigned by the executor, which runs straight away.
|
|
2083
|
+
let releaseSync: () => void = (): void => {
|
|
2084
|
+
// Replaced below.
|
|
2085
|
+
};
|
|
2086
|
+
const syncReleased: Promise<void> = new Promise<void>(
|
|
2087
|
+
(resolve: () => void) => {
|
|
2088
|
+
releaseSync = resolve;
|
|
2089
|
+
},
|
|
2090
|
+
);
|
|
2091
|
+
|
|
2092
|
+
sync.mockClear();
|
|
2093
|
+
sync.mockImplementation(async (data: SyncArguments): Promise<void> => {
|
|
2094
|
+
await syncReleased;
|
|
2095
|
+
return syncForReal(data);
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
try {
|
|
2099
|
+
const incident: Incident = await IncidentService.create({
|
|
2100
|
+
data: newIncident({
|
|
2101
|
+
createdByUserId: userId,
|
|
2102
|
+
currentIncidentStateId: incidentStates.acknowledged,
|
|
2103
|
+
}),
|
|
2104
|
+
miscDataProps: declaredFrom([alertId], true),
|
|
2105
|
+
props: { isRoot: true },
|
|
2106
|
+
});
|
|
2107
|
+
|
|
2108
|
+
expect(incident.currentIncidentStateId?.toString()).toBe(
|
|
2109
|
+
incidentStates.acknowledged.toString(),
|
|
2110
|
+
);
|
|
2111
|
+
|
|
2112
|
+
const result: AcknowledgeDeclaredAlertsResult = await acknowledgement();
|
|
2113
|
+
|
|
2114
|
+
expect(ids(result.leftToLinkedAlertSyncAlertIds)).toEqual([
|
|
2115
|
+
alertId.toString(),
|
|
2116
|
+
]);
|
|
2117
|
+
expect(result.acknowledgedAlertIds).toEqual([]);
|
|
2118
|
+
expect(result.alreadyAcknowledgedAlertIds).toEqual([]);
|
|
2119
|
+
expect(result.failed).toEqual([]);
|
|
2120
|
+
|
|
2121
|
+
// The declaration wrote nothing on the alert.
|
|
2122
|
+
expect(await timelineOf(alertId)).toHaveLength(1);
|
|
2123
|
+
expect(await currentAlertStateOf(alertId)).toBe(
|
|
2124
|
+
alertStates.created.toString(),
|
|
2125
|
+
);
|
|
2126
|
+
|
|
2127
|
+
expect(sync).toHaveBeenCalledTimes(1);
|
|
2128
|
+
releaseSync();
|
|
2129
|
+
await sync.mock.results[0]!.value;
|
|
2130
|
+
|
|
2131
|
+
// The sync moved it, once.
|
|
2132
|
+
expect(await currentAlertStateOf(alertId)).toBe(
|
|
2133
|
+
alertStates.acknowledged.toString(),
|
|
2134
|
+
);
|
|
2135
|
+
|
|
2136
|
+
const timeline: Array<TimelineRow> = await timelineOf(alertId);
|
|
2137
|
+
expect(
|
|
2138
|
+
timeline.map((row: TimelineRow): string => {
|
|
2139
|
+
return row.alertStateId;
|
|
2140
|
+
}),
|
|
2141
|
+
).toEqual([
|
|
2142
|
+
alertStates.created.toString(),
|
|
2143
|
+
alertStates.acknowledged.toString(),
|
|
2144
|
+
]);
|
|
2145
|
+
expect(timeline[1]!.rootCause).toBe(
|
|
2146
|
+
"Acknowledged because linked Incident #1 was acknowledged.",
|
|
2147
|
+
);
|
|
2148
|
+
expect(stateChangeFeedItems()).toHaveLength(1);
|
|
2149
|
+
} finally {
|
|
2150
|
+
releaseSync();
|
|
2151
|
+
sync.mockResolvedValue(undefined);
|
|
2152
|
+
}
|
|
2153
|
+
});
|
|
2154
|
+
|
|
2155
|
+
/*
|
|
2156
|
+
* A responder may change only some alerts: a Viewer who is also an
|
|
2157
|
+
* AlertMember for one label. Only the alerts that will be written are
|
|
2158
|
+
* theirs to be allowed to change - one already resolved is left alone,
|
|
2159
|
+
* whatever its label.
|
|
2160
|
+
*/
|
|
2161
|
+
describe("a member who may change only one label's alerts", () => {
|
|
2162
|
+
const DECLARED_CAUSE: string =
|
|
2163
|
+
"Acknowledged because Incident #1 was declared from this alert.";
|
|
2164
|
+
|
|
2165
|
+
let userId: ObjectID;
|
|
2166
|
+
let labelA: ObjectID;
|
|
2167
|
+
let labelB: ObjectID;
|
|
2168
|
+
let member: DatabaseCommonInteractionProps;
|
|
2169
|
+
|
|
2170
|
+
beforeEach(async () => {
|
|
2171
|
+
userId = await seedUser();
|
|
2172
|
+
labelA = await seedLabel("Payments");
|
|
2173
|
+
labelB = await seedLabel("Search");
|
|
2174
|
+
member = labelScopedMemberProps(userId, labelA);
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
async function timelineRowCount(): Promise<number> {
|
|
2178
|
+
const rows: Array<{ count: string }> = await database.query(
|
|
2179
|
+
`SELECT count(*)::text AS "count" FROM "${schema}"."AlertStateTimeline"`,
|
|
2180
|
+
);
|
|
2181
|
+
return Number(rows[0]!.count);
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// The member's own right to change these alerts' states, on its own.
|
|
2185
|
+
function checkMayChange(alertIds: Array<ObjectID>): Promise<void> {
|
|
2186
|
+
return AlertStateChangeAuthorization.assertCanChangeStateOfAlerts({
|
|
2187
|
+
projectId: projectId,
|
|
2188
|
+
alertIds: alertIds,
|
|
2189
|
+
props: member,
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
test("declaring from A1 (label A, Created) and B1 (label B, Resolved) with acknowledgeAlertsToLink goes through, and only A1 is acknowledged", async () => {
|
|
2194
|
+
const alertA1: ObjectID = await labelAlert(
|
|
2195
|
+
await seedAlertThrough([alertStates.created]),
|
|
2196
|
+
labelA,
|
|
2197
|
+
);
|
|
2198
|
+
const alertB1: ObjectID = await labelAlert(
|
|
2199
|
+
await seedAlertThrough([alertStates.created, alertStates.resolved]),
|
|
2200
|
+
labelB,
|
|
2201
|
+
);
|
|
2202
|
+
|
|
2203
|
+
const resolvedTimeline: Array<TimelineRow> = await timelineOf(alertB1);
|
|
2204
|
+
|
|
2205
|
+
// The member may change A1, and not B1.
|
|
2206
|
+
await expect(checkMayChange([alertA1])).resolves.toBeUndefined();
|
|
2207
|
+
await expect(checkMayChange([alertB1])).rejects.toThrow(
|
|
2208
|
+
NotAuthorizedException,
|
|
2209
|
+
);
|
|
2210
|
+
|
|
2211
|
+
// B1 is not one to acknowledge, so it is not checked, nor written.
|
|
2212
|
+
expect(await validateAcknowledge([alertA1, alertB1], member)).toEqual({
|
|
2213
|
+
acknowledgedAlertStateId: alertStates.acknowledged.toString(),
|
|
2214
|
+
alertIdsToAcknowledge: [alertA1.toString()],
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
const incident: Incident = await IncidentService.create({
|
|
2218
|
+
data: newIncident(),
|
|
2219
|
+
miscDataProps: declaredFrom([alertA1, alertB1], true),
|
|
2220
|
+
props: member,
|
|
2221
|
+
});
|
|
2222
|
+
|
|
2223
|
+
expect(incident.incidentNumber).toBe(1);
|
|
2224
|
+
|
|
2225
|
+
await waitForAlertState(alertA1, alertStates.acknowledged);
|
|
2226
|
+
|
|
2227
|
+
const result: AcknowledgeDeclaredAlertsResult = await acknowledgement();
|
|
2228
|
+
|
|
2229
|
+
expect(ids(result.acknowledgedAlertIds)).toEqual([alertA1.toString()]);
|
|
2230
|
+
expect(result.alreadyAcknowledgedAlertIds).toEqual([]);
|
|
2231
|
+
expect(result.leftToLinkedAlertSyncAlertIds).toEqual([]);
|
|
2232
|
+
expect(result.failed).toEqual([]);
|
|
2233
|
+
|
|
2234
|
+
const args: AcknowledgeArguments = acknowledgementArguments();
|
|
2235
|
+
expect(ids(args.alertIds)).toEqual([alertA1.toString()]);
|
|
2236
|
+
expect(ids(args.linkedAlertIds)).toEqual([
|
|
2237
|
+
alertA1.toString(),
|
|
2238
|
+
alertB1.toString(),
|
|
2239
|
+
]);
|
|
2240
|
+
expect(String(args.acknowledgedByUserId)).toBe(userId.toString());
|
|
2241
|
+
|
|
2242
|
+
// Both are linked, by the member.
|
|
2243
|
+
expect(await linkRows()).toEqual([
|
|
2244
|
+
{ _id: expect.any(String), createdByUserId: userId.toString() },
|
|
2245
|
+
{ _id: expect.any(String), createdByUserId: userId.toString() },
|
|
2246
|
+
]);
|
|
2247
|
+
|
|
2248
|
+
const timeline: Array<TimelineRow> = await timelineOf(alertA1);
|
|
2249
|
+
expect(timelineStates(timeline)).toEqual([
|
|
2250
|
+
alertStates.created.toString(),
|
|
2251
|
+
alertStates.acknowledged.toString(),
|
|
2252
|
+
]);
|
|
2253
|
+
expect(timeline[1]).toMatchObject({
|
|
2254
|
+
createdByUserId: userId.toString(),
|
|
2255
|
+
rootCause: DECLARED_CAUSE,
|
|
2256
|
+
endsAt: null,
|
|
2257
|
+
});
|
|
2258
|
+
|
|
2259
|
+
// B1 is untouched.
|
|
2260
|
+
expect(await currentAlertStateOf(alertB1)).toBe(
|
|
2261
|
+
alertStates.resolved.toString(),
|
|
2262
|
+
);
|
|
2263
|
+
expect(await timelineOf(alertB1)).toEqual(resolvedTimeline);
|
|
2264
|
+
|
|
2265
|
+
expect(
|
|
2266
|
+
stateChangeFeedItems().map((item: AlertFeedItem): string => {
|
|
2267
|
+
return item.alertId.toString();
|
|
2268
|
+
}),
|
|
2269
|
+
).toEqual([alertA1.toString()]);
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2272
|
+
test("declaring from A1 and B2 (label B, Created) with acknowledgeAlertsToLink is refused before the incident, its number or its links exist", async () => {
|
|
2273
|
+
const alertA1: ObjectID = await labelAlert(
|
|
2274
|
+
await seedAlertThrough([alertStates.created]),
|
|
2275
|
+
labelA,
|
|
2276
|
+
);
|
|
2277
|
+
const alertB2: ObjectID = await labelAlert(
|
|
2278
|
+
await seedAlertThrough([alertStates.created]),
|
|
2279
|
+
labelB,
|
|
2280
|
+
);
|
|
2281
|
+
|
|
2282
|
+
const refusal: unknown = await failureOf(() => {
|
|
2283
|
+
return IncidentService.create({
|
|
2284
|
+
data: newIncident(),
|
|
2285
|
+
miscDataProps: declaredFrom([alertA1, alertB2], true),
|
|
2286
|
+
props: member,
|
|
2287
|
+
});
|
|
2288
|
+
});
|
|
2289
|
+
|
|
2290
|
+
expect(refusal).toBeInstanceOf(BadDataException);
|
|
2291
|
+
expect((refusal as Error).message).toBe(
|
|
2292
|
+
NO_PERMISSION_TO_ACKNOWLEDGE_MESSAGE,
|
|
2293
|
+
);
|
|
2294
|
+
|
|
2295
|
+
expect(await incidentRows()).toEqual([]);
|
|
2296
|
+
expect(await incidentCounter()).toBe(0);
|
|
2297
|
+
expect(await linkCount()).toBe(0);
|
|
2298
|
+
expect(acknowledge).not.toHaveBeenCalled();
|
|
2299
|
+
|
|
2300
|
+
for (const alertId of [alertA1, alertB2]) {
|
|
2301
|
+
expect(await currentAlertStateOf(alertId)).toBe(
|
|
2302
|
+
alertStates.created.toString(),
|
|
2303
|
+
);
|
|
2304
|
+
expect(await timelineOf(alertId)).toHaveLength(1);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
expect(stateChangeFeedItems()).toEqual([]);
|
|
2308
|
+
|
|
2309
|
+
/*
|
|
2310
|
+
* Not a refusal to link: declared without asking to acknowledge, the
|
|
2311
|
+
* member may declare from both - and it takes number 1.
|
|
2312
|
+
*/
|
|
2313
|
+
const incident: Incident = await IncidentService.create({
|
|
2314
|
+
data: newIncident(),
|
|
2315
|
+
miscDataProps: declaredFrom([alertA1, alertB2]),
|
|
2316
|
+
props: member,
|
|
2317
|
+
});
|
|
2318
|
+
|
|
2319
|
+
expect(incident.incidentNumber).toBe(1);
|
|
2320
|
+
expect(await linkCount()).toBe(2);
|
|
2321
|
+
expect(acknowledge).not.toHaveBeenCalled();
|
|
2322
|
+
expect(await currentAlertStateOf(alertA1)).toBe(
|
|
2323
|
+
alertStates.created.toString(),
|
|
2324
|
+
);
|
|
2325
|
+
expect(await currentAlertStateOf(alertB2)).toBe(
|
|
2326
|
+
alertStates.created.toString(),
|
|
2327
|
+
);
|
|
2328
|
+
});
|
|
2329
|
+
|
|
2330
|
+
test("every alert already acknowledged or resolved: the declaration goes through with nothing to acknowledge, even outside the member's labels, and writes no timeline row", async () => {
|
|
2331
|
+
const acknowledgedA: ObjectID = await labelAlert(
|
|
2332
|
+
await seedAlertThrough([
|
|
2333
|
+
alertStates.created,
|
|
2334
|
+
alertStates.acknowledged,
|
|
2335
|
+
]),
|
|
2336
|
+
labelA,
|
|
2337
|
+
);
|
|
2338
|
+
const acknowledgedB: ObjectID = await labelAlert(
|
|
2339
|
+
await seedAlertThrough([
|
|
2340
|
+
alertStates.created,
|
|
2341
|
+
alertStates.acknowledged,
|
|
2342
|
+
]),
|
|
2343
|
+
labelB,
|
|
2344
|
+
);
|
|
2345
|
+
const resolvedB: ObjectID = await labelAlert(
|
|
2346
|
+
await seedAlertThrough([alertStates.created, alertStates.resolved]),
|
|
2347
|
+
labelB,
|
|
2348
|
+
);
|
|
2349
|
+
|
|
2350
|
+
const declaredAlertIds: Array<ObjectID> = [
|
|
2351
|
+
acknowledgedA,
|
|
2352
|
+
acknowledgedB,
|
|
2353
|
+
resolvedB,
|
|
2354
|
+
];
|
|
2355
|
+
|
|
2356
|
+
const timelinesBefore: Array<Array<TimelineRow>> = [];
|
|
2357
|
+
for (const alertId of declaredAlertIds) {
|
|
2358
|
+
timelinesBefore.push(await timelineOf(alertId));
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
expect(await timelineRowCount()).toBe(6);
|
|
2362
|
+
|
|
2363
|
+
// The member may not change the label-B alerts.
|
|
2364
|
+
await expect(checkMayChange([acknowledgedA])).resolves.toBeUndefined();
|
|
2365
|
+
await expect(checkMayChange([acknowledgedB])).rejects.toThrow(
|
|
2366
|
+
NotAuthorizedException,
|
|
2367
|
+
);
|
|
2368
|
+
await expect(checkMayChange([resolvedB])).rejects.toThrow(
|
|
2369
|
+
NotAuthorizedException,
|
|
2370
|
+
);
|
|
2371
|
+
|
|
2372
|
+
// Nothing to acknowledge, so nothing the member must be allowed.
|
|
2373
|
+
expect(await validateAcknowledge(declaredAlertIds, member)).toEqual({
|
|
2374
|
+
acknowledgedAlertStateId: alertStates.acknowledged.toString(),
|
|
2375
|
+
alertIdsToAcknowledge: [],
|
|
2376
|
+
});
|
|
2377
|
+
|
|
2378
|
+
const incident: Incident = await IncidentService.create({
|
|
2379
|
+
data: newIncident(),
|
|
2380
|
+
miscDataProps: declaredFrom(declaredAlertIds, true),
|
|
2381
|
+
props: member,
|
|
2382
|
+
});
|
|
2383
|
+
|
|
2384
|
+
expect(incident.incidentNumber).toBe(1);
|
|
2385
|
+
expect(await linkCount()).toBe(3);
|
|
2386
|
+
|
|
2387
|
+
/*
|
|
2388
|
+
* The acknowledgement is started before create() returns when there
|
|
2389
|
+
* is anything to acknowledge, so not having started by now is final.
|
|
2390
|
+
*/
|
|
2391
|
+
expect(acknowledge).not.toHaveBeenCalled();
|
|
2392
|
+
|
|
2393
|
+
expect(await timelineRowCount()).toBe(6);
|
|
2394
|
+
|
|
2395
|
+
for (let index: number = 0; index < declaredAlertIds.length; index++) {
|
|
2396
|
+
expect(await timelineOf(declaredAlertIds[index]!)).toEqual(
|
|
2397
|
+
timelinesBefore[index],
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
expect(await currentAlertStateOf(acknowledgedA)).toBe(
|
|
2402
|
+
alertStates.acknowledged.toString(),
|
|
2403
|
+
);
|
|
2404
|
+
expect(await currentAlertStateOf(acknowledgedB)).toBe(
|
|
2405
|
+
alertStates.acknowledged.toString(),
|
|
2406
|
+
);
|
|
2407
|
+
expect(await currentAlertStateOf(resolvedB)).toBe(
|
|
2408
|
+
alertStates.resolved.toString(),
|
|
2409
|
+
);
|
|
2410
|
+
expect(stateChangeFeedItems()).toEqual([]);
|
|
2411
|
+
});
|
|
2412
|
+
});
|
|
2413
|
+
|
|
2414
|
+
test("seven alerts declared at once are all acknowledged, five written at a time, each with one new timeline row", async () => {
|
|
2415
|
+
const userId: ObjectID = await seedUser();
|
|
2416
|
+
|
|
2417
|
+
const alertIds: Array<ObjectID> = [];
|
|
2418
|
+
for (let index: number = 0; index < 7; index++) {
|
|
2419
|
+
alertIds.push(await seedAlertThrough([alertStates.created]));
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
type ChangeAlertStateArguments = Parameters<
|
|
2423
|
+
typeof AlertService.changeAlertState
|
|
2424
|
+
>[0];
|
|
2425
|
+
|
|
2426
|
+
// The real state change, recording when each write starts and ends.
|
|
2427
|
+
const changeAlertStateForReal: (
|
|
2428
|
+
data: ChangeAlertStateArguments,
|
|
2429
|
+
) => Promise<void> = AlertService.changeAlertState.bind(AlertService);
|
|
2430
|
+
|
|
2431
|
+
const events: Array<string> = [];
|
|
2432
|
+
let inFlight: number = 0;
|
|
2433
|
+
let mostInFlight: number = 0;
|
|
2434
|
+
|
|
2435
|
+
const changeAlertState: jest.SpyInstance = jest
|
|
2436
|
+
.spyOn(AlertService, "changeAlertState")
|
|
2437
|
+
.mockImplementation(
|
|
2438
|
+
async (data: ChangeAlertStateArguments): Promise<void> => {
|
|
2439
|
+
events.push(`start ${data.alertId.toString()}`);
|
|
2440
|
+
inFlight++;
|
|
2441
|
+
mostInFlight = Math.max(mostInFlight, inFlight);
|
|
2442
|
+
|
|
2443
|
+
try {
|
|
2444
|
+
return await changeAlertStateForReal(data);
|
|
2445
|
+
} finally {
|
|
2446
|
+
inFlight--;
|
|
2447
|
+
events.push(`end ${data.alertId.toString()}`);
|
|
2448
|
+
}
|
|
2449
|
+
},
|
|
2450
|
+
);
|
|
2451
|
+
|
|
2452
|
+
try {
|
|
2453
|
+
const incident: Incident = await IncidentService.create({
|
|
2454
|
+
data: newIncident({ createdByUserId: userId }),
|
|
2455
|
+
miscDataProps: declaredFrom(alertIds, true),
|
|
2456
|
+
props: { isRoot: true },
|
|
2457
|
+
});
|
|
2458
|
+
|
|
2459
|
+
expect(incident.incidentNumber).toBe(1);
|
|
2460
|
+
|
|
2461
|
+
const result: AcknowledgeDeclaredAlertsResult = await acknowledgement();
|
|
2462
|
+
|
|
2463
|
+
// All seven, in the order they were declared from.
|
|
2464
|
+
expect(ids(result.acknowledgedAlertIds)).toEqual(ids(alertIds));
|
|
2465
|
+
expect(result.alreadyAcknowledgedAlertIds).toEqual([]);
|
|
2466
|
+
expect(result.leftToLinkedAlertSyncAlertIds).toEqual([]);
|
|
2467
|
+
expect(result.failed).toEqual([]);
|
|
2468
|
+
expect(ids(acknowledgementArguments().alertIds)).toEqual(ids(alertIds));
|
|
2469
|
+
|
|
2470
|
+
// Five written together, the last two once those five had finished.
|
|
2471
|
+
const firstBatch: Array<string> = ids(alertIds.slice(0, 5));
|
|
2472
|
+
const secondBatch: Array<string> = ids(alertIds.slice(5));
|
|
2473
|
+
|
|
2474
|
+
function eventsFor(kind: string, batch: Array<string>): Array<string> {
|
|
2475
|
+
return batch.map((alertId: string): string => {
|
|
2476
|
+
return `${kind} ${alertId}`;
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
expect(changeAlertState).toHaveBeenCalledTimes(7);
|
|
2481
|
+
expect(mostInFlight).toBe(5);
|
|
2482
|
+
expect(events).toHaveLength(14);
|
|
2483
|
+
expect(events.slice(0, 5)).toEqual(eventsFor("start", firstBatch));
|
|
2484
|
+
expect([...events.slice(5, 10)].sort()).toEqual(
|
|
2485
|
+
eventsFor("end", firstBatch).sort(),
|
|
2486
|
+
);
|
|
2487
|
+
expect(events.slice(10, 12)).toEqual(eventsFor("start", secondBatch));
|
|
2488
|
+
expect([...events.slice(12)].sort()).toEqual(
|
|
2489
|
+
eventsFor("end", secondBatch).sort(),
|
|
2490
|
+
);
|
|
2491
|
+
|
|
2492
|
+
for (const alertId of alertIds) {
|
|
2493
|
+
expect(await currentAlertStateOf(alertId)).toBe(
|
|
2494
|
+
alertStates.acknowledged.toString(),
|
|
2495
|
+
);
|
|
2496
|
+
|
|
2497
|
+
const timeline: Array<TimelineRow> = await timelineOf(alertId);
|
|
2498
|
+
expect(timelineStates(timeline)).toEqual([
|
|
2499
|
+
alertStates.created.toString(),
|
|
2500
|
+
alertStates.acknowledged.toString(),
|
|
2501
|
+
]);
|
|
2502
|
+
expect(timeline[1]).toMatchObject({
|
|
2503
|
+
createdByUserId: userId.toString(),
|
|
2504
|
+
rootCause:
|
|
2505
|
+
"Acknowledged because Incident #1 was declared from this alert.",
|
|
2506
|
+
endsAt: null,
|
|
2507
|
+
});
|
|
2508
|
+
expect(timeline[0]!.endsAt?.getTime()).toBe(
|
|
2509
|
+
timeline[1]!.startsAt.getTime(),
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// One feed entry per alert, and every alert linked.
|
|
2514
|
+
expect(
|
|
2515
|
+
stateChangeFeedItems()
|
|
2516
|
+
.map((item: AlertFeedItem): string => {
|
|
2517
|
+
return item.alertId.toString();
|
|
2518
|
+
})
|
|
2519
|
+
.sort(),
|
|
2520
|
+
).toEqual([...ids(alertIds)].sort());
|
|
2521
|
+
expect(await linkCount()).toBe(7);
|
|
2522
|
+
} finally {
|
|
2523
|
+
changeAlertState.mockRestore();
|
|
2524
|
+
}
|
|
2525
|
+
});
|
|
2526
|
+
});
|
|
903
2527
|
});
|