@oneuptime/common 14.0.1 → 14.0.2
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/AlertEpisodeMember.ts +8 -1
- package/Models/DatabaseModels/AutoRemediationSuggestion.ts +63 -0
- package/Models/DatabaseModels/IncidentEpisodeMember.ts +8 -1
- package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +9 -2
- package/Models/DatabaseModels/IncidentPublicNote.ts +9 -2
- package/Models/DatabaseModels/KubernetesCluster.ts +429 -0
- package/Models/DatabaseModels/Monitor.ts +48 -0
- package/Models/DatabaseModels/Runner.ts +5 -4
- package/Models/DatabaseModels/RunnerJob.ts +66 -0
- package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +10 -2
- package/Models/DatabaseModels/WorkflowVariable.ts +479 -2
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +79 -25
- package/Server/API/AIInvestigationAPI.ts +341 -0
- package/Server/API/AutoRemediationAPI.ts +184 -2
- package/Server/API/CommonAPI.ts +70 -0
- package/Server/API/KubernetesClusterAiAccessAPI.ts +793 -0
- package/Server/API/MicrosoftTeamsAPI.ts +770 -315
- package/Server/API/MonitorAPI.ts +140 -0
- package/Server/API/OnCallScheduleTimelineAPI.ts +5 -21
- package/Server/API/SlackAPI.ts +447 -99
- package/Server/API/StatusPageAPI.ts +146 -26
- package/Server/API/TelemetryAPI.ts +216 -5
- package/Server/API/UserTwoFactorBackupCodeAPI.ts +1 -0
- package/Server/API/WorkflowVariableAPI.ts +156 -0
- package/Server/Infrastructure/GlobalCache.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.ts +97 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.ts +129 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.ts +20 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.ts +38 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
- package/Server/Middleware/MasterAdminAuthorization.ts +28 -0
- package/Server/Middleware/TelemetryIngest.ts +47 -2
- package/Server/Middleware/UserAuthorization.ts +56 -2
- package/Server/Services/AIAgentService.ts +1 -0
- package/Server/Services/AccessTokenService.ts +121 -48
- package/Server/Services/AlertEpisodeMemberService.ts +9 -0
- package/Server/Services/AlertEpisodeService.ts +14 -1
- package/Server/Services/AlertService.ts +134 -22
- package/Server/Services/AutoRemediationRuleEngineService.ts +968 -39
- package/Server/Services/AutoRemediationRuleService.ts +118 -0
- package/Server/Services/BillingService.ts +1 -0
- package/Server/Services/InboundEmail/Providers/SendGridInboundProvider.ts +16 -22
- package/Server/Services/IncidentEpisodeMemberService.ts +9 -0
- package/Server/Services/IncidentEpisodePublicNoteService.ts +66 -0
- package/Server/Services/IncidentEpisodeRoleMemberService.ts +27 -0
- package/Server/Services/IncidentEpisodeService.ts +14 -1
- package/Server/Services/IncidentGroupingEngineService.ts +11 -0
- package/Server/Services/IncidentMemberService.ts +21 -0
- package/Server/Services/IncidentPublicNoteService.ts +63 -0
- package/Server/Services/IncidentService.ts +120 -20
- package/Server/Services/IncidentStateTimelineService.ts +15 -0
- package/Server/Services/IncidentTemplateService.ts +83 -3
- package/Server/Services/KubernetesClusterAiAccessService.ts +1943 -0
- package/Server/Services/KubernetesClusterService.ts +1570 -1
- package/Server/Services/LogAggregationService.ts +14 -6
- package/Server/Services/MonitorService.ts +651 -52
- package/Server/Services/MonitorStatusTimelineService.ts +639 -2
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +2 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.ts +2 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +2 -0
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +3 -0
- package/Server/Services/OnCallNotificationAlertingService.ts +36 -12
- package/Server/Services/OnCallSetupReminderService.ts +1 -0
- package/Server/Services/ProbeService.ts +7 -1
- package/Server/Services/ProjectService.ts +11 -1
- package/Server/Services/RunbookCredentialService.ts +26 -1
- package/Server/Services/RunbookSecretService.ts +36 -0
- package/Server/Services/RunnerJobService.ts +941 -23
- package/Server/Services/RunnerService.ts +439 -6
- package/Server/Services/ScheduledMaintenancePublicNoteService.ts +66 -0
- package/Server/Services/ScheduledMaintenanceService.ts +124 -19
- package/Server/Services/ScheduledMaintenanceTemplateService.ts +82 -3
- package/Server/Services/ServiceLevelObjectiveService.ts +14 -1
- package/Server/Services/StatusPagePrivateUserService.ts +1 -0
- package/Server/Services/StatusPageService.ts +301 -74
- package/Server/Services/StatusPageSubscriberService.ts +4 -0
- package/Server/Services/TeamMemberService.ts +309 -7
- package/Server/Services/TraceAggregationService.ts +10 -6
- package/Server/Services/UserEmailService.ts +1 -0
- package/Server/Services/UserNotificationMethodAdminService.ts +1 -0
- package/Server/Services/UserNotificationRuleAdminService.ts +1 -0
- package/Server/Services/UserNotificationRuleService.ts +4 -0
- package/Server/Services/UserService.ts +122 -1
- package/Server/Services/WorkflowVariableService.ts +593 -8
- package/Server/Services/WorkspaceNotificationRuleService.ts +588 -8
- package/Server/Services/WorkspaceProjectAuthTokenService.ts +170 -1
- package/Server/Types/Database/QueryHelper.ts +22 -0
- package/Server/Types/Markdown.ts +502 -27
- package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +182 -0
- package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +525 -0
- package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +854 -0
- package/Server/Utils/AI/CodeFix/CodeFixAgentAuth.ts +22 -2
- package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +1916 -307
- package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +1601 -74
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +139 -7
- package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +58 -1
- package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +54 -1
- package/Server/Utils/AI/Toolbox/AIActionTools.ts +14 -6
- package/Server/Utils/AI/Toolbox/ResourceTools.ts +4 -2
- package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +1341 -150
- package/Server/Utils/AutoRemediation/RemediationVerifier.ts +375 -4
- package/Server/Utils/Database/AffectedResourceRelations.ts +127 -0
- package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +240 -1
- package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +1 -0
- package/Server/Utils/Monitor/MonitorAlert.ts +83 -9
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +5 -1
- package/Server/Utils/Monitor/MonitorIncident.ts +104 -9
- package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +124 -7
- package/Server/Utils/Monitor/ProbeMonitorsNotification.ts +407 -0
- package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +1 -0
- package/Server/Utils/PushNotificationUtil.ts +45 -0
- package/Server/Utils/Rules/OwnerRuleAssignment.ts +40 -3
- package/Server/Utils/Runbook/RunbookExecutePermission.ts +56 -6
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +223 -33
- package/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.ts +559 -0
- package/Server/Utils/StartServer.ts +4 -3
- package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +528 -0
- package/Server/Utils/TwoFactorBackupCodeNotification.ts +1 -0
- package/Server/Utils/UserPermission/UserPermission.ts +16 -0
- package/Server/Utils/Workflow/OAuth2TokenClient.ts +704 -0
- package/Server/Utils/Workflow/WorkflowVariableOAuthToken.ts +318 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +47 -7
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +57 -16
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +193 -214
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +57 -13
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.ts +54 -17
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +179 -161
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +281 -167
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.ts +114 -0
- package/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.ts +895 -0
- package/Server/Utils/Workspace/Slack/Actions/ActionTypes.ts +7 -9
- package/Server/Utils/Workspace/Slack/Actions/Alert.ts +96 -227
- package/Server/Utils/Workspace/Slack/Actions/AlertEpisode.ts +93 -214
- package/Server/Utils/Workspace/Slack/Actions/Auth.ts +30 -3
- package/Server/Utils/Workspace/Slack/Actions/Authorization.ts +99 -0
- package/Server/Utils/Workspace/Slack/Actions/Incident.ts +117 -283
- package/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.ts +101 -196
- package/Server/Utils/Workspace/Slack/Actions/ReactionNote.ts +329 -0
- package/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.ts +111 -287
- package/Server/Utils/Workspace/Slack/Slack.ts +86 -17
- package/Server/Utils/Workspace/Slack/app-manifest.json +2 -0
- package/Server/Utils/Workspace/Workspace.ts +171 -54
- package/Server/Utils/Workspace/WorkspaceActionAuthorization.ts +252 -0
- package/Server/Utils/Workspace/WorkspaceBase.ts +39 -0
- package/Server/Utils/Workspace/WorkspaceOAuthState.ts +280 -0
- package/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.ts +96 -0
- package/Server/Utils/Workspace/WorkspaceReactionNote.ts +620 -0
- package/Tests/App/Dashboard/AttributesJSONDashboardSurfaces.test.tsx +496 -0
- package/Tests/App/Dashboard/AutoRemediationRulesTableCommandRunners.test.tsx +152 -0
- package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +32 -0
- package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +794 -0
- package/Tests/App/Dashboard/CreateOAuthWorkflowVariableModal.test.tsx +840 -0
- package/Tests/App/Dashboard/CreateWorkflowModal.test.tsx +653 -0
- package/Tests/App/Dashboard/DatabaseMonitorStepForm.test.tsx +197 -0
- package/Tests/App/Dashboard/DatabaseMonitorView.test.tsx +376 -0
- package/Tests/App/Dashboard/DisabledWarningRobustness.test.tsx +262 -0
- package/Tests/App/Dashboard/EventNotes.test.tsx +2080 -0
- package/Tests/App/Dashboard/EventNotesPages.test.tsx +486 -0
- package/Tests/App/Dashboard/EventNotesUtil.test.ts +933 -0
- package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +218 -0
- package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +665 -0
- package/Tests/App/Dashboard/IncidentEpisodeFeedPublicNoteNotifyDefault.test.tsx +494 -0
- package/Tests/App/Dashboard/IncidentEpisodeOverviewNotifyDefault.test.tsx +706 -0
- package/Tests/App/Dashboard/IncidentFeedPublicNoteNotifyDefault.test.tsx +472 -0
- package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +858 -0
- package/Tests/App/Dashboard/IncomingEmailAddressSettings.test.tsx +567 -0
- package/Tests/App/Dashboard/InvestigationClusterToolFormat.test.ts +241 -0
- package/Tests/App/Dashboard/InvestigationEvidenceKubectl.test.tsx +322 -0
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +12 -0
- package/Tests/App/Dashboard/InvestigationPanelClusterAccess.test.tsx +476 -0
- package/Tests/App/Dashboard/InvestigationRunDetailsKubectl.test.tsx +383 -0
- package/Tests/App/Dashboard/KubectlActivitySummary.test.tsx +366 -0
- package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +2886 -0
- package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +2688 -0
- package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +915 -0
- package/Tests/App/Dashboard/LayerDateTimeFieldTimezoneName.test.tsx +66 -0
- package/Tests/App/Dashboard/LogMonitorPreview.test.tsx +174 -0
- package/Tests/App/Dashboard/MonitorFeedRefresh.test.tsx +279 -0
- package/Tests/App/Dashboard/MonitorMetricQueryConfig.test.ts +244 -0
- package/Tests/App/Dashboard/MonitorOpenWorkCard.test.tsx +381 -0
- package/Tests/App/Dashboard/MonitorOpenWorkHook.test.tsx +784 -0
- package/Tests/App/Dashboard/MonitorOverviewData.test.tsx +2199 -0
- package/Tests/App/Dashboard/MonitorOverviewHero.test.tsx +1169 -0
- package/Tests/App/Dashboard/MonitorOverviewInput.test.ts +1127 -0
- package/Tests/App/Dashboard/MonitorOverviewPage.test.tsx +2136 -0
- package/Tests/App/Dashboard/MonitorOverviewSelectReadable.test.ts +368 -0
- package/Tests/App/Dashboard/MonitorOverviewStatBar.test.tsx +526 -0
- package/Tests/App/Dashboard/MonitorOverviewSupportingCards.test.tsx +542 -0
- package/Tests/App/Dashboard/MonitorOwnersCombine.test.ts +299 -0
- package/Tests/App/Dashboard/MonitorProbesCard.test.tsx +602 -0
- package/Tests/App/Dashboard/MonitorResponseTimeCard.test.tsx +386 -0
- package/Tests/App/Dashboard/MonitorSetupAndConnectionCards.test.tsx +612 -0
- package/Tests/App/Dashboard/MonitorStatusChangesCard.test.tsx +257 -0
- package/Tests/App/Dashboard/MonitorSummaryTestMonitorButton.test.tsx +360 -1
- package/Tests/App/Dashboard/MonitorUptimeHistoryCard.test.tsx +835 -0
- package/Tests/App/Dashboard/MonitorUptimeSummaryHook.test.tsx +971 -0
- package/Tests/App/Dashboard/MonitorViewLayout.test.tsx +580 -0
- package/Tests/App/Dashboard/OnCallLayerRestrictionTimesPreserved.test.tsx +441 -0
- package/Tests/App/Dashboard/OnCallNavbar.test.tsx +214 -0
- package/Tests/App/Dashboard/OnCallSchedulePreviewCalendarDisplay.test.tsx +1389 -0
- package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +205 -1
- package/Tests/App/Dashboard/OnCallScheduleTimeInputsSettingsZone.test.tsx +198 -0
- package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +524 -0
- package/Tests/App/Dashboard/RemediationSuggestionCardApproval.test.tsx +702 -0
- package/Tests/App/Dashboard/RemoveUserFromProject.test.tsx +427 -0
- package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +4 -0
- package/Tests/App/Dashboard/RumSessionReplaySideMenu.test.tsx +3 -1
- package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +166 -0
- package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +622 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceChangeStateNotifyDefault.test.tsx +1491 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceFeedPublicNoteNotifyDefault.test.tsx +489 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceOverviewNotifyDefault.test.tsx +909 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +14 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceWindowStatsTranslation.test.tsx +307 -0
- package/Tests/App/Dashboard/SecurityEventAttributeColumnPicker.test.tsx +542 -0
- package/Tests/App/Dashboard/SecurityEventConnectionFormModal.test.tsx +174 -0
- package/Tests/App/Dashboard/SecurityEventConnectionsEmptyState.test.tsx +973 -212
- package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +491 -52
- package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +301 -0
- package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +158 -1
- package/Tests/App/Dashboard/SecurityEventProviderTile.test.tsx +869 -0
- package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +959 -94
- package/Tests/App/Dashboard/SecurityEventsNoResults.test.tsx +2 -0
- package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +274 -1
- package/Tests/App/Dashboard/TeamMembershipRemoval.test.ts +900 -0
- package/Tests/App/Dashboard/TeamMembershipRemovalPages.test.tsx +460 -0
- package/Tests/App/Dashboard/TimezoneElements.test.tsx +321 -0
- package/Tests/App/Dashboard/TimezoneSelectButton.test.tsx +634 -0
- package/Tests/App/Dashboard/TraceExplorer.test.tsx +260 -0
- package/Tests/App/Dashboard/TraceMonitorPreview.test.tsx +182 -0
- package/Tests/App/Dashboard/UserTimezoneInit.test.tsx +544 -0
- package/Tests/App/Dashboard/WorkflowVariableModals.test.tsx +1630 -0
- package/Tests/App/Dashboard/WorkflowVariableRoutes.test.tsx +603 -0
- package/Tests/App/Dashboard/WorkflowVariableTokenStatus.test.tsx +589 -0
- package/Tests/App/Dashboard/WorkflowVariableUtil.test.ts +1543 -0
- package/Tests/App/Dashboard/WorkflowVariableView.test.tsx +3227 -0
- package/Tests/App/Dashboard/WorkflowVariablesTable.test.tsx +1277 -395
- package/Tests/App/StatusPage/StatusPageOverviewMonitorGroupUptime.test.tsx +714 -0
- package/Tests/App/StatusPage/StatusPageOverviewUptimeBars.NewYork.test.tsx +1009 -0
- package/Tests/App/StatusPage/StatusPageOverviewUptimeBars.test.tsx +1092 -0
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +5 -0
- package/Tests/Models/DatabaseModels/KubernetesClusterAiAccessColumns.test.ts +520 -0
- package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +66 -2
- package/Tests/Models/DatabaseModels/WorkspaceProjectAuthTokenAccessControl.test.ts +236 -0
- package/Tests/Models/IncidentEpisodePublicNoteNotifyColumnContract.test.ts +103 -0
- package/Tests/Models/IncidentPublicNoteNotifyColumnContract.test.ts +96 -0
- package/Tests/Models/ScheduledMaintenancePublicNoteNotifyColumnContract.test.ts +111 -0
- package/Tests/Server/API/AIInvestigationAPI.test.ts +976 -0
- package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +836 -0
- package/Tests/Server/API/CommonAPIAssertCanCreateTable.test.ts +565 -0
- package/Tests/Server/API/CommonAPIAssertCanReadTable.test.ts +282 -0
- package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +1744 -0
- package/Tests/Server/API/LogHistogramAPI.test.ts +371 -0
- package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +125 -30
- package/Tests/Server/API/MicrosoftTeamsOAuthState.test.ts +1121 -0
- package/Tests/Server/API/MonitorUptimeSummaryAPI.test.ts +685 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +602 -1
- package/Tests/Server/API/SlackChannelCacheAPI.test.ts +358 -0
- package/Tests/Server/API/SlackOAuthState.test.ts +688 -0
- package/Tests/Server/API/StatusPageOverviewMonitorGroupUptime.test.ts +527 -0
- package/Tests/Server/API/StatusPageTestEmailReportAuth.test.ts +2 -0
- package/Tests/Server/API/StatusPageUptimeAPI.test.ts +427 -0
- package/Tests/Server/API/UserProjectsAPI.test.ts +4 -0
- package/Tests/Server/API/WorkflowVariableAPI.test.ts +404 -0
- package/Tests/Server/API/WorkspaceOAuthTestHelpers.ts +289 -0
- package/Tests/Server/API/WorkspaceSendTestNotificationRoutes.test.ts +2064 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +86 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +589 -0
- package/Tests/Server/Infrastructure/Postgres/AddOAuth2WorkflowVariablesMigration.test.ts +254 -0
- package/Tests/Server/Middleware/BlockedUserMiddleware.test.ts +403 -0
- package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +13 -0
- package/Tests/Server/Middleware/PublicRouteUserMiddleware.test.ts +2 -0
- package/Tests/Server/Middleware/RemovedProjectMemberAuthorization.test.ts +469 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +77 -1
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +211 -0
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +358 -0
- package/Tests/Server/Middleware/UserAuthorization.test.ts +97 -0
- package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +2 -0
- package/Tests/Server/Services/AccessTokenProjectUserGrant.test.ts +14 -2
- package/Tests/Server/Services/AccessTokenRemovedMemberAccess.test.ts +1044 -0
- package/Tests/Server/Services/AddOwnersIdempotency.test.ts +5 -0
- package/Tests/Server/Services/AlertEpisodeMemberCreatePermission.test.ts +139 -0
- package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +1361 -0
- package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +375 -0
- package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +2384 -0
- package/Tests/Server/Services/FindOwnersProjectMembership.test.ts +267 -0
- package/Tests/Server/Services/InboundEmail/SendGridInboundProviderAddress.test.ts +90 -0
- package/Tests/Server/Services/IncidentEpisodeMemberCreatePermission.test.ts +143 -0
- package/Tests/Server/Services/IncidentEpisodePublicNoteNotifyDefault.test.ts +805 -0
- package/Tests/Server/Services/IncidentPublicNoteNotifyDefault.test.ts +724 -0
- package/Tests/Server/Services/IncidentRoleProjectMembership.test.ts +233 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessAgentRunnerAssignments.test.ts +372 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessBindingGuard.test.ts +1876 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +620 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +421 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +3662 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessSubjectClusters.test.ts +405 -0
- package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +975 -0
- package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +497 -0
- package/Tests/Server/Services/KubernetesClusterServiceDeleteSettlesRemediation.test.ts +328 -0
- package/Tests/Server/Services/LogAggregationService.test.ts +99 -0
- package/Tests/Server/Services/MonitorServiceIncomingEmailCustomAddress.test.ts +447 -0
- package/Tests/Server/Services/MonitorServiceProbeFlagSync.test.ts +1334 -0
- package/Tests/Server/Services/MonitorServiceProbeStatusGrouping.test.ts +1640 -0
- package/Tests/Server/Services/MonitorServiceRefreshCurrentStatus.test.ts +225 -0
- package/Tests/Server/Services/MonitorStatusBridgeProbePath.test.ts +8 -8
- package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +264 -1
- package/Tests/Server/Services/MonitorStatusTimelineMergedDowntime.test.ts +284 -0
- package/Tests/Server/Services/MonitorUptimeSummaryService.test.ts +1043 -0
- package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +5 -0
- package/Tests/Server/Services/OnCallUndeliverablePageOwnerMembership.test.ts +359 -0
- package/Tests/Server/Services/ProbeServiceDashboardLink.test.ts +191 -0
- package/Tests/Server/Services/ProjectOwnersProjectMembership.test.ts +221 -0
- package/Tests/Server/Services/RuleRun/IncidentAlertMaintenanceRuleEnginesRun.test.ts +5 -0
- package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupARun.test.ts +28 -0
- package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +9 -1
- package/Tests/Server/Services/RunnerJobEnqueueAiCommandAgentRunner.test.ts +230 -0
- package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +978 -0
- package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +866 -0
- package/Tests/Server/Services/RunnerJobSubmitResultRedaction.test.ts +322 -0
- package/Tests/Server/Services/RunnerJobSubmitResultUnstorableText.test.ts +357 -0
- package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +227 -0
- package/Tests/Server/Services/RunnerServiceAgentName.test.ts +581 -0
- package/Tests/Server/Services/ScheduledMaintenancePublicNoteNotifyDefault.test.ts +802 -0
- package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +53 -0
- package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +5 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +12 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +1176 -1
- package/Tests/Server/Services/TeamMemberBillingSeatSync.test.ts +9 -0
- package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +18 -0
- package/Tests/Server/Services/TeamMemberProjectMembership.test.ts +255 -0
- package/Tests/Server/Services/TeamMemberRemovalRevokesProjectAccess.test.ts +600 -0
- package/Tests/Server/Services/TeamMemberResourceCleanup.test.ts +731 -0
- package/Tests/Server/Services/TeamMemberWorkspaceAccountCleanup.test.ts +164 -0
- package/Tests/Server/Services/TraceAggregationService.test.ts +64 -0
- package/Tests/Server/Services/UserServiceBlockedUser.test.ts +293 -0
- package/Tests/Server/Services/WorkflowVariableOAuthHooks.test.ts +1016 -0
- package/Tests/Server/Services/WorkflowVariableOAuthPostgres.test.ts +543 -0
- package/Tests/Server/Services/WorkflowVariableRenameUniqueness.test.ts +16 -5
- package/Tests/Server/Services/WorkspaceNotificationRuleNoteDelivery.test.ts +473 -0
- package/Tests/Server/Services/WorkspaceNotificationRuleSendTestNotification.test.ts +3690 -0
- package/Tests/Server/Services/WorkspaceProjectAuthTokenMiscDataSecrets.test.ts +374 -0
- package/Tests/Server/Services/WorkspaceProjectAuthTokenSlackChannelCache.test.ts +143 -0
- package/Tests/Server/Types/Database/Permissions/WorkflowVariableOAuthColumnPermission.test.ts +250 -0
- package/Tests/Server/Types/Database/QueryHelperJsonContains.test.ts +96 -0
- package/Tests/Server/Types/Markdown.test.ts +1033 -0
- package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +285 -0
- package/Tests/Server/Utils/AI/CodeFix/CodeFixAgentAuth.test.ts +159 -1
- package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +752 -0
- package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +1840 -0
- package/Tests/Server/Utils/AI/KubectlJobRunner.test.ts +1067 -0
- package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +355 -0
- package/Tests/Server/Utils/AI/PostIncidentStatusUpdateTool.test.ts +496 -0
- package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +305 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +1365 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +639 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +1585 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +1065 -0
- package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +2138 -1
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +1035 -0
- package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +517 -0
- package/Tests/Server/Utils/AI/ResourceTools.test.ts +27 -1
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +1371 -24
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorRollbackRace.test.ts +962 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +518 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +939 -2
- package/Tests/Server/Utils/Database/ProjectScopedRelationReferences.test.ts +371 -0
- package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +64 -0
- package/Tests/Server/Utils/Monitor/DatabaseMetricObservationRemediation.test.ts +102 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +362 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +55 -0
- package/Tests/Server/Utils/Monitor/MonitorEmailAddressRedaction.test.ts +255 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +347 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +203 -0
- package/Tests/Server/Utils/Monitor/ProbeMonitorsNotification.test.ts +1147 -0
- package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +79 -0
- package/Tests/Server/Utils/ResourceFeed/ResourceFeedUtil.test.ts +21 -1
- package/Tests/Server/Utils/Rules/OwnerRuleAssignment.test.ts +155 -1
- package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +288 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.test.ts +304 -0
- package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +40 -3
- package/Tests/Server/Utils/Workflow/OAuth2TokenClient.test.ts +702 -0
- package/Tests/Server/Utils/Workflow/OAuth2TokenClientHttpIntegration.test.ts +507 -0
- package/Tests/Server/Utils/Workflow/WorkflowVariableOAuthToken.test.ts +831 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +63 -1
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsAppAccessToken.test.ts +256 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +60 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChatActionAuthorization.test.ts +442 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +44 -2
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +55 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsReactionNoteSync.test.ts +1522 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsServiceUrl.test.ts +130 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +4 -4
- package/Tests/Server/Utils/Workspace/Slack/SlackActionAuthorization.test.ts +691 -0
- package/Tests/Server/Utils/Workspace/Slack/SlackMessageByTimestamp.test.ts +253 -0
- package/Tests/Server/Utils/Workspace/Slack/SlackReactionNote.test.ts +574 -0
- package/Tests/Server/Utils/Workspace/WorkspaceActionAuthorization.test.ts +488 -0
- package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +861 -0
- package/Tests/Server/Utils/Workspace/WorkspaceMessageAuthor.test.ts +635 -0
- package/Tests/Server/Utils/Workspace/WorkspaceOAuthState.test.ts +548 -0
- package/Tests/Server/Utils/Workspace/WorkspaceReactionNote.test.ts +839 -0
- package/Tests/Server/Utils/Workspace/WorkspaceSendErrorMessage.test.ts +725 -0
- package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +283 -9
- package/Tests/Types/DateBrowserLocalWallClock.test.ts +280 -0
- package/Tests/Types/DateLocalShortMonthNameFallback.test.ts +389 -0
- package/Tests/Types/DateUserTimezone.test.ts +177 -4
- package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +306 -0
- package/Tests/Types/Kubernetes/KubernetesClusterAiAccess.test.ts +283 -0
- package/Tests/Types/Kubernetes/KubernetesRunnerPostureWriteScope.test.ts +68 -0
- package/Tests/Types/Monitor/DatabaseMonitor/SqlServerPlatform.test.ts +144 -0
- package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +160 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +53 -0
- package/Tests/Types/Runbook/RunbookStepType.test.ts +91 -16
- package/Tests/Types/Runner/RunnerLiveStatus.test.ts +14 -3
- package/Tests/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.test.ts +15 -0
- package/Tests/Types/StatusPage/PublicNoteSubscriberNotificationDefault.test.ts +1868 -0
- package/Tests/Types/Timezone.test.ts +5 -0
- package/Tests/Types/TimezoneAlias.test.ts +1034 -0
- package/Tests/Types/Workflow/JiraTemplateFixtures.ts +833 -0
- package/Tests/Types/Workflow/JiraTemplateScripts.test.ts +5414 -0
- package/Tests/Types/Workflow/JiraTemplateSimulation.test.ts +4803 -0
- package/Tests/Types/Workflow/JiraTemplates.test.ts +5429 -0
- package/Tests/Types/Workflow/Templates.test.ts +79 -4
- package/Tests/Types/Workflow/WorkflowVariableOAuth.test.ts +520 -0
- package/Tests/Types/Workspace/WorkspaceNoteReaction.test.ts +177 -0
- package/Tests/UI/Components/AttributesJSON/AttributesJSONView.test.tsx +390 -0
- package/Tests/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.test.tsx +566 -0
- package/Tests/UI/Components/AttributesJSON/JSONHighlight.test.ts +131 -0
- package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +276 -1
- package/Tests/UI/Components/Calendar/Calendar.test.tsx +1611 -0
- package/Tests/UI/Components/Calendar/CalendarDayLayout.test.ts +1486 -0
- package/Tests/UI/Components/Calendar/CalendarDayLayoutDst.test.ts +632 -0
- package/Tests/UI/Components/Calendar/CalendarDst.test.tsx +388 -0
- package/Tests/UI/Components/Charts/ChartGroupPlotFloor.test.tsx +170 -0
- package/Tests/UI/Components/Charts/HistogramSelection.test.ts +282 -0
- package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +6 -0
- package/Tests/UI/Components/CustomTimeRangeModalTimezoneTranslation.test.tsx +285 -0
- package/Tests/UI/Components/Dropdown.test.tsx +999 -2
- package/Tests/UI/Components/Forms/FormFieldCheckboxTranslation.test.tsx +216 -0
- package/Tests/UI/Components/Forms/FormFieldTimezoneDescriptionTranslation.test.tsx +461 -0
- package/Tests/UI/Components/Forms/IncidentEpisodePublicNoteNotifyPayload.test.tsx +641 -0
- package/Tests/UI/Components/Forms/IncidentPublicNoteNotifyPayload.test.tsx +346 -0
- package/Tests/UI/Components/Forms/ScheduledMaintenancePublicNoteNotifyPayload.test.tsx +493 -0
- package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +911 -2
- package/Tests/UI/Components/Graphs/DayUptimeGraphTimezone.NewYork.test.tsx +730 -0
- package/Tests/UI/Components/Graphs/DayUptimeGraphTimezone.Tokyo.test.tsx +506 -0
- package/Tests/UI/Components/HistogramClickToZoom.test.tsx +619 -0
- package/Tests/UI/Components/LogDetailsPanelAttributesJSON.test.tsx +248 -0
- package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +47 -1
- package/Tests/UI/Components/LogsViewerSmallScreens.test.tsx +620 -0
- package/Tests/UI/Components/ModelPageModelChange.test.tsx +242 -3
- package/Tests/UI/Components/ModelTable/BaseModelTableDeleteConfirmation.test.tsx +521 -0
- package/Tests/UI/Components/MonitorGraphs/Uptime.test.tsx +406 -2
- package/Tests/UI/Components/MonitorGraphs/UptimeBarDayModal.test.tsx +169 -2
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +266 -0
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerSmallScreens.test.tsx +439 -0
- package/Tests/UI/Components/TimezoneDropdownLegacyValues.test.tsx +704 -0
- package/Tests/UI/Components/UseHistogramZoom.test.tsx +89 -1
- package/Tests/UI/Components/UseLogsHistogram.test.tsx +175 -0
- package/Tests/UI/Monitor/MonitorSummarySnapshotRender.test.tsx +5 -5
- package/Tests/UI/Telemetry/TelemetryDetailPanel.test.tsx +28 -0
- package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +51 -0
- package/Tests/UI/Utils/Timezone.test.ts +747 -33
- package/Tests/UI/Workspace/SendTestNotificationButton.test.tsx +1854 -0
- package/Tests/UI/Workspace/WorkspaceCardsSendTestNotification.test.tsx +1864 -0
- package/Tests/Utils/AI/InvestigationReport.test.ts +435 -7
- package/Tests/Utils/AiRemediation/KubectlAllowlistValidatorParity.test.ts +1084 -0
- package/Tests/Utils/AiRemediation/KubectlOutputRedactor.test.ts +2531 -0
- package/Tests/Utils/AiRemediation/KubectlPolicy.test.ts +1895 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyAllowlist.test.ts +428 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +828 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyCustomResourceScope.test.ts +573 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyDenials.test.ts +1525 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyExposeOverrides.test.ts +275 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyNodePatch.test.ts +428 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyObjectWords.test.ts +195 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyPatchParents.test.ts +637 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyPodSecurityBaseline.test.ts +322 -0
- package/Tests/Utils/AiRemediation/KubectlPolicySafeWriteScope.test.ts +1026 -0
- package/Tests/Utils/AiRemediation/KubectlWaitBudget.test.ts +267 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScope.test.ts +390 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +777 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopeClusterScoped.test.ts +613 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopeEntryPoint.test.ts +576 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopeKubectlSpellings.test.ts +752 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopeObjectWords.test.ts +358 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopeOverrides.test.ts +293 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopeParityCases.ts +598 -0
- package/Tests/Utils/AiRemediation/KubectlWriteScopePolicyParity.test.ts +403 -0
- package/Tests/Utils/Dashboard/DashboardClockComponent.test.ts +154 -5
- package/Tests/Utils/Monitor/IncomingEmailMonitorAddress.test.ts +478 -0
- package/Tests/Utils/Monitor/MonitorCheckScheduleUtil.test.ts +646 -0
- package/Tests/Utils/Monitor/MonitorOverviewCriteriaUtil.test.ts +292 -0
- package/Tests/Utils/Monitor/MonitorOverviewFamily.test.ts +274 -0
- package/Tests/Utils/Monitor/MonitorOverviewPresentationUtil.test.ts +3100 -0
- package/Tests/Utils/Monitor/MonitorOverviewProbeUtil.test.ts +1412 -0
- package/Tests/Utils/Monitor/MonitorOverviewTargetUtil.test.ts +374 -0
- package/Tests/Utils/Monitor/MonitorStatusHistoryUtil.test.ts +332 -0
- package/Tests/Utils/Monitor/MonitorUptimeSummaryUtil.test.ts +946 -0
- package/Tests/Utils/Rum/SessionIdentity.test.ts +94 -0
- package/Tests/Utils/Rum/SessionReplayWafCompatibility.test.ts +840 -0
- package/Tests/Utils/Rum/SessionReplayWireEncoding.test.ts +208 -0
- package/Tests/Utils/Rum/UserFlow.test.ts +1010 -0
- package/Tests/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.test.ts +217 -0
- package/Tests/Utils/StatusPage/ResourceUptime.test.ts +1272 -1
- package/Tests/Utils/StatusPage/UptimeDailyAggregateTotals.test.ts +298 -0
- package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +649 -1
- package/Tests/Utils/StatusPage/UptimeDailyAggregateWindow.test.ts +314 -0
- package/Tests/Utils/Telemetry/AttributesJSON.test.ts +454 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtil.test.ts +418 -2
- package/Tests/Utils/Uptime/DayUptimeGraphUtilMidnightDst.test.ts +160 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Auckland.test.ts +45 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Kolkata.test.ts +40 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.London.test.ts +147 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.NewYork.test.ts +48 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Tokyo.test.ts +39 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezoneCases.ts +1015 -0
- package/Tests/Utils/Uptime/UptimeUtil.test.ts +75 -0
- package/Tests/Utils/Uptime/UptimeUtilChronologicalOrder.test.ts +1279 -0
- package/Types/AutoRemediation/AiRemediationCommandPlan.ts +203 -5
- package/Types/Date.ts +121 -7
- package/Types/Email/EmailMessage.ts +8 -0
- package/Types/Email/EmailTemplateType.ts +1 -0
- package/Types/Exception/ExceptionMessages.ts +1 -0
- package/Types/Kubernetes/KubernetesClusterAiAccess.ts +488 -0
- package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +54 -0
- package/Types/Kubernetes/KubernetesClusterAiAccessToolNames.ts +17 -0
- package/Types/Monitor/DatabaseMonitor/DatabaseMonitorResponse.ts +9 -0
- package/Types/Monitor/DatabaseMonitor/SqlServerPlatform.ts +117 -0
- package/Types/Monitor/KubernetesAlertTemplates.ts +6 -5
- package/Types/Monitor/MonitorStepExceptionMonitor.ts +56 -0
- package/Types/Monitor/MonitorUptimeSummary.ts +113 -0
- package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +11 -0
- package/Types/Rum/SessionReplay.ts +55 -2
- package/Types/Rum/SessionReplayApi.ts +44 -1
- package/Types/Rum/UserFlow.ts +187 -0
- package/Types/Runbook/RunbookStepType.ts +10 -0
- package/Types/Runbook/RunnerJobOrigin.ts +15 -0
- package/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.ts +6 -2
- package/Types/StatusPage/MergedDowntimeTotals.ts +17 -0
- package/Types/StatusPage/PublicNoteSubscriberNotificationDefault.ts +148 -0
- package/Types/StatusPage/UptimeDailyAggregate.ts +15 -0
- package/Types/Telemetry/TelemetryIngestSurface.ts +9 -0
- package/Types/Timezone.ts +5 -0
- package/Types/TimezoneAlias.ts +385 -0
- package/Types/Workflow/Templates.ts +3041 -2
- package/Types/Workflow/WorkflowVariableOAuth.ts +476 -0
- package/Types/Workspace/WorkspaceNoteReaction.ts +132 -0
- package/Typings/Index.d.ts +24 -0
- package/UI/Components/AttributesJSON/AttributesJSONPreferences.ts +175 -0
- package/UI/Components/AttributesJSON/AttributesJSONView.tsx +138 -0
- package/UI/Components/AttributesJSON/AttributesViewToggle.tsx +79 -0
- package/UI/Components/AttributesJSON/BracesGlyph.tsx +30 -0
- package/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.tsx +491 -0
- package/UI/Components/AttributesJSON/JSONHighlight.ts +77 -0
- package/UI/Components/Calendar/Calendar.css +83 -0
- package/UI/Components/Calendar/Calendar.tsx +136 -7
- package/UI/Components/Calendar/CalendarDayLayout.ts +236 -0
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +23 -1
- package/UI/Components/Charts/Utils/HistogramSelection.ts +117 -0
- package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +269 -0
- package/UI/Components/Charts/Utils/useHistogramZoom.ts +26 -2
- package/UI/Components/Date/CustomTimeRangeModal.tsx +26 -3
- package/UI/Components/Dropdown/Dropdown.tsx +58 -9
- package/UI/Components/Forms/Fields/FormField.tsx +49 -14
- package/UI/Components/Graphs/DayUptimeGraph.tsx +166 -25
- package/UI/Components/Graphs/UptimeBarTooltip.tsx +10 -2
- package/UI/Components/Graphs/UptimeDaySummary.tsx +8 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +52 -5
- package/UI/Components/LogsViewer/components/ColumnSelector.tsx +13 -2
- package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +13 -1
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +91 -76
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +15 -1
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +28 -106
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +54 -6
- package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +8 -2
- package/UI/Components/LogsViewer/useLogsHistogram.ts +39 -6
- package/UI/Components/ModelTable/BaseModelTable.tsx +66 -14
- package/UI/Components/MonitorGraphs/Uptime.tsx +12 -0
- package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +10 -2
- package/UI/Components/Page/ModelPage.tsx +59 -3
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +69 -5
- package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +12 -1
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +15 -1
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +36 -104
- package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +9 -1
- package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +19 -1
- package/UI/Utils/Timezone.ts +69 -28
- package/Utils/AI/InvestigationReport.ts +195 -25
- package/Utils/AiRemediation/KubectlOutputRedactor.ts +1724 -0
- package/Utils/AiRemediation/KubectlPolicy.ts +4136 -0
- package/Utils/AiRemediation/KubectlWaitBudget.ts +156 -0
- package/Utils/AiRemediation/KubectlWriteScope.ts +1964 -0
- package/Utils/Dashboard/Components/DashboardClockComponent.ts +7 -6
- package/Utils/Monitor/IncomingEmailMonitorAddress.ts +292 -0
- package/Utils/Monitor/MonitorCheckScheduleUtil.ts +654 -0
- package/Utils/Monitor/MonitorOverviewCriteriaUtil.ts +188 -0
- package/Utils/Monitor/MonitorOverviewFamily.ts +263 -0
- package/Utils/Monitor/MonitorOverviewPresentationUtil.ts +1958 -0
- package/Utils/Monitor/MonitorOverviewProbeUtil.ts +979 -0
- package/Utils/Monitor/MonitorOverviewTargetUtil.ts +299 -0
- package/Utils/Monitor/MonitorStatusHistoryUtil.ts +201 -0
- package/Utils/Monitor/MonitorUptimeSummaryUtil.ts +638 -0
- package/Utils/Rum/SessionIdentity.ts +44 -0
- package/Utils/Rum/SessionReplayWireEncoding.ts +137 -0
- package/Utils/Rum/UserFlow.ts +1527 -0
- package/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.ts +107 -0
- package/Utils/StatusPage/ResourceUptime.ts +99 -0
- package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +197 -0
- package/Utils/Telemetry/AttributesJSON.ts +367 -0
- package/Utils/Uptime/DayUptimeGraphUtil.ts +165 -0
- package/Utils/Uptime/UptimeUtil.ts +122 -38
- package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +8 -1
- package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
- package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js +8 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +10 -3
- package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +10 -3
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +434 -0
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +49 -0
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Runner.js +4 -4
- package/build/dist/Models/DatabaseModels/Runner.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunnerJob.js +66 -0
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +11 -3
- package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkflowVariable.js +487 -3
- package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js +64 -16
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +205 -0
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/AutoRemediationAPI.js +115 -2
- package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +49 -0
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +564 -0
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -0
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +465 -176
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/MonitorAPI.js +107 -1
- package/build/dist/Server/API/MonitorAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallScheduleTimelineAPI.js +5 -16
- package/build/dist/Server/API/OnCallScheduleTimelineAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +322 -78
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +123 -17
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +144 -5
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js +1 -0
- package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js.map +1 -1
- package/build/dist/Server/API/WorkflowVariableAPI.js +109 -0
- package/build/dist/Server/API/WorkflowVariableAPI.js.map +1 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js +29 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.js +38 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.js +54 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.js +32 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js +21 -0
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +32 -2
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +39 -6
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Services/AIAgentService.js +1 -0
- package/build/dist/Server/Services/AIAgentService.js.map +1 -1
- package/build/dist/Server/Services/AccessTokenService.js +87 -42
- package/build/dist/Server/Services/AccessTokenService.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodeMemberService.js +4 -0
- package/build/dist/Server/Services/AlertEpisodeMemberService.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodeService.js +12 -1
- package/build/dist/Server/Services/AlertEpisodeService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +110 -23
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +716 -30
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js +100 -0
- package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +1 -0
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/InboundEmail/Providers/SendGridInboundProvider.js +11 -16
- package/build/dist/Server/Services/InboundEmail/Providers/SendGridInboundProvider.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeMemberService.js +4 -0
- package/build/dist/Server/Services/IncidentEpisodeMemberService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +48 -0
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeRoleMemberService.js +21 -0
- package/build/dist/Server/Services/IncidentEpisodeRoleMemberService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeService.js +12 -1
- package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
- package/build/dist/Server/Services/IncidentGroupingEngineService.js +8 -0
- package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentMemberService.js +14 -0
- package/build/dist/Server/Services/IncidentMemberService.js.map +1 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js +48 -0
- package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +99 -21
- 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 +68 -4
- package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +1470 -0
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -0
- package/build/dist/Server/Services/KubernetesClusterService.js +1097 -0
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +14 -6
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +487 -48
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +540 -3
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +2 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js +2 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +2 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +3 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallNotificationAlertingService.js +29 -7
- package/build/dist/Server/Services/OnCallNotificationAlertingService.js.map +1 -1
- package/build/dist/Server/Services/OnCallSetupReminderService.js +1 -0
- package/build/dist/Server/Services/OnCallSetupReminderService.js.map +1 -1
- package/build/dist/Server/Services/ProbeService.js +7 -1
- package/build/dist/Server/Services/ProbeService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +11 -1
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/RunbookCredentialService.js +24 -0
- package/build/dist/Server/Services/RunbookCredentialService.js.map +1 -1
- package/build/dist/Server/Services/RunbookSecretService.js +44 -0
- package/build/dist/Server/Services/RunbookSecretService.js.map +1 -1
- package/build/dist/Server/Services/RunnerJobService.js +641 -21
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Services/RunnerService.js +344 -5
- package/build/dist/Server/Services/RunnerService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +48 -0
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +102 -20
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceTemplateService.js +67 -4
- package/build/dist/Server/Services/ScheduledMaintenanceTemplateService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +12 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserService.js +1 -0
- package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +230 -32
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +4 -0
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +270 -11
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js +10 -6
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +1 -0
- package/build/dist/Server/Services/UserEmailService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1 -0
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js +1 -0
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +4 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +107 -1
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/WorkflowVariableService.js +396 -7
- package/build/dist/Server/Services/WorkflowVariableService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +463 -7
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +144 -2
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +20 -0
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +394 -27
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +123 -0
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -0
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +344 -0
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -0
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +587 -0
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentAuth.js +18 -2
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentAuth.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +1380 -239
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +1099 -68
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +88 -6
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +45 -11
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +41 -11
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js +11 -4
- package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +4 -2
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +997 -141
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
- package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +299 -4
- package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
- package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +120 -0
- package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -0
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +160 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +1 -0
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +59 -12
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +5 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +84 -22
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +70 -5
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/ProbeMonitorsNotification.js +249 -0
- package/build/dist/Server/Utils/Monitor/ProbeMonitorsNotification.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -1
- package/build/dist/Server/Utils/PushNotificationUtil.js +27 -0
- package/build/dist/Server/Utils/PushNotificationUtil.js.map +1 -1
- package/build/dist/Server/Utils/Rules/OwnerRuleAssignment.js +26 -3
- package/build/dist/Server/Utils/Rules/OwnerRuleAssignment.js.map +1 -1
- package/build/dist/Server/Utils/Runbook/RunbookExecutePermission.js +38 -3
- package/build/dist/Server/Utils/Runbook/RunbookExecutePermission.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +143 -23
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.js +406 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.js.map +1 -0
- package/build/dist/Server/Utils/StartServer.js +4 -3
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +382 -0
- package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -0
- package/build/dist/Server/Utils/TwoFactorBackupCodeNotification.js +1 -0
- package/build/dist/Server/Utils/TwoFactorBackupCodeNotification.js.map +1 -1
- package/build/dist/Server/Utils/UserPermission/UserPermission.js +10 -0
- package/build/dist/Server/Utils/UserPermission/UserPermission.js.map +1 -1
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +447 -0
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -0
- package/build/dist/Server/Utils/Workflow/WorkflowVariableOAuthToken.js +202 -0
- package/build/dist/Server/Utils/Workflow/WorkflowVariableOAuthToken.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +39 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +44 -4
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +150 -191
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +44 -4
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.js +39 -17
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +151 -145
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +221 -127
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.js +91 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.js +673 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js +3 -9
- package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js +65 -166
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js +63 -157
- package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +23 -3
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Authorization.js +92 -0
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Authorization.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js +81 -210
- package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.js +68 -138
- package/build/dist/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Actions/ReactionNote.js +240 -0
- package/build/dist/Server/Utils/Workspace/Slack/Actions/ReactionNote.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js +85 -212
- package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js +60 -17
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +2 -0
- package/build/dist/Server/Utils/Workspace/Workspace.js +134 -41
- package/build/dist/Server/Utils/Workspace/Workspace.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/WorkspaceActionAuthorization.js +197 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceActionAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceBase.js +32 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceBase.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/WorkspaceOAuthState.js +199 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceOAuthState.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.js +91 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceReactionNote.js +450 -0
- package/build/dist/Server/Utils/Workspace/WorkspaceReactionNote.js.map +1 -0
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +103 -5
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
- package/build/dist/Types/Date.js +97 -7
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Exception/ExceptionMessages.js +1 -0
- package/build/dist/Types/Exception/ExceptionMessages.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +245 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +49 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessToolNames.js +16 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessToolNames.js.map +1 -0
- package/build/dist/Types/Monitor/DatabaseMonitor/SqlServerPlatform.js +104 -0
- package/build/dist/Types/Monitor/DatabaseMonitor/SqlServerPlatform.js.map +1 -0
- package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +6 -5
- package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepExceptionMonitor.js +32 -0
- package/build/dist/Types/Monitor/MonitorStepExceptionMonitor.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorUptimeSummary.js +52 -0
- package/build/dist/Types/Monitor/MonitorUptimeSummary.js.map +1 -0
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +9 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +52 -2
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayApi.js +11 -0
- package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
- package/build/dist/Types/Rum/UserFlow.js +115 -0
- package/build/dist/Types/Rum/UserFlow.js.map +1 -0
- package/build/dist/Types/Runbook/RunbookStepType.js +10 -0
- package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
- package/build/dist/Types/Runbook/RunnerJobOrigin.js +14 -0
- package/build/dist/Types/Runbook/RunnerJobOrigin.js.map +1 -1
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js +2 -2
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js.map +1 -1
- package/build/dist/Types/StatusPage/MergedDowntimeTotals.js +2 -0
- package/build/dist/Types/StatusPage/MergedDowntimeTotals.js.map +1 -0
- package/build/dist/Types/StatusPage/PublicNoteSubscriberNotificationDefault.js +65 -0
- package/build/dist/Types/StatusPage/PublicNoteSubscriberNotificationDefault.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +8 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
- package/build/dist/Types/Timezone.js +5 -0
- package/build/dist/Types/Timezone.js.map +1 -1
- package/build/dist/Types/TimezoneAlias.js +340 -0
- package/build/dist/Types/TimezoneAlias.js.map +1 -0
- package/build/dist/Types/Workflow/Templates.js +2694 -2
- package/build/dist/Types/Workflow/Templates.js.map +1 -1
- package/build/dist/Types/Workflow/WorkflowVariableOAuth.js +314 -0
- package/build/dist/Types/Workflow/WorkflowVariableOAuth.js.map +1 -0
- package/build/dist/Types/Workspace/WorkspaceNoteReaction.js +98 -0
- package/build/dist/Types/Workspace/WorkspaceNoteReaction.js.map +1 -0
- package/build/dist/UI/Components/AttributesJSON/AttributesJSONPreferences.js +95 -0
- package/build/dist/UI/Components/AttributesJSON/AttributesJSONPreferences.js.map +1 -0
- package/build/dist/UI/Components/AttributesJSON/AttributesJSONView.js +44 -0
- package/build/dist/UI/Components/AttributesJSON/AttributesJSONView.js.map +1 -0
- package/build/dist/UI/Components/AttributesJSON/AttributesViewToggle.js +27 -0
- package/build/dist/UI/Components/AttributesJSON/AttributesViewToggle.js.map +1 -0
- package/build/dist/UI/Components/AttributesJSON/BracesGlyph.js +9 -0
- package/build/dist/UI/Components/AttributesJSON/BracesGlyph.js.map +1 -0
- package/build/dist/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.js +260 -0
- package/build/dist/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.js.map +1 -0
- package/build/dist/UI/Components/AttributesJSON/JSONHighlight.js +56 -0
- package/build/dist/UI/Components/AttributesJSON/JSONHighlight.js.map +1 -0
- package/build/dist/UI/Components/Calendar/Calendar.js +94 -7
- package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
- package/build/dist/UI/Components/Calendar/CalendarDayLayout.js +136 -0
- package/build/dist/UI/Components/Calendar/CalendarDayLayout.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/HistogramSelection.js +72 -0
- package/build/dist/UI/Components/Charts/Utils/HistogramSelection.js.map +1 -0
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +178 -0
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -0
- package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js +13 -1
- package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js.map +1 -1
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +20 -4
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js +33 -9
- package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +36 -13
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +117 -11
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
- package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +5 -1
- package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -1
- package/build/dist/UI/Components/Graphs/UptimeDaySummary.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +36 -8
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js +10 -2
- package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +9 -1
- package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +16 -15
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +14 -74
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +22 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +9 -2
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +16 -5
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +23 -8
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js +7 -1
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
- package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +5 -1
- package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -1
- package/build/dist/UI/Components/Page/ModelPage.js +37 -3
- package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +27 -6
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +7 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +17 -77
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +10 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +15 -1
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/Timezone.js +52 -17
- package/build/dist/UI/Utils/Timezone.js.map +1 -1
- package/build/dist/Utils/AI/InvestigationReport.js +116 -21
- package/build/dist/Utils/AI/InvestigationReport.js.map +1 -1
- package/build/dist/Utils/AiRemediation/KubectlOutputRedactor.js +1209 -0
- package/build/dist/Utils/AiRemediation/KubectlOutputRedactor.js.map +1 -0
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js +2973 -0
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/KubectlWaitBudget.js +99 -0
- package/build/dist/Utils/AiRemediation/KubectlWaitBudget.js.map +1 -0
- package/build/dist/Utils/AiRemediation/KubectlWriteScope.js +1429 -0
- package/build/dist/Utils/AiRemediation/KubectlWriteScope.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardClockComponent.js +7 -6
- package/build/dist/Utils/Dashboard/Components/DashboardClockComponent.js.map +1 -1
- package/build/dist/Utils/Monitor/IncomingEmailMonitorAddress.js +214 -0
- package/build/dist/Utils/Monitor/IncomingEmailMonitorAddress.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorCheckScheduleUtil.js +471 -0
- package/build/dist/Utils/Monitor/MonitorCheckScheduleUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorOverviewCriteriaUtil.js +107 -0
- package/build/dist/Utils/Monitor/MonitorOverviewCriteriaUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorOverviewFamily.js +191 -0
- package/build/dist/Utils/Monitor/MonitorOverviewFamily.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorOverviewPresentationUtil.js +1259 -0
- package/build/dist/Utils/Monitor/MonitorOverviewPresentationUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorOverviewProbeUtil.js +593 -0
- package/build/dist/Utils/Monitor/MonitorOverviewProbeUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorOverviewTargetUtil.js +195 -0
- package/build/dist/Utils/Monitor/MonitorOverviewTargetUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorStatusHistoryUtil.js +127 -0
- package/build/dist/Utils/Monitor/MonitorStatusHistoryUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorUptimeSummaryUtil.js +464 -0
- package/build/dist/Utils/Monitor/MonitorUptimeSummaryUtil.js.map +1 -0
- package/build/dist/Utils/Rum/SessionIdentity.js +31 -1
- package/build/dist/Utils/Rum/SessionIdentity.js.map +1 -1
- package/build/dist/Utils/Rum/SessionReplayWireEncoding.js +128 -0
- package/build/dist/Utils/Rum/SessionReplayWireEncoding.js.map +1 -0
- package/build/dist/Utils/Rum/UserFlow.js +941 -0
- package/build/dist/Utils/Rum/UserFlow.js.map +1 -0
- package/build/dist/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.js +82 -0
- package/build/dist/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.js.map +1 -0
- package/build/dist/Utils/StatusPage/ResourceUptime.js +46 -0
- package/build/dist/Utils/StatusPage/ResourceUptime.js.map +1 -1
- package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
- package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/AttributesJSON.js +245 -0
- package/build/dist/Utils/Telemetry/AttributesJSON.js.map +1 -0
- package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js +103 -0
- package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js.map +1 -1
- package/build/dist/Utils/Uptime/UptimeUtil.js +91 -27
- package/build/dist/Utils/Uptime/UptimeUtil.js.map +1 -1
- package/jest.config.json +2 -1
- package/package.json +2 -1
- package/tsconfig.tests.json +36 -0
- package/Tests/UI/Components/EmptyState/EmptyStateGuideLinks.test.tsx +0 -337
- package/UI/Components/EmptyState/EmptyStateGuideLinks.tsx +0 -124
- package/build/dist/UI/Components/EmptyState/EmptyStateGuideLinks.js +0 -47
- package/build/dist/UI/Components/EmptyState/EmptyStateGuideLinks.js.map +0 -1
|
@@ -0,0 +1,4803 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* End-to-end simulations of the Jira templates.
|
|
3
|
+
*
|
|
4
|
+
* The script tests prove that each script decides correctly given its
|
|
5
|
+
* arguments. What they cannot prove is that the arguments the runner actually
|
|
6
|
+
* hands a script are the ones it expects, that an If / Else reads the value the
|
|
7
|
+
* script returned, that a request body is still valid JSON after substitution,
|
|
8
|
+
* or that two templates chained together settle instead of echoing each other
|
|
9
|
+
* forever. Those are properties of a whole graph, so this file runs whole
|
|
10
|
+
* graphs.
|
|
11
|
+
*
|
|
12
|
+
* The interpreter below walks a built template the way RunWorkflow does: the
|
|
13
|
+
* arguments are resolved by the same substitution, one step runs at a time in
|
|
14
|
+
* FIFO order, and each step's return values are stored where the next step's
|
|
15
|
+
* references look for them. What is real and what is faked is deliberate:
|
|
16
|
+
*
|
|
17
|
+
* - Real: VMAPI substitution, the JavaScript component in the isolated-vm
|
|
18
|
+
* sandbox, the If / Else component, the Log component, and the API
|
|
19
|
+
* components themselves — sanitizeArgs, URL parsing, HTTPResponse and the
|
|
20
|
+
* choice of port included.
|
|
21
|
+
* - Faked: Jira, at API.fetch, the one function every API component sends
|
|
22
|
+
* through; the database, per step, by component id; and one DNS lookup, so
|
|
23
|
+
* the SSRF check the API components run stays offline.
|
|
24
|
+
*
|
|
25
|
+
* Each run returns a trace — the steps executed, every request sent to Jira,
|
|
26
|
+
* every database call and every log line — and the tests assert exactly what
|
|
27
|
+
* would have left the workflow.
|
|
28
|
+
*
|
|
29
|
+
* Every Jira template comes in an incident and an alert version, built from
|
|
30
|
+
* the same builders. The scenarios are written once and run for both kinds,
|
|
31
|
+
* from the table under "The two kinds"; the few ways the kinds differ (alerts
|
|
32
|
+
* have no public notes, no status page and two default severities) have tests
|
|
33
|
+
* of their own. A last group runs the two kinds against each other, because a
|
|
34
|
+
* project can enable both sets at once and an issue linked to one kind of
|
|
35
|
+
* record must never be taken for the other.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
ACKNOWLEDGED_STATE,
|
|
40
|
+
ALERT_ACKNOWLEDGED_STATE,
|
|
41
|
+
ALERT_CREATED_STATE,
|
|
42
|
+
ALERT_ID,
|
|
43
|
+
ALERT_NUMBER,
|
|
44
|
+
ALERT_RESOLVED_STATE,
|
|
45
|
+
ALERT_SEVERITIES,
|
|
46
|
+
ALERT_STATES,
|
|
47
|
+
CREATED_STATE,
|
|
48
|
+
CRITICAL_SEVERITY_ID,
|
|
49
|
+
DONE,
|
|
50
|
+
EMPTY_JIRA_SEARCH,
|
|
51
|
+
HIGH_ALERT_SEVERITY_ID,
|
|
52
|
+
IN_PROGRESS,
|
|
53
|
+
INCIDENT_ID,
|
|
54
|
+
INCIDENT_NUMBER,
|
|
55
|
+
INCIDENT_SEVERITIES,
|
|
56
|
+
INCIDENT_STATES,
|
|
57
|
+
JIRA_ISSUE_ID,
|
|
58
|
+
JIRA_ISSUE_KEY,
|
|
59
|
+
JIRA_SITE,
|
|
60
|
+
LOW_ALERT_SEVERITY_ID,
|
|
61
|
+
MAJOR_SEVERITY_ID,
|
|
62
|
+
MINOR_SEVERITY_ID,
|
|
63
|
+
OTHER_ALERT_ID,
|
|
64
|
+
OTHER_INCIDENT_ID,
|
|
65
|
+
PROJECT_ID,
|
|
66
|
+
RESOLVED_CHANGELOG,
|
|
67
|
+
RESOLVED_STATE,
|
|
68
|
+
STARTED_CHANGELOG,
|
|
69
|
+
TO_DO,
|
|
70
|
+
alertLabel,
|
|
71
|
+
alertModel,
|
|
72
|
+
alertNoteModel,
|
|
73
|
+
changelogItem,
|
|
74
|
+
incidentLabel,
|
|
75
|
+
incidentModel,
|
|
76
|
+
jiraCommentEvent,
|
|
77
|
+
jiraIssueCreatedEvent,
|
|
78
|
+
jiraIssueLabelsResponse,
|
|
79
|
+
jiraIssueUpdatedEvent,
|
|
80
|
+
jiraSearchResponse,
|
|
81
|
+
jiraStatus,
|
|
82
|
+
jiraTransition,
|
|
83
|
+
jiraUser,
|
|
84
|
+
noteModel,
|
|
85
|
+
} from "./JiraTemplateFixtures";
|
|
86
|
+
import {
|
|
87
|
+
JIRA_ALERT_LABEL_PREFIX,
|
|
88
|
+
JIRA_INCIDENT_LABEL_PREFIX,
|
|
89
|
+
JIRA_LINK_LABEL,
|
|
90
|
+
JIRA_SYNCED_FROM_ONEUPTIME_MARKER,
|
|
91
|
+
ONEUPTIME_SYNCED_FROM_JIRA_MARKER,
|
|
92
|
+
WorkflowTemplate,
|
|
93
|
+
WorkflowTemplateCategory,
|
|
94
|
+
WorkflowTemplateVariable,
|
|
95
|
+
buildGraphForTemplate,
|
|
96
|
+
getWorkflowTemplate,
|
|
97
|
+
getWorkflowTemplatesByCategory,
|
|
98
|
+
} from "../../../Types/Workflow/Templates";
|
|
99
|
+
import ComponentMetadata, {
|
|
100
|
+
Argument,
|
|
101
|
+
ComponentInputType,
|
|
102
|
+
ComponentType,
|
|
103
|
+
} from "../../../Types/Workflow/Component";
|
|
104
|
+
import ComponentID from "../../../Types/Workflow/ComponentID";
|
|
105
|
+
import { JSONObject, JSONValue } from "../../../Types/JSON";
|
|
106
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
107
|
+
import Exception from "../../../Types/Exception/Exception";
|
|
108
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
109
|
+
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
|
110
|
+
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
|
111
|
+
import API, { APIFetchOptions } from "../../../Utils/API";
|
|
112
|
+
import { loadComponentsAndCategories } from "../../../UI/Components/Workflow/Utils";
|
|
113
|
+
import VMUtil from "../../../Server/Utils/VM/VMAPI";
|
|
114
|
+
import ComponentCode, {
|
|
115
|
+
RunOptions,
|
|
116
|
+
RunReturnType,
|
|
117
|
+
} from "../../../Server/Types/Workflow/ComponentCode";
|
|
118
|
+
import JavaScriptCode from "../../../Server/Types/Workflow/Components/JavaScript";
|
|
119
|
+
import IfElse from "../../../Server/Types/Workflow/Components/Conditions/IfElse";
|
|
120
|
+
import Log from "../../../Server/Types/Workflow/Components/Log";
|
|
121
|
+
import ApiGet from "../../../Server/Types/Workflow/Components/API/Get";
|
|
122
|
+
import ApiPost from "../../../Server/Types/Workflow/Components/API/Post";
|
|
123
|
+
import ApiPut from "../../../Server/Types/Workflow/Components/API/Put";
|
|
124
|
+
import {
|
|
125
|
+
afterAll,
|
|
126
|
+
beforeAll,
|
|
127
|
+
describe,
|
|
128
|
+
expect,
|
|
129
|
+
jest,
|
|
130
|
+
test,
|
|
131
|
+
} from "@jest/globals";
|
|
132
|
+
import dns from "dns";
|
|
133
|
+
|
|
134
|
+
// Every script and condition runs in a real isolate, and each one takes a moment to start.
|
|
135
|
+
jest.setTimeout(120000);
|
|
136
|
+
|
|
137
|
+
/* ------------------------------ Configuration ------------------------------ */
|
|
138
|
+
|
|
139
|
+
/*
|
|
140
|
+
* A value that could only have come from the secret variable, so finding it
|
|
141
|
+
* anywhere in a trace means the secret travelled somewhere it should not.
|
|
142
|
+
*/
|
|
143
|
+
const JIRA_TOKEN: string =
|
|
144
|
+
"c2ltdWxhdGlvbkBhY21lLmV4YW1wbGU6QVRBVFQtc2ltdWxhdGVkLXNlY3JldA==";
|
|
145
|
+
|
|
146
|
+
const ONEUPTIME_URL: string = "https://oneuptime.com";
|
|
147
|
+
|
|
148
|
+
/** The id the database gives the incident the declare template creates. */
|
|
149
|
+
const NEW_INCIDENT_ID: string = "5d6e7f80-9a1b-4c2d-8e3f-4a5b6c7d8e9f";
|
|
150
|
+
|
|
151
|
+
/** The id the database gives the alert the create-alert template creates. */
|
|
152
|
+
const NEW_ALERT_ID: string = "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d";
|
|
153
|
+
|
|
154
|
+
/*
|
|
155
|
+
* The ids the database gives a new note or timeline row. Neither is ever
|
|
156
|
+
* written anywhere, so they are kept apart from every fixture id: a template
|
|
157
|
+
* quoting the wrong step's id would otherwise pass by coincidence.
|
|
158
|
+
*/
|
|
159
|
+
const NEW_NOTE_ID: string = "abababab-0000-4000-8000-000000000001";
|
|
160
|
+
|
|
161
|
+
const NEW_TIMELINE_ID: string = "cdcdcdcd-0000-4000-8000-000000000001";
|
|
162
|
+
|
|
163
|
+
/*
|
|
164
|
+
* The account the templates' API token belongs to. Its own edits come back
|
|
165
|
+
* as webhooks too, which is what the echo suite replays.
|
|
166
|
+
*/
|
|
167
|
+
const AUTOMATION_USER: JSONObject = jiraUser(
|
|
168
|
+
"OneUptime Automation",
|
|
169
|
+
"712020:0e6c1b8a-automation",
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
/*
|
|
173
|
+
* A header Jira sends with every delivery. Its value is looked for in the
|
|
174
|
+
* output of the security suite, because a Jira user naming the webhook's
|
|
175
|
+
* request headers in a comment must not get them written anywhere.
|
|
176
|
+
*/
|
|
177
|
+
const WEBHOOK_IDENTIFIER: string = "a1b2c3d4-webhook-delivery-sentinel";
|
|
178
|
+
|
|
179
|
+
/** What the create wizard would have stored for each variable a template declares. */
|
|
180
|
+
const VARIABLE_VALUES: Dictionary<string> = {
|
|
181
|
+
jiraBaseUrl: JIRA_SITE,
|
|
182
|
+
jiraBasicAuthToken: JIRA_TOKEN,
|
|
183
|
+
jiraProjectKey: "OPS",
|
|
184
|
+
jiraIssueType: "Task",
|
|
185
|
+
oneuptimeUrl: ONEUPTIME_URL,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const AUTHORIZATION: Dictionary<string> = {
|
|
189
|
+
Authorization: `Basic ${JIRA_TOKEN}`,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/* ------------------------------- The graph ------------------------------- */
|
|
193
|
+
|
|
194
|
+
/*
|
|
195
|
+
* The MERGED registry, as in Templates.test.ts: the database components
|
|
196
|
+
* (incident-find-one, alert-state-timeline-create-one, ...) only exist there,
|
|
197
|
+
* and a node without metadata has no arguments to resolve.
|
|
198
|
+
*/
|
|
199
|
+
const registry: Array<ComponentMetadata> =
|
|
200
|
+
loadComponentsAndCategories().components;
|
|
201
|
+
|
|
202
|
+
interface SimulatedNode {
|
|
203
|
+
componentId: string;
|
|
204
|
+
metadataId: string;
|
|
205
|
+
componentType: ComponentType;
|
|
206
|
+
metadata: ComponentMetadata;
|
|
207
|
+
arguments: JSONObject;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface SimulatedGraph {
|
|
211
|
+
triggerComponentId: string;
|
|
212
|
+
nodes: Dictionary<SimulatedNode>;
|
|
213
|
+
/** componentId -> port id -> the component ids its edges lead to, as RunWorkflow's run stack keeps them. */
|
|
214
|
+
outPorts: Dictionary<Dictionary<Array<string>>>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let generatedIdCounter: number = 0;
|
|
218
|
+
|
|
219
|
+
type GenerateIdFunction = () => string;
|
|
220
|
+
|
|
221
|
+
const generateId: GenerateIdFunction = (): string => {
|
|
222
|
+
generatedIdCounter++;
|
|
223
|
+
return `simulated-${generatedIdCounter}`;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
type BuildSimulatedGraphFunction = (templateId: string) => SimulatedGraph;
|
|
227
|
+
|
|
228
|
+
/*
|
|
229
|
+
* Built through buildGraphForTemplate, the path the create wizard takes, so
|
|
230
|
+
* the simulation runs the graph a user actually gets — not the spec behind it.
|
|
231
|
+
*/
|
|
232
|
+
const buildSimulatedGraph: BuildSimulatedGraphFunction = (
|
|
233
|
+
templateId: string,
|
|
234
|
+
): SimulatedGraph => {
|
|
235
|
+
const graph: JSONObject | null = buildGraphForTemplate(
|
|
236
|
+
templateId,
|
|
237
|
+
generateId,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
if (!graph) {
|
|
241
|
+
throw new Error(`There is no template ${templateId}.`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const nodes: Dictionary<SimulatedNode> = {};
|
|
245
|
+
const outPorts: Dictionary<Dictionary<Array<string>>> = {};
|
|
246
|
+
const componentIdByNodeId: Dictionary<string> = {};
|
|
247
|
+
|
|
248
|
+
for (const rawNode of graph["nodes"] as Array<JSONObject>) {
|
|
249
|
+
const data: JSONObject = rawNode["data"] as JSONObject;
|
|
250
|
+
const componentId: string = data["id"] as string;
|
|
251
|
+
const metadataId: string = data["metadataId"] as string;
|
|
252
|
+
const metadata: ComponentMetadata | undefined = registry.find(
|
|
253
|
+
(component: ComponentMetadata) => {
|
|
254
|
+
return component.id === metadataId;
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if (!metadata) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`${componentId} uses ${metadataId}, which is not a component.`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
nodes[componentId] = {
|
|
265
|
+
componentId: componentId,
|
|
266
|
+
metadataId: metadataId,
|
|
267
|
+
componentType: data["componentType"] as ComponentType,
|
|
268
|
+
metadata: metadata,
|
|
269
|
+
arguments: (data["arguments"] as JSONObject) || {},
|
|
270
|
+
};
|
|
271
|
+
outPorts[componentId] = {};
|
|
272
|
+
componentIdByNodeId[rawNode["id"] as string] = componentId;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const edge of graph["edges"] as Array<JSONObject>) {
|
|
276
|
+
const from: string = componentIdByNodeId[
|
|
277
|
+
edge["source"] as string
|
|
278
|
+
] as string;
|
|
279
|
+
const to: string = componentIdByNodeId[edge["target"] as string] as string;
|
|
280
|
+
const port: string = edge["sourceHandle"] as string;
|
|
281
|
+
const ports: Dictionary<Array<string>> = outPorts[from] as Dictionary<
|
|
282
|
+
Array<string>
|
|
283
|
+
>;
|
|
284
|
+
|
|
285
|
+
ports[port] = [...(ports[port] || []), to];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const triggers: Array<SimulatedNode> = Object.values(nodes).filter(
|
|
289
|
+
(node: SimulatedNode) => {
|
|
290
|
+
return node.componentType === ComponentType.Trigger;
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
if (triggers.length !== 1) {
|
|
295
|
+
throw new Error(`${templateId} has ${triggers.length} triggers, not one.`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
triggerComponentId: (triggers[0] as SimulatedNode).componentId,
|
|
300
|
+
nodes: nodes,
|
|
301
|
+
outPorts: outPorts,
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
type VariablesForFunction = (templateId: string) => Dictionary<string>;
|
|
306
|
+
|
|
307
|
+
/*
|
|
308
|
+
* Only the variables the template declares, because that is all the wizard
|
|
309
|
+
* creates. A reference to anything else stays unresolved, exactly as it would
|
|
310
|
+
* in a real workflow.
|
|
311
|
+
*/
|
|
312
|
+
const variablesFor: VariablesForFunction = (
|
|
313
|
+
templateId: string,
|
|
314
|
+
): Dictionary<string> => {
|
|
315
|
+
const template: WorkflowTemplate | null = getWorkflowTemplate(templateId);
|
|
316
|
+
|
|
317
|
+
if (!template) {
|
|
318
|
+
throw new Error(`There is no template ${templateId}.`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const values: Dictionary<string> = {};
|
|
322
|
+
|
|
323
|
+
for (const variable of template.variables) {
|
|
324
|
+
const value: string | undefined = VARIABLE_VALUES[variable.name];
|
|
325
|
+
|
|
326
|
+
if (value === undefined) {
|
|
327
|
+
throw new Error(`The simulation has no value for ${variable.name}.`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
values[variable.name] = value;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return values;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/* -------------------------- Argument resolution -------------------------- */
|
|
337
|
+
|
|
338
|
+
/** RunWorkflow's StorageMap, which lives in the App package this one cannot import. */
|
|
339
|
+
interface StorageMap {
|
|
340
|
+
local: {
|
|
341
|
+
variables: Dictionary<string>;
|
|
342
|
+
components: Dictionary<{ returnValues: JSONObject }>;
|
|
343
|
+
};
|
|
344
|
+
global: {
|
|
345
|
+
variables: Dictionary<string>;
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const PARSED_ARGUMENT_TYPES: Array<ComponentInputType> = [
|
|
350
|
+
ComponentInputType.JSON,
|
|
351
|
+
ComponentInputType.Query,
|
|
352
|
+
ComponentInputType.Select,
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
type ResolveArgumentsFunction = (
|
|
356
|
+
storageMap: StorageMap,
|
|
357
|
+
node: SimulatedNode,
|
|
358
|
+
) => JSONObject;
|
|
359
|
+
|
|
360
|
+
/*
|
|
361
|
+
* A copy of RunWorkflow.getComponentArguments
|
|
362
|
+
* (packages/App/FeatureSet/Workflow/Services/RunWorkflow.ts, around line
|
|
363
|
+
* 1210), kept step for step, because every property this file checks rests on
|
|
364
|
+
* it: falsy values are skipped; VMAPI.replaceValueInPlace substitutes, escaping
|
|
365
|
+
* for JSON only when the argument is JSON typed (an object-valued argument is
|
|
366
|
+
* stringified, substituted with escaping and parsed back); and a string left
|
|
367
|
+
* for a JSON, Query or Select argument is parsed, with a template that built
|
|
368
|
+
* invalid JSON failing the run — and this test — the way it fails the runner.
|
|
369
|
+
*/
|
|
370
|
+
const resolveArguments: ResolveArgumentsFunction = (
|
|
371
|
+
storageMap: StorageMap,
|
|
372
|
+
node: SimulatedNode,
|
|
373
|
+
): JSONObject => {
|
|
374
|
+
const resolved: JSONObject = {};
|
|
375
|
+
|
|
376
|
+
for (const argument of node.metadata.arguments as Array<Argument>) {
|
|
377
|
+
const content: JSONValue | undefined = node.arguments[argument.id];
|
|
378
|
+
|
|
379
|
+
if (!content) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let value: JSONValue = VMUtil.replaceValueInPlace(
|
|
384
|
+
storageMap as unknown as JSONObject,
|
|
385
|
+
content as string,
|
|
386
|
+
argument.type === ComponentInputType.JSON,
|
|
387
|
+
) as JSONValue;
|
|
388
|
+
|
|
389
|
+
if (
|
|
390
|
+
typeof value === "string" &&
|
|
391
|
+
PARSED_ARGUMENT_TYPES.includes(argument.type)
|
|
392
|
+
) {
|
|
393
|
+
try {
|
|
394
|
+
value = JSON.parse(value) as JSONValue;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
argument.isSensitive
|
|
398
|
+
? `Invalid JSON provided for sensitive argument ${argument.id} of ${node.componentId}. The value has been redacted.`
|
|
399
|
+
: `Invalid JSON provided for argument ${argument.id} of ${node.componentId}. JSON parse error: ${(error as Error).message}. JSON: ${value}`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
resolved[argument.id] = value;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return resolved;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
/* ------------------------------ The fakes ------------------------------ */
|
|
411
|
+
|
|
412
|
+
interface JiraRequest {
|
|
413
|
+
method: string;
|
|
414
|
+
url: string;
|
|
415
|
+
headers: Dictionary<string>;
|
|
416
|
+
body?: JSONValue | undefined;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
interface JiraReply {
|
|
420
|
+
status: number;
|
|
421
|
+
/** A string is a body that is not JSON: a 204's empty one, or a plain-text 401. */
|
|
422
|
+
body: JSONObject | Array<JSONObject> | string;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
type FakeJiraFunction = (request: JiraRequest) => JiraReply;
|
|
426
|
+
|
|
427
|
+
interface DatabaseCall {
|
|
428
|
+
componentId: string;
|
|
429
|
+
metadataId: string;
|
|
430
|
+
args: JSONObject;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
interface DatabaseReply {
|
|
434
|
+
port: "success" | "error";
|
|
435
|
+
returnValues: JSONObject;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
type DatabaseAnswer = DatabaseReply | ((call: DatabaseCall) => DatabaseReply);
|
|
439
|
+
|
|
440
|
+
/** Keyed by the step's metadata id, e.g. "incident-find-one" or "alert-find-one". */
|
|
441
|
+
type FakeDatabase = Dictionary<DatabaseAnswer>;
|
|
442
|
+
|
|
443
|
+
type EditCodeFunction = (code: string) => string;
|
|
444
|
+
|
|
445
|
+
interface Scenario {
|
|
446
|
+
templateId: string;
|
|
447
|
+
/** What the trigger hands over: { model } from a database trigger, the delivery from the webhook. */
|
|
448
|
+
trigger: JSONObject;
|
|
449
|
+
jira?: FakeJiraFunction | undefined;
|
|
450
|
+
database?: FakeDatabase | undefined;
|
|
451
|
+
/*
|
|
452
|
+
* Edits a step's script before the run, keyed by component id — what a
|
|
453
|
+
* user following the script's own comments would do, such as switching
|
|
454
|
+
* SYNC_PRIVATE_INCIDENTS or SYNC_PRIVATE_ALERTS on.
|
|
455
|
+
*/
|
|
456
|
+
editCode?: Dictionary<EditCodeFunction> | undefined;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
interface SimulationTrace {
|
|
460
|
+
/** The template the run was built from. */
|
|
461
|
+
templateId: string;
|
|
462
|
+
/** Component ids, in the order they ran. */
|
|
463
|
+
executed: Array<string>;
|
|
464
|
+
/** The port each step left by. */
|
|
465
|
+
ports: Dictionary<string | null>;
|
|
466
|
+
requests: Array<JiraRequest>;
|
|
467
|
+
databaseCalls: Array<DatabaseCall>;
|
|
468
|
+
/** The resolved value of every Log step. */
|
|
469
|
+
logs: Array<string>;
|
|
470
|
+
/** What the components themselves wrote to the run log. */
|
|
471
|
+
runLog: Array<string>;
|
|
472
|
+
storage: StorageMap;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/*
|
|
476
|
+
* Jira as the network sees it. An unexpected request is answered with a 404
|
|
477
|
+
* rather than thrown, because a throw inside API.fetch is caught by the API
|
|
478
|
+
* component and would look like an ordinary failure; every test asserts the
|
|
479
|
+
* exact list of requests instead, so a stray one shows up there.
|
|
480
|
+
*/
|
|
481
|
+
type JiraRoutesFunction = (routes: Dictionary<JiraReply>) => FakeJiraFunction;
|
|
482
|
+
|
|
483
|
+
const jiraRoutes: JiraRoutesFunction = (
|
|
484
|
+
routes: Dictionary<JiraReply>,
|
|
485
|
+
): FakeJiraFunction => {
|
|
486
|
+
return (request: JiraRequest): JiraReply => {
|
|
487
|
+
const key: string = `${request.method} ${request.url.replace(JIRA_SITE, "")}`;
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
routes[key] || {
|
|
491
|
+
status: 404,
|
|
492
|
+
body: {
|
|
493
|
+
errorMessages: [`The simulation has no reply for ${key}.`],
|
|
494
|
+
errors: {},
|
|
495
|
+
},
|
|
496
|
+
}
|
|
497
|
+
);
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const NO_JIRA: FakeJiraFunction = jiraRoutes({});
|
|
502
|
+
|
|
503
|
+
type ReplyFunction = (body: JSONObject) => JiraReply;
|
|
504
|
+
|
|
505
|
+
const ok: ReplyFunction = (body: JSONObject): JiraReply => {
|
|
506
|
+
return { status: 200, body: body };
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const createdReply: ReplyFunction = (body: JSONObject): JiraReply => {
|
|
510
|
+
return { status: 201, body: body };
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const badRequest: ReplyFunction = (body: JSONObject): JiraReply => {
|
|
514
|
+
return { status: 400, body: body };
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
/** Transitions, edits and label changes answer 204 with no body at all. */
|
|
518
|
+
const NO_CONTENT: JiraReply = { status: 204, body: "" };
|
|
519
|
+
|
|
520
|
+
/** POST /rest/api/3/issue answers with the new issue's id, key and URL, nothing else. */
|
|
521
|
+
const ISSUE_CREATED: JiraReply = createdReply({
|
|
522
|
+
id: JIRA_ISSUE_ID,
|
|
523
|
+
key: JIRA_ISSUE_KEY,
|
|
524
|
+
self: `${JIRA_SITE}/rest/api/3/issue/${JIRA_ISSUE_ID}`,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const COMMENT_CREATED: JiraReply = createdReply({
|
|
528
|
+
self: `${JIRA_SITE}/rest/api/3/issue/${JIRA_ISSUE_ID}/comment/10020`,
|
|
529
|
+
id: "10020",
|
|
530
|
+
created: "2026-09-23T10:12:00.000+0100",
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const SEARCH: string = "POST /rest/api/3/search/jql";
|
|
534
|
+
const CREATE_ISSUE: string = "POST /rest/api/3/issue";
|
|
535
|
+
const TRANSITION: string = `POST /rest/api/3/issue/${JIRA_ISSUE_KEY}/transitions`;
|
|
536
|
+
const COMMENT: string = `POST /rest/api/3/issue/${JIRA_ISSUE_KEY}/comment`;
|
|
537
|
+
const EDIT_ISSUE: string = `PUT /rest/api/3/issue/${JIRA_ISSUE_KEY}`;
|
|
538
|
+
const GET_LABELS: string = `GET /rest/api/3/issue/${JIRA_ISSUE_KEY}?fields=labels`;
|
|
539
|
+
|
|
540
|
+
type ModelReplyFunction = (model: JSONObject | null) => DatabaseReply;
|
|
541
|
+
|
|
542
|
+
/* Find One answers "nothing matched" with a null model on its Success port, not an error. */
|
|
543
|
+
const found: ModelReplyFunction = (model: JSONObject | null): DatabaseReply => {
|
|
544
|
+
return { port: "success", returnValues: { model: model } };
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
type ModelsReplyFunction = (models: Array<JSONObject>) => DatabaseReply;
|
|
548
|
+
|
|
549
|
+
const many: ModelsReplyFunction = (
|
|
550
|
+
models: Array<JSONObject>,
|
|
551
|
+
): DatabaseReply => {
|
|
552
|
+
return { port: "success", returnValues: { models: models } };
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
type CreatedWithIdFunction = (id: string) => DatabaseAnswer;
|
|
556
|
+
|
|
557
|
+
/* Create One hands back the saved row, which is the posted fields plus the id the database gave it. */
|
|
558
|
+
const createdWithId: CreatedWithIdFunction = (id: string): DatabaseAnswer => {
|
|
559
|
+
return (call: DatabaseCall): DatabaseReply => {
|
|
560
|
+
return {
|
|
561
|
+
port: "success",
|
|
562
|
+
returnValues: {
|
|
563
|
+
model: { ...(call.args["json"] as JSONObject), _id: id },
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
};
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
/* A failed database step takes its Error port with nothing in its return values; the error itself goes to the run log. */
|
|
570
|
+
const DATABASE_FAILURE: DatabaseReply = { port: "error", returnValues: {} };
|
|
571
|
+
|
|
572
|
+
/* ------------------------------ The runner ------------------------------ */
|
|
573
|
+
|
|
574
|
+
/* Components the simulation runs for real, the way RunWorkflow's Components registry does. */
|
|
575
|
+
const COMPONENT_CODE: Dictionary<ComponentCode> = {
|
|
576
|
+
[ComponentID.JavaScriptCode]: new JavaScriptCode(),
|
|
577
|
+
[ComponentID.IfElse]: new IfElse(),
|
|
578
|
+
[ComponentID.Log]: new Log(),
|
|
579
|
+
[ComponentID.ApiGet]: new ApiGet(),
|
|
580
|
+
[ComponentID.ApiPost]: new ApiPost(),
|
|
581
|
+
[ComponentID.ApiPut]: new ApiPut(),
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const DATABASE_OPERATION: RegExp = /-(find-one|find-many|create-one)$/;
|
|
585
|
+
|
|
586
|
+
/** Enough of a jest spy to put the real function back. */
|
|
587
|
+
interface RestorableSpy {
|
|
588
|
+
mockRestore: () => void;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
type AnswerFetchFunction = (
|
|
592
|
+
options: APIFetchOptions,
|
|
593
|
+
scenario: Scenario,
|
|
594
|
+
trace: SimulationTrace,
|
|
595
|
+
) => HTTPResponse<JSONObject> | HTTPErrorResponse;
|
|
596
|
+
|
|
597
|
+
/*
|
|
598
|
+
* What API.fetch resolves with on the server. Axios accepts 2xx only, and the
|
|
599
|
+
* API components turn redirects off, so anything else becomes the
|
|
600
|
+
* HTTPErrorResponse the API utility resolves with (never throws). A 204's
|
|
601
|
+
* empty body reaches HTTPResponse as an empty string, which it stores as
|
|
602
|
+
* { data: "" } — both come from the real classes here, not from a copy.
|
|
603
|
+
*/
|
|
604
|
+
const answerFetch: AnswerFetchFunction = (
|
|
605
|
+
options: APIFetchOptions,
|
|
606
|
+
scenario: Scenario,
|
|
607
|
+
trace: SimulationTrace,
|
|
608
|
+
): HTTPResponse<JSONObject> | HTTPErrorResponse => {
|
|
609
|
+
const request: JiraRequest = {
|
|
610
|
+
method: options.method,
|
|
611
|
+
url: options.url.toString(),
|
|
612
|
+
headers: { ...(options.headers || {}) },
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// Axios would send JSON.stringify(data); a JSON round trip is what arrives.
|
|
616
|
+
if (options.data !== undefined) {
|
|
617
|
+
request.body = JSON.parse(JSON.stringify(options.data)) as JSONValue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
trace.requests.push(request);
|
|
621
|
+
|
|
622
|
+
const reply: JiraReply = (scenario.jira || NO_JIRA)(request);
|
|
623
|
+
const headers: Dictionary<string> = {
|
|
624
|
+
"content-type":
|
|
625
|
+
typeof reply.body === "string"
|
|
626
|
+
? "text/plain;charset=UTF-8"
|
|
627
|
+
: "application/json;charset=UTF-8",
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
if (reply.status >= 200 && reply.status < 300) {
|
|
631
|
+
return new HTTPResponse<JSONObject>(
|
|
632
|
+
reply.status,
|
|
633
|
+
reply.body as JSONObject,
|
|
634
|
+
headers,
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return new HTTPErrorResponse(reply.status, reply.body as JSONObject, headers);
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
interface StepResult {
|
|
642
|
+
returnValues: JSONObject;
|
|
643
|
+
port: string | null;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
type ExecuteStepFunction = (data: {
|
|
647
|
+
node: SimulatedNode;
|
|
648
|
+
args: JSONObject;
|
|
649
|
+
scenario: Scenario;
|
|
650
|
+
trace: SimulationTrace;
|
|
651
|
+
}) => Promise<StepResult>;
|
|
652
|
+
|
|
653
|
+
const executeStep: ExecuteStepFunction = async (data: {
|
|
654
|
+
node: SimulatedNode;
|
|
655
|
+
args: JSONObject;
|
|
656
|
+
scenario: Scenario;
|
|
657
|
+
trace: SimulationTrace;
|
|
658
|
+
}): Promise<StepResult> => {
|
|
659
|
+
const node: SimulatedNode = data.node;
|
|
660
|
+
|
|
661
|
+
if (node.componentType === ComponentType.Trigger) {
|
|
662
|
+
/*
|
|
663
|
+
* A trigger hands over what the event carried: the webhook component
|
|
664
|
+
* returns the request it received, and a database trigger the record.
|
|
665
|
+
* Both leave by their only port ("out" and "success" respectively).
|
|
666
|
+
*/
|
|
667
|
+
if (node.metadata.outPorts.length !== 1) {
|
|
668
|
+
throw new Error(`${node.metadataId} has more than one way out.`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
returnValues: data.scenario.trigger,
|
|
673
|
+
port: node.metadata.outPorts[0]!.id,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (DATABASE_OPERATION.test(node.metadataId)) {
|
|
678
|
+
const call: DatabaseCall = {
|
|
679
|
+
componentId: node.componentId,
|
|
680
|
+
metadataId: node.metadataId,
|
|
681
|
+
args: data.args,
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
data.trace.databaseCalls.push(call);
|
|
685
|
+
|
|
686
|
+
const answer: DatabaseAnswer | undefined =
|
|
687
|
+
data.scenario.database?.[node.metadataId];
|
|
688
|
+
|
|
689
|
+
// Unlike Jira, the database is called by the simulation itself, so a surprise can fail loudly.
|
|
690
|
+
if (!answer) {
|
|
691
|
+
throw new Error(
|
|
692
|
+
`The scenario did not expect ${node.componentId} (${node.metadataId}) to run. It was called with ${JSON.stringify(data.args)}`,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const reply: DatabaseReply =
|
|
697
|
+
typeof answer === "function" ? answer(call) : answer;
|
|
698
|
+
|
|
699
|
+
return { returnValues: reply.returnValues, port: reply.port };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const code: ComponentCode | undefined = COMPONENT_CODE[node.metadataId];
|
|
703
|
+
|
|
704
|
+
if (!code) {
|
|
705
|
+
throw new Error(`The simulation cannot run ${node.metadataId}.`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (node.metadataId === ComponentID.Log) {
|
|
709
|
+
data.trace.logs.push(String(data.args["value"]));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
let errorMessage: string | null = null;
|
|
713
|
+
|
|
714
|
+
const options: RunOptions = {
|
|
715
|
+
log: (item: JSONValue | Error): void => {
|
|
716
|
+
data.trace.runLog.push(
|
|
717
|
+
typeof item === "string" ? item : JSON.stringify(item),
|
|
718
|
+
);
|
|
719
|
+
},
|
|
720
|
+
workflowLogId: ObjectID.generate(),
|
|
721
|
+
workflowId: ObjectID.generate(),
|
|
722
|
+
projectId: new ObjectID(PROJECT_ID),
|
|
723
|
+
// RunWorkflow stops the whole run once a component reports an error this way.
|
|
724
|
+
onError: (exception: Exception): Exception => {
|
|
725
|
+
errorMessage = exception.message;
|
|
726
|
+
return exception;
|
|
727
|
+
},
|
|
728
|
+
executeWorkflow: async (): Promise<void> => {},
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
const result: RunReturnType = await code.run(data.args, options);
|
|
732
|
+
|
|
733
|
+
if (errorMessage !== null) {
|
|
734
|
+
throw new Error(
|
|
735
|
+
`Workflow stopped because of an error in ${node.componentId}: ${errorMessage}`,
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return {
|
|
740
|
+
returnValues: result.returnValues,
|
|
741
|
+
port: result.executePort?.id || null,
|
|
742
|
+
};
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
type RunTemplateFunction = (scenario: Scenario) => Promise<SimulationTrace>;
|
|
746
|
+
|
|
747
|
+
/*
|
|
748
|
+
* The loop of RunWorkflow.runWorkflow in miniature: a FIFO of pending steps,
|
|
749
|
+
* each run once (a second visit is the cycle error the runner raises), its
|
|
750
|
+
* return values stored under local.components.<component id>, and every step
|
|
751
|
+
* wired to the port it left by queued unless it already is.
|
|
752
|
+
*/
|
|
753
|
+
const runTemplate: RunTemplateFunction = async (
|
|
754
|
+
scenario: Scenario,
|
|
755
|
+
): Promise<SimulationTrace> => {
|
|
756
|
+
const graph: SimulatedGraph = buildSimulatedGraph(scenario.templateId);
|
|
757
|
+
|
|
758
|
+
for (const componentId of Object.keys(scenario.editCode || {})) {
|
|
759
|
+
const node: SimulatedNode | undefined = graph.nodes[componentId];
|
|
760
|
+
const edit: EditCodeFunction = (
|
|
761
|
+
scenario.editCode as Dictionary<EditCodeFunction>
|
|
762
|
+
)[componentId] as EditCodeFunction;
|
|
763
|
+
|
|
764
|
+
if (!node || typeof node.arguments["code"] !== "string") {
|
|
765
|
+
throw new Error(`${scenario.templateId} has no script ${componentId}.`);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const edited: string = edit(node.arguments["code"]);
|
|
769
|
+
|
|
770
|
+
// An edit that matched nothing would leave the test proving the unedited script.
|
|
771
|
+
if (edited === node.arguments["code"]) {
|
|
772
|
+
throw new Error(`The edit did not change ${componentId}'s script.`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
node.arguments["code"] = edited;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const trace: SimulationTrace = {
|
|
779
|
+
templateId: scenario.templateId,
|
|
780
|
+
executed: [],
|
|
781
|
+
ports: {},
|
|
782
|
+
requests: [],
|
|
783
|
+
databaseCalls: [],
|
|
784
|
+
logs: [],
|
|
785
|
+
runLog: [],
|
|
786
|
+
storage: {
|
|
787
|
+
local: { variables: variablesFor(scenario.templateId), components: {} },
|
|
788
|
+
global: { variables: {} },
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const fetchSpy: RestorableSpy = jest
|
|
793
|
+
.spyOn(API, "fetch")
|
|
794
|
+
.mockImplementation((async (
|
|
795
|
+
options: APIFetchOptions,
|
|
796
|
+
): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> => {
|
|
797
|
+
return answerFetch(options, scenario, trace);
|
|
798
|
+
}) as never) as unknown as RestorableSpy;
|
|
799
|
+
|
|
800
|
+
try {
|
|
801
|
+
const pending: Array<string> = [graph.triggerComponentId];
|
|
802
|
+
|
|
803
|
+
while (pending.length > 0) {
|
|
804
|
+
const componentId: string = pending.shift() as string;
|
|
805
|
+
|
|
806
|
+
if (trace.executed.includes(componentId)) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
`Cyclic Workflow Detected. Cannot execute ${componentId} when it has already been executed.`,
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
trace.executed.push(componentId);
|
|
813
|
+
|
|
814
|
+
const node: SimulatedNode = graph.nodes[componentId] as SimulatedNode;
|
|
815
|
+
const args: JSONObject = resolveArguments(trace.storage, node);
|
|
816
|
+
const result: StepResult = await executeStep({
|
|
817
|
+
node: node,
|
|
818
|
+
args: args,
|
|
819
|
+
scenario: scenario,
|
|
820
|
+
trace: trace,
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
trace.storage.local.components[componentId] = {
|
|
824
|
+
returnValues: result.returnValues,
|
|
825
|
+
};
|
|
826
|
+
trace.ports[componentId] = result.port;
|
|
827
|
+
|
|
828
|
+
if (!result.port) {
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const next: Array<string> =
|
|
833
|
+
(graph.outPorts[componentId] as Dictionary<Array<string>>)[
|
|
834
|
+
result.port
|
|
835
|
+
] || [];
|
|
836
|
+
|
|
837
|
+
for (const nextComponentId of next) {
|
|
838
|
+
if (!pending.includes(nextComponentId)) {
|
|
839
|
+
pending.push(nextComponentId);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
} finally {
|
|
844
|
+
fetchSpy.mockRestore();
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return trace;
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
/*
|
|
851
|
+
* The API components resolve the Jira host before every request, to refuse
|
|
852
|
+
* internal addresses. Pin the answer to a public one so the suite stays
|
|
853
|
+
* offline; which addresses are refused is ApiComponentSsrf.test.ts's concern.
|
|
854
|
+
*/
|
|
855
|
+
beforeAll(() => {
|
|
856
|
+
jest
|
|
857
|
+
.spyOn(dns.promises, "lookup")
|
|
858
|
+
.mockResolvedValue([{ address: "104.192.142.20", family: 4 }] as never);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
afterAll(() => {
|
|
862
|
+
jest.restoreAllMocks();
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
/* ------------------------------ The two kinds ------------------------------ */
|
|
866
|
+
|
|
867
|
+
/*
|
|
868
|
+
* What a scenario needs to know about the record a template syncs. The
|
|
869
|
+
* templates build both of their versions from a table of their own; this one
|
|
870
|
+
* is written out value by value rather than read from it, so a wrong entry
|
|
871
|
+
* there fails here instead of being copied into the expectation.
|
|
872
|
+
*/
|
|
873
|
+
|
|
874
|
+
/** Everything a scenario may set on the record a database trigger hands over. */
|
|
875
|
+
interface RecordModelProps {
|
|
876
|
+
_id?: string | undefined;
|
|
877
|
+
title?: string | undefined;
|
|
878
|
+
description?: string | null | undefined;
|
|
879
|
+
customFields?: JSONObject | null | undefined;
|
|
880
|
+
state?: JSONObject | undefined;
|
|
881
|
+
severity?: string | undefined;
|
|
882
|
+
rootCause?: string | null | undefined;
|
|
883
|
+
remediationNotes?: string | null | undefined;
|
|
884
|
+
isPrivate?: boolean | undefined;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
type RecordModelFunction = (props?: RecordModelProps) => JSONObject;
|
|
888
|
+
|
|
889
|
+
interface RecordNoteProps {
|
|
890
|
+
note?: string | undefined;
|
|
891
|
+
/** null leaves the author off, as on a note a workflow wrote. */
|
|
892
|
+
authorName?: string | null | undefined;
|
|
893
|
+
recordId?: string | undefined;
|
|
894
|
+
/** Whether the record the note is on is private. */
|
|
895
|
+
isPrivate?: boolean | undefined;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
type RecordNoteFunction = (props?: RecordNoteProps) => JSONObject;
|
|
899
|
+
|
|
900
|
+
type RecordLabelFunction = (recordId?: string) => string;
|
|
901
|
+
|
|
902
|
+
interface NoteTemplate {
|
|
903
|
+
templateId: string;
|
|
904
|
+
/** "private" or "public": what the comment calls the note. */
|
|
905
|
+
visibility: string;
|
|
906
|
+
/** Whether Jira Service Management is told to keep the comment from the customer. */
|
|
907
|
+
internal: boolean;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
interface SeverityRef {
|
|
911
|
+
id: string;
|
|
912
|
+
name: string;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
interface KindTemplates {
|
|
916
|
+
createIssue: string;
|
|
917
|
+
transition: string;
|
|
918
|
+
privateNote: string;
|
|
919
|
+
updateComment: string;
|
|
920
|
+
createFromIssue: string;
|
|
921
|
+
statusToState: string;
|
|
922
|
+
commentToNote: string;
|
|
923
|
+
issueChangesToNote: string;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** The component ids that carry the record's noun. The rest are the same for both kinds. */
|
|
927
|
+
interface KindSteps {
|
|
928
|
+
onCreate: string;
|
|
929
|
+
onUpdate: string;
|
|
930
|
+
prepareRecord: string;
|
|
931
|
+
ifCreate: string;
|
|
932
|
+
createRecord: string;
|
|
933
|
+
find: string;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** The database components, by what they do. */
|
|
937
|
+
interface KindComponents {
|
|
938
|
+
findOne: string;
|
|
939
|
+
stateFindMany: string;
|
|
940
|
+
timelineCreateOne: string;
|
|
941
|
+
noteCreateOne: string;
|
|
942
|
+
severityFindMany: string;
|
|
943
|
+
createOne: string;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
interface SimulatedKind {
|
|
947
|
+
/** "incident" or "alert". Both take "an", which the test titles rely on. */
|
|
948
|
+
noun: string;
|
|
949
|
+
id: string;
|
|
950
|
+
/** A record of the same kind that this project does not have. */
|
|
951
|
+
otherId: string;
|
|
952
|
+
/** The id the database gives the record the create-from-Jira template writes. */
|
|
953
|
+
newId: string;
|
|
954
|
+
number: string;
|
|
955
|
+
label: RecordLabelFunction;
|
|
956
|
+
labelPrefix: string;
|
|
957
|
+
numberField: string;
|
|
958
|
+
severityRelation: string;
|
|
959
|
+
severityIdColumn: string;
|
|
960
|
+
stateRelation: string;
|
|
961
|
+
stateIdColumn: string;
|
|
962
|
+
/** The column a note or a state-timeline row names its record by. */
|
|
963
|
+
idColumn: string;
|
|
964
|
+
/** The column a state-timeline row names its new state by. */
|
|
965
|
+
timelineStateColumn: string;
|
|
966
|
+
/** The dashboard path the record's page lives under. */
|
|
967
|
+
dashboardPath: string;
|
|
968
|
+
/** An incident is declared; an alert is created. */
|
|
969
|
+
created: string;
|
|
970
|
+
Created: string;
|
|
971
|
+
create: string;
|
|
972
|
+
templates: KindTemplates;
|
|
973
|
+
/** Every OneUptime note that goes to Jira: private and public for incidents, private for alerts. */
|
|
974
|
+
noteTemplates: Array<NoteTemplate>;
|
|
975
|
+
steps: KindSteps;
|
|
976
|
+
components: KindComponents;
|
|
977
|
+
createdState: JSONObject;
|
|
978
|
+
acknowledgedState: JSONObject;
|
|
979
|
+
resolvedState: JSONObject;
|
|
980
|
+
states: Array<JSONObject>;
|
|
981
|
+
severities: Array<JSONObject>;
|
|
982
|
+
mostSevere: SeverityRef;
|
|
983
|
+
/** Where a Medium priority lands: the middle of three, or the less severe of two. */
|
|
984
|
+
middleSeverity: SeverityRef;
|
|
985
|
+
leastSevere: SeverityRef;
|
|
986
|
+
/** Written beside the essentials when a record is created from a Jira issue. */
|
|
987
|
+
quietCreateFields: JSONObject;
|
|
988
|
+
/** The updates the edit-comment template listens for. */
|
|
989
|
+
editListenOn: JSONObject;
|
|
990
|
+
/** The fixture record's own values, as its comments and issues quote them. */
|
|
991
|
+
defaultTitle: string;
|
|
992
|
+
defaultDescription: string;
|
|
993
|
+
defaultSeverityName: string;
|
|
994
|
+
defaultNoteText: string;
|
|
995
|
+
model: RecordModelFunction;
|
|
996
|
+
note: RecordNoteFunction;
|
|
997
|
+
/** The switch every script that syncs a private record documents. */
|
|
998
|
+
syncPrivate: EditCodeFunction;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
type SyncPrivateSwitchFunction = (constant: string) => EditCodeFunction;
|
|
1002
|
+
|
|
1003
|
+
const syncPrivateSwitch: SyncPrivateSwitchFunction = (
|
|
1004
|
+
constant: string,
|
|
1005
|
+
): EditCodeFunction => {
|
|
1006
|
+
return (code: string): string => {
|
|
1007
|
+
return code.replace(
|
|
1008
|
+
`const ${constant} = false;`,
|
|
1009
|
+
`const ${constant} = true;`,
|
|
1010
|
+
);
|
|
1011
|
+
};
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
/*
|
|
1015
|
+
* The incident fixture leaves isPrivate off, and the triggers select it, so a
|
|
1016
|
+
* scenario that sets it gets it added here.
|
|
1017
|
+
*/
|
|
1018
|
+
const incidentRecordModel: RecordModelFunction = (
|
|
1019
|
+
props?: RecordModelProps,
|
|
1020
|
+
): JSONObject => {
|
|
1021
|
+
const model: JSONObject = incidentModel(props);
|
|
1022
|
+
|
|
1023
|
+
if (props?.isPrivate !== undefined) {
|
|
1024
|
+
model["isPrivate"] = props.isPrivate;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
return model;
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
/* A note on an incident; the note triggers reach through to the incident for its Private flag. */
|
|
1031
|
+
const incidentRecordNote: RecordNoteFunction = (
|
|
1032
|
+
props?: RecordNoteProps,
|
|
1033
|
+
): JSONObject => {
|
|
1034
|
+
const note: JSONObject = noteModel({
|
|
1035
|
+
note: props?.note,
|
|
1036
|
+
authorName: props?.authorName,
|
|
1037
|
+
incidentId: props?.recordId,
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
if (props?.isPrivate !== undefined) {
|
|
1041
|
+
note["incident"] = {
|
|
1042
|
+
...(note["incident"] as JSONObject),
|
|
1043
|
+
isPrivate: props.isPrivate,
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
return note;
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
const alertRecordNote: RecordNoteFunction = (
|
|
1051
|
+
props?: RecordNoteProps,
|
|
1052
|
+
): JSONObject => {
|
|
1053
|
+
return alertNoteModel({
|
|
1054
|
+
note: props?.note,
|
|
1055
|
+
authorName: props?.authorName,
|
|
1056
|
+
alertId: props?.recordId,
|
|
1057
|
+
isPrivate: props?.isPrivate,
|
|
1058
|
+
});
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
const INCIDENT: SimulatedKind = {
|
|
1062
|
+
noun: "incident",
|
|
1063
|
+
id: INCIDENT_ID,
|
|
1064
|
+
otherId: OTHER_INCIDENT_ID,
|
|
1065
|
+
newId: NEW_INCIDENT_ID,
|
|
1066
|
+
number: INCIDENT_NUMBER,
|
|
1067
|
+
label: incidentLabel,
|
|
1068
|
+
labelPrefix: "oneuptime-incident-",
|
|
1069
|
+
numberField: "incidentNumberWithPrefix",
|
|
1070
|
+
severityRelation: "incidentSeverity",
|
|
1071
|
+
severityIdColumn: "incidentSeverityId",
|
|
1072
|
+
stateRelation: "currentIncidentState",
|
|
1073
|
+
stateIdColumn: "currentIncidentStateId",
|
|
1074
|
+
idColumn: "incidentId",
|
|
1075
|
+
timelineStateColumn: "incidentStateId",
|
|
1076
|
+
dashboardPath: "incidents",
|
|
1077
|
+
created: "declared",
|
|
1078
|
+
Created: "Declared",
|
|
1079
|
+
create: "declare",
|
|
1080
|
+
templates: {
|
|
1081
|
+
createIssue: "jira-create-issue-for-incident",
|
|
1082
|
+
transition: "jira-transition-issue-on-incident-state",
|
|
1083
|
+
privateNote: "jira-comment-from-incident-private-note",
|
|
1084
|
+
updateComment: "jira-comment-on-incident-update",
|
|
1085
|
+
createFromIssue: "jira-declare-incident-from-issue",
|
|
1086
|
+
statusToState: "jira-status-to-incident-state",
|
|
1087
|
+
commentToNote: "jira-comment-to-incident-private-note",
|
|
1088
|
+
issueChangesToNote: "jira-issue-changes-to-incident-private-note",
|
|
1089
|
+
},
|
|
1090
|
+
noteTemplates: [
|
|
1091
|
+
{
|
|
1092
|
+
templateId: "jira-comment-from-incident-private-note",
|
|
1093
|
+
visibility: "private",
|
|
1094
|
+
internal: true,
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
templateId: "jira-comment-from-incident-public-note",
|
|
1098
|
+
visibility: "public",
|
|
1099
|
+
internal: false,
|
|
1100
|
+
},
|
|
1101
|
+
],
|
|
1102
|
+
steps: {
|
|
1103
|
+
onCreate: "incident-on-create-1",
|
|
1104
|
+
onUpdate: "incident-on-update-1",
|
|
1105
|
+
prepareRecord: "prepare-incident-1",
|
|
1106
|
+
ifCreate: "if-declare-1",
|
|
1107
|
+
createRecord: "create-incident-1",
|
|
1108
|
+
find: "find-incident-1",
|
|
1109
|
+
},
|
|
1110
|
+
components: {
|
|
1111
|
+
findOne: "incident-find-one",
|
|
1112
|
+
stateFindMany: "incident-state-find-many",
|
|
1113
|
+
timelineCreateOne: "incident-state-timeline-create-one",
|
|
1114
|
+
noteCreateOne: "incident-internal-note-create-one",
|
|
1115
|
+
severityFindMany: "incident-severity-find-many",
|
|
1116
|
+
createOne: "incident-create-one",
|
|
1117
|
+
},
|
|
1118
|
+
createdState: CREATED_STATE,
|
|
1119
|
+
acknowledgedState: ACKNOWLEDGED_STATE,
|
|
1120
|
+
resolvedState: RESOLVED_STATE,
|
|
1121
|
+
states: INCIDENT_STATES,
|
|
1122
|
+
severities: INCIDENT_SEVERITIES,
|
|
1123
|
+
mostSevere: { id: CRITICAL_SEVERITY_ID, name: "Critical Incident" },
|
|
1124
|
+
middleSeverity: { id: MAJOR_SEVERITY_ID, name: "Major Incident" },
|
|
1125
|
+
leastSevere: { id: MINOR_SEVERITY_ID, name: "Minor Incident" },
|
|
1126
|
+
// An issue's text was not written for customers, so it stays off status pages.
|
|
1127
|
+
quietCreateFields: {
|
|
1128
|
+
isVisibleOnStatusPage: false,
|
|
1129
|
+
shouldStatusPageSubscribersBeNotifiedOnIncidentCreated: false,
|
|
1130
|
+
},
|
|
1131
|
+
editListenOn: {
|
|
1132
|
+
title: true,
|
|
1133
|
+
description: true,
|
|
1134
|
+
incidentSeverityId: true,
|
|
1135
|
+
incidentSeverity: true,
|
|
1136
|
+
rootCause: true,
|
|
1137
|
+
remediationNotes: true,
|
|
1138
|
+
},
|
|
1139
|
+
defaultTitle: "Checkout latency high",
|
|
1140
|
+
defaultDescription:
|
|
1141
|
+
'p99 latency on /checkout is above 2s.\nStarted after the "v4.2" deploy.',
|
|
1142
|
+
defaultSeverityName: "Critical Incident",
|
|
1143
|
+
defaultNoteText:
|
|
1144
|
+
"Failed over to the **secondary** database. Error rate is dropping.",
|
|
1145
|
+
model: incidentRecordModel,
|
|
1146
|
+
note: incidentRecordNote,
|
|
1147
|
+
syncPrivate: syncPrivateSwitch("SYNC_PRIVATE_INCIDENTS"),
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
const ALERT: SimulatedKind = {
|
|
1151
|
+
noun: "alert",
|
|
1152
|
+
id: ALERT_ID,
|
|
1153
|
+
otherId: OTHER_ALERT_ID,
|
|
1154
|
+
newId: NEW_ALERT_ID,
|
|
1155
|
+
number: ALERT_NUMBER,
|
|
1156
|
+
label: alertLabel,
|
|
1157
|
+
labelPrefix: "oneuptime-alert-",
|
|
1158
|
+
numberField: "alertNumberWithPrefix",
|
|
1159
|
+
severityRelation: "alertSeverity",
|
|
1160
|
+
severityIdColumn: "alertSeverityId",
|
|
1161
|
+
stateRelation: "currentAlertState",
|
|
1162
|
+
stateIdColumn: "currentAlertStateId",
|
|
1163
|
+
idColumn: "alertId",
|
|
1164
|
+
timelineStateColumn: "alertStateId",
|
|
1165
|
+
dashboardPath: "alerts",
|
|
1166
|
+
created: "created",
|
|
1167
|
+
Created: "Created",
|
|
1168
|
+
create: "create",
|
|
1169
|
+
templates: {
|
|
1170
|
+
createIssue: "jira-create-issue-for-alert",
|
|
1171
|
+
transition: "jira-transition-issue-on-alert-state",
|
|
1172
|
+
privateNote: "jira-comment-from-alert-private-note",
|
|
1173
|
+
updateComment: "jira-comment-on-alert-update",
|
|
1174
|
+
createFromIssue: "jira-create-alert-from-issue",
|
|
1175
|
+
statusToState: "jira-status-to-alert-state",
|
|
1176
|
+
commentToNote: "jira-comment-to-alert-private-note",
|
|
1177
|
+
issueChangesToNote: "jira-issue-changes-to-alert-private-note",
|
|
1178
|
+
},
|
|
1179
|
+
// Alerts have no public notes.
|
|
1180
|
+
noteTemplates: [
|
|
1181
|
+
{
|
|
1182
|
+
templateId: "jira-comment-from-alert-private-note",
|
|
1183
|
+
visibility: "private",
|
|
1184
|
+
internal: true,
|
|
1185
|
+
},
|
|
1186
|
+
],
|
|
1187
|
+
steps: {
|
|
1188
|
+
onCreate: "alert-on-create-1",
|
|
1189
|
+
onUpdate: "alert-on-update-1",
|
|
1190
|
+
prepareRecord: "prepare-alert-1",
|
|
1191
|
+
ifCreate: "if-create-1",
|
|
1192
|
+
createRecord: "create-alert-1",
|
|
1193
|
+
find: "find-alert-1",
|
|
1194
|
+
},
|
|
1195
|
+
components: {
|
|
1196
|
+
findOne: "alert-find-one",
|
|
1197
|
+
stateFindMany: "alert-state-find-many",
|
|
1198
|
+
timelineCreateOne: "alert-state-timeline-create-one",
|
|
1199
|
+
noteCreateOne: "alert-internal-note-create-one",
|
|
1200
|
+
severityFindMany: "alert-severity-find-many",
|
|
1201
|
+
createOne: "alert-create-one",
|
|
1202
|
+
},
|
|
1203
|
+
createdState: ALERT_CREATED_STATE,
|
|
1204
|
+
acknowledgedState: ALERT_ACKNOWLEDGED_STATE,
|
|
1205
|
+
resolvedState: ALERT_RESOLVED_STATE,
|
|
1206
|
+
states: ALERT_STATES,
|
|
1207
|
+
severities: ALERT_SEVERITIES,
|
|
1208
|
+
mostSevere: { id: HIGH_ALERT_SEVERITY_ID, name: "High" },
|
|
1209
|
+
// Two severities: the middle of the range rounds to the less severe one.
|
|
1210
|
+
middleSeverity: { id: LOW_ALERT_SEVERITY_ID, name: "Low" },
|
|
1211
|
+
leastSevere: { id: LOW_ALERT_SEVERITY_ID, name: "Low" },
|
|
1212
|
+
// An alert never reaches a status page, so there is nothing to keep it off.
|
|
1213
|
+
quietCreateFields: {},
|
|
1214
|
+
// An alert's root cause is written at creation and cannot be edited after.
|
|
1215
|
+
editListenOn: {
|
|
1216
|
+
title: true,
|
|
1217
|
+
description: true,
|
|
1218
|
+
alertSeverityId: true,
|
|
1219
|
+
alertSeverity: true,
|
|
1220
|
+
remediationNotes: true,
|
|
1221
|
+
},
|
|
1222
|
+
defaultTitle: "Disk usage above 90% on db-1",
|
|
1223
|
+
defaultDescription:
|
|
1224
|
+
'The "db-1" volume is at 93%.\nIt grew 4% in the last hour.',
|
|
1225
|
+
defaultSeverityName: "High",
|
|
1226
|
+
defaultNoteText: "Cleared old WAL segments. Usage is back to 71%.",
|
|
1227
|
+
model: alertModel,
|
|
1228
|
+
note: alertRecordNote,
|
|
1229
|
+
syncPrivate: syncPrivateSwitch("SYNC_PRIVATE_ALERTS"),
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
const KINDS: Array<SimulatedKind> = [INCIDENT, ALERT];
|
|
1233
|
+
|
|
1234
|
+
type TemplateIdsOfFunction = (kind: SimulatedKind) => Array<string>;
|
|
1235
|
+
|
|
1236
|
+
/** Every template of one kind, each once. */
|
|
1237
|
+
const templateIdsOf: TemplateIdsOfFunction = (
|
|
1238
|
+
kind: SimulatedKind,
|
|
1239
|
+
): Array<string> => {
|
|
1240
|
+
const ids: Array<string> = [
|
|
1241
|
+
...Object.values(kind.templates),
|
|
1242
|
+
...kind.noteTemplates.map((noteTemplate: NoteTemplate) => {
|
|
1243
|
+
return noteTemplate.templateId;
|
|
1244
|
+
}),
|
|
1245
|
+
];
|
|
1246
|
+
|
|
1247
|
+
return ids.filter((id: string, index: number) => {
|
|
1248
|
+
return ids.indexOf(id) === index;
|
|
1249
|
+
});
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
/* ------------------------------ Scenario helpers ------------------------------ */
|
|
1253
|
+
|
|
1254
|
+
type OnRecordFunction = (model: JSONObject) => JSONObject;
|
|
1255
|
+
|
|
1256
|
+
/** What a database trigger hands over. */
|
|
1257
|
+
const onRecord: OnRecordFunction = (model: JSONObject): JSONObject => {
|
|
1258
|
+
return { model: model };
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
type WebhookDeliveryFunction = (body: JSONObject) => JSONObject;
|
|
1262
|
+
|
|
1263
|
+
/** What the Webhook trigger hands over for a Jira delivery: lowercased headers, no query, the parsed body. */
|
|
1264
|
+
const webhookDelivery: WebhookDeliveryFunction = (
|
|
1265
|
+
body: JSONObject,
|
|
1266
|
+
): JSONObject => {
|
|
1267
|
+
return {
|
|
1268
|
+
"request-headers": {
|
|
1269
|
+
"content-type": "application/json; charset=utf-8",
|
|
1270
|
+
"user-agent": "Atlassian Webhook HTTP Client",
|
|
1271
|
+
"x-atlassian-webhook-identifier": WEBHOOK_IDENTIFIER,
|
|
1272
|
+
"x-atlassian-webhook-retry": "0",
|
|
1273
|
+
},
|
|
1274
|
+
"request-params": {},
|
|
1275
|
+
"request-body": body,
|
|
1276
|
+
};
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
type SearchRequestFunction = (
|
|
1280
|
+
kind: SimulatedKind,
|
|
1281
|
+
fields: Array<string>,
|
|
1282
|
+
expandTransitions?: boolean,
|
|
1283
|
+
) => JiraRequest;
|
|
1284
|
+
|
|
1285
|
+
/*
|
|
1286
|
+
* The label search every OneUptime -> Jira template starts with. It asks for
|
|
1287
|
+
* two issues, oldest first, so a clone carrying the same label is seen and
|
|
1288
|
+
* refused rather than silently being "the" issue.
|
|
1289
|
+
*/
|
|
1290
|
+
const searchRequest: SearchRequestFunction = (
|
|
1291
|
+
kind: SimulatedKind,
|
|
1292
|
+
fields: Array<string>,
|
|
1293
|
+
expandTransitions?: boolean,
|
|
1294
|
+
): JiraRequest => {
|
|
1295
|
+
const body: JSONObject = {
|
|
1296
|
+
jql: `labels = "${kind.labelPrefix}${kind.id}" ORDER BY created ASC`,
|
|
1297
|
+
fields: fields,
|
|
1298
|
+
maxResults: 2,
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
if (expandTransitions) {
|
|
1302
|
+
body["expand"] = "transitions";
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
return {
|
|
1306
|
+
method: "POST",
|
|
1307
|
+
url: `${JIRA_SITE}/rest/api/3/search/jql`,
|
|
1308
|
+
headers: AUTHORIZATION,
|
|
1309
|
+
body: body,
|
|
1310
|
+
};
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
type AdfOfFunction = (text: string) => JSONObject;
|
|
1314
|
+
|
|
1315
|
+
/** A comment body as the comment templates build it: one paragraph holding one text node. */
|
|
1316
|
+
const adfOf: AdfOfFunction = (text: string): JSONObject => {
|
|
1317
|
+
return {
|
|
1318
|
+
type: "doc",
|
|
1319
|
+
version: 1,
|
|
1320
|
+
content: [{ type: "paragraph", content: [{ type: "text", text: text }] }],
|
|
1321
|
+
};
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
/** Marks a comment internal in Jira Service Management, so the customer never sees it. */
|
|
1325
|
+
const INTERNAL_COMMENT_PROPERTY: JSONObject = {
|
|
1326
|
+
key: "sd.public.comment",
|
|
1327
|
+
value: { internal: true },
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
type ExpectValidAdfFunction = (document: JSONValue | undefined) => void;
|
|
1331
|
+
|
|
1332
|
+
/*
|
|
1333
|
+
* Jira's v3 API answers 400 to a document that is not a doc of paragraphs, or
|
|
1334
|
+
* that holds an empty text node, so a body that parses is not enough.
|
|
1335
|
+
*/
|
|
1336
|
+
const expectValidAdf: ExpectValidAdfFunction = (
|
|
1337
|
+
document: JSONValue | undefined,
|
|
1338
|
+
): void => {
|
|
1339
|
+
const doc: JSONObject = document as JSONObject;
|
|
1340
|
+
|
|
1341
|
+
expect(doc["type"]).toBe("doc");
|
|
1342
|
+
expect(doc["version"]).toBe(1);
|
|
1343
|
+
|
|
1344
|
+
const paragraphs: Array<JSONObject> = doc["content"] as Array<JSONObject>;
|
|
1345
|
+
expect(paragraphs.length).toBeGreaterThan(0);
|
|
1346
|
+
|
|
1347
|
+
for (const paragraph of paragraphs) {
|
|
1348
|
+
expect(paragraph["type"]).toBe("paragraph");
|
|
1349
|
+
|
|
1350
|
+
const nodes: Array<JSONObject> = paragraph["content"] as Array<JSONObject>;
|
|
1351
|
+
expect(nodes.length).toBeGreaterThan(0);
|
|
1352
|
+
|
|
1353
|
+
for (const node of nodes) {
|
|
1354
|
+
expect(node["type"]).toBe("text");
|
|
1355
|
+
expect(typeof node["text"]).toBe("string");
|
|
1356
|
+
expect((node["text"] as string).length).toBeGreaterThan(0);
|
|
1357
|
+
|
|
1358
|
+
for (const mark of (node["marks"] as Array<JSONObject> | undefined) ||
|
|
1359
|
+
[]) {
|
|
1360
|
+
expect(mark["type"]).toBe("link");
|
|
1361
|
+
expect(
|
|
1362
|
+
((mark["attrs"] as JSONObject)["href"] as string).startsWith(
|
|
1363
|
+
"https://",
|
|
1364
|
+
),
|
|
1365
|
+
).toBe(true);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
type WikiTextOfFunction = (document: JSONValue | undefined) => string;
|
|
1372
|
+
|
|
1373
|
+
/*
|
|
1374
|
+
* A posted ADF document as the webhook later carries it: Jira Cloud webhooks
|
|
1375
|
+
* send rich text as a wiki-markup string, a paragraph per block and a link
|
|
1376
|
+
* as [text|href].
|
|
1377
|
+
*/
|
|
1378
|
+
const wikiTextOf: WikiTextOfFunction = (
|
|
1379
|
+
document: JSONValue | undefined,
|
|
1380
|
+
): string => {
|
|
1381
|
+
const paragraphs: Array<JSONObject> = (document as JSONObject)[
|
|
1382
|
+
"content"
|
|
1383
|
+
] as Array<JSONObject>;
|
|
1384
|
+
|
|
1385
|
+
return paragraphs
|
|
1386
|
+
.map((paragraph: JSONObject): string => {
|
|
1387
|
+
return (paragraph["content"] as Array<JSONObject>)
|
|
1388
|
+
.map((node: JSONObject): string => {
|
|
1389
|
+
const marks: Array<JSONObject> =
|
|
1390
|
+
(node["marks"] as Array<JSONObject> | undefined) || [];
|
|
1391
|
+
const link: JSONObject | undefined = marks.find(
|
|
1392
|
+
(mark: JSONObject) => {
|
|
1393
|
+
return mark["type"] === "link";
|
|
1394
|
+
},
|
|
1395
|
+
);
|
|
1396
|
+
|
|
1397
|
+
return link
|
|
1398
|
+
? `[${node["text"] as string}|${(link["attrs"] as JSONObject)["href"] as string}]`
|
|
1399
|
+
: (node["text"] as string);
|
|
1400
|
+
})
|
|
1401
|
+
.join("");
|
|
1402
|
+
})
|
|
1403
|
+
.join("\n\n");
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
type RecordOfFunction = (
|
|
1407
|
+
kind: SimulatedKind,
|
|
1408
|
+
state: JSONObject,
|
|
1409
|
+
recordId?: string,
|
|
1410
|
+
) => JSONObject;
|
|
1411
|
+
|
|
1412
|
+
/* A record as Find One returns it for the status template's select. */
|
|
1413
|
+
const recordOf: RecordOfFunction = (
|
|
1414
|
+
kind: SimulatedKind,
|
|
1415
|
+
state: JSONObject,
|
|
1416
|
+
recordId?: string,
|
|
1417
|
+
): JSONObject => {
|
|
1418
|
+
return {
|
|
1419
|
+
_id: recordId || kind.id,
|
|
1420
|
+
[kind.numberField]: kind.number,
|
|
1421
|
+
[kind.stateRelation]: {
|
|
1422
|
+
_id: state["_id"] as string,
|
|
1423
|
+
name: state["name"] as string,
|
|
1424
|
+
order: state["order"] as number,
|
|
1425
|
+
},
|
|
1426
|
+
};
|
|
1427
|
+
};
|
|
1428
|
+
|
|
1429
|
+
type KindObjectFunction = (kind: SimulatedKind) => JSONObject;
|
|
1430
|
+
|
|
1431
|
+
/* A record as Find One returns it for the note templates' select. */
|
|
1432
|
+
const linkedRecordOf: KindObjectFunction = (
|
|
1433
|
+
kind: SimulatedKind,
|
|
1434
|
+
): JSONObject => {
|
|
1435
|
+
return { _id: kind.id, [kind.numberField]: kind.number };
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1438
|
+
type LinkedLabelsOfFunction = (kind: SimulatedKind) => Array<string>;
|
|
1439
|
+
|
|
1440
|
+
/** The labels an issue linked to the kind's fixture record carries. */
|
|
1441
|
+
const linkedLabelsOf: LinkedLabelsOfFunction = (
|
|
1442
|
+
kind: SimulatedKind,
|
|
1443
|
+
): Array<string> => {
|
|
1444
|
+
return [JIRA_LINK_LABEL, kind.label()];
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
type KindDatabaseFunction = (
|
|
1448
|
+
kind: SimulatedKind,
|
|
1449
|
+
record: JSONObject | null,
|
|
1450
|
+
) => FakeDatabase;
|
|
1451
|
+
|
|
1452
|
+
const statusDatabase: KindDatabaseFunction = (
|
|
1453
|
+
kind: SimulatedKind,
|
|
1454
|
+
record: JSONObject | null,
|
|
1455
|
+
): FakeDatabase => {
|
|
1456
|
+
return {
|
|
1457
|
+
[kind.components.findOne]: found(record),
|
|
1458
|
+
[kind.components.stateFindMany]: many(kind.states),
|
|
1459
|
+
[kind.components.timelineCreateOne]: createdWithId(NEW_TIMELINE_ID),
|
|
1460
|
+
};
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
const noteDatabase: KindDatabaseFunction = (
|
|
1464
|
+
kind: SimulatedKind,
|
|
1465
|
+
record: JSONObject | null,
|
|
1466
|
+
): FakeDatabase => {
|
|
1467
|
+
return {
|
|
1468
|
+
[kind.components.findOne]: found(record),
|
|
1469
|
+
[kind.components.noteCreateOne]: createdWithId(NEW_NOTE_ID),
|
|
1470
|
+
};
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
type CreateDatabaseFunction = (kind: SimulatedKind) => FakeDatabase;
|
|
1474
|
+
|
|
1475
|
+
/** What the create-from-Jira template reads and writes. */
|
|
1476
|
+
const createDatabase: CreateDatabaseFunction = (
|
|
1477
|
+
kind: SimulatedKind,
|
|
1478
|
+
): FakeDatabase => {
|
|
1479
|
+
return {
|
|
1480
|
+
[kind.components.severityFindMany]: many(kind.severities),
|
|
1481
|
+
[kind.components.createOne]: createdWithId(kind.newId),
|
|
1482
|
+
};
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
/** The one read the Jira -> OneUptime templates make: an issue's labels, as Jira has them now. */
|
|
1486
|
+
const GET_LABELS_REQUEST: JiraRequest = {
|
|
1487
|
+
method: "GET",
|
|
1488
|
+
url: `${JIRA_SITE}/rest/api/3/issue/${JIRA_ISSUE_KEY}?fields=labels`,
|
|
1489
|
+
headers: AUTHORIZATION,
|
|
1490
|
+
};
|
|
1491
|
+
|
|
1492
|
+
type CreateFromIssueJiraFunction = (props?: {
|
|
1493
|
+
labels?: Array<string> | undefined;
|
|
1494
|
+
getIssue?: JiraReply | undefined;
|
|
1495
|
+
edit?: JiraReply | undefined;
|
|
1496
|
+
}) => FakeJiraFunction;
|
|
1497
|
+
|
|
1498
|
+
/*
|
|
1499
|
+
* Jira as the create-from-Jira templates meet it: first asked for the issue's
|
|
1500
|
+
* labels, then asked to add the link labels. By default the issue has none
|
|
1501
|
+
* yet, as a freshly created issue does.
|
|
1502
|
+
*/
|
|
1503
|
+
const createFromIssueJira: CreateFromIssueJiraFunction = (props?: {
|
|
1504
|
+
labels?: Array<string> | undefined;
|
|
1505
|
+
getIssue?: JiraReply | undefined;
|
|
1506
|
+
edit?: JiraReply | undefined;
|
|
1507
|
+
}): FakeJiraFunction => {
|
|
1508
|
+
return jiraRoutes({
|
|
1509
|
+
[GET_LABELS]:
|
|
1510
|
+
props?.getIssue || ok(jiraIssueLabelsResponse(props?.labels || [])),
|
|
1511
|
+
[EDIT_ISSUE]: props?.edit || NO_CONTENT,
|
|
1512
|
+
});
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
type LinkLabelsRequestFunction = (kind: SimulatedKind) => JiraRequest;
|
|
1516
|
+
|
|
1517
|
+
/*
|
|
1518
|
+
* The PUT that links an issue to the record just created from it. The label
|
|
1519
|
+
* carries the id the database gave the record — nothing else could.
|
|
1520
|
+
*/
|
|
1521
|
+
const linkLabelsRequest: LinkLabelsRequestFunction = (
|
|
1522
|
+
kind: SimulatedKind,
|
|
1523
|
+
): JiraRequest => {
|
|
1524
|
+
return {
|
|
1525
|
+
method: "PUT",
|
|
1526
|
+
url: `${JIRA_SITE}/rest/api/3/issue/${JIRA_ISSUE_KEY}`,
|
|
1527
|
+
headers: AUTHORIZATION,
|
|
1528
|
+
body: {
|
|
1529
|
+
update: {
|
|
1530
|
+
labels: [
|
|
1531
|
+
{ add: JIRA_LINK_LABEL },
|
|
1532
|
+
{ add: `${kind.labelPrefix}${kind.newId}` },
|
|
1533
|
+
],
|
|
1534
|
+
},
|
|
1535
|
+
},
|
|
1536
|
+
};
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
const LABEL_JQL: RegExp = /^labels = "([^"]+)" ORDER BY created ASC$/;
|
|
1540
|
+
|
|
1541
|
+
type LabelAwareJiraFunction = (issueLabels: Array<string>) => FakeJiraFunction;
|
|
1542
|
+
|
|
1543
|
+
/*
|
|
1544
|
+
* Jira holding one issue, OPS-17, carrying the given labels. A label search
|
|
1545
|
+
* finds it only when its JQL names one of them, as Jira itself would answer,
|
|
1546
|
+
* so a template searching by another kind's label finds nothing. Transitions
|
|
1547
|
+
* and comments on the issue are accepted.
|
|
1548
|
+
*/
|
|
1549
|
+
const labelAwareJira: LabelAwareJiraFunction = (
|
|
1550
|
+
issueLabels: Array<string>,
|
|
1551
|
+
): FakeJiraFunction => {
|
|
1552
|
+
const otherRoutes: FakeJiraFunction = jiraRoutes({
|
|
1553
|
+
[TRANSITION]: NO_CONTENT,
|
|
1554
|
+
[COMMENT]: COMMENT_CREATED,
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
return (request: JiraRequest): JiraReply => {
|
|
1558
|
+
if (`${request.method} ${request.url.replace(JIRA_SITE, "")}` !== SEARCH) {
|
|
1559
|
+
return otherRoutes(request);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
const match: RegExpExecArray | null = LABEL_JQL.exec(
|
|
1563
|
+
String((request.body as JSONObject)["jql"]),
|
|
1564
|
+
);
|
|
1565
|
+
|
|
1566
|
+
return ok(
|
|
1567
|
+
match && issueLabels.includes(match[1] as string)
|
|
1568
|
+
? jiraSearchResponse()
|
|
1569
|
+
: EMPTY_JIRA_SEARCH,
|
|
1570
|
+
);
|
|
1571
|
+
};
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1574
|
+
type TriggerArgumentsOfFunction = (templateId: string) => JSONObject;
|
|
1575
|
+
|
|
1576
|
+
/*
|
|
1577
|
+
* What a database trigger is configured with. The simulation hands the
|
|
1578
|
+
* trigger's model over as given, so a scenario that depends on a field — like
|
|
1579
|
+
* isPrivate — also checks that the real trigger would have selected it.
|
|
1580
|
+
*/
|
|
1581
|
+
const triggerArgumentsOf: TriggerArgumentsOfFunction = (
|
|
1582
|
+
templateId: string,
|
|
1583
|
+
): JSONObject => {
|
|
1584
|
+
const graph: SimulatedGraph = buildSimulatedGraph(templateId);
|
|
1585
|
+
const trigger: SimulatedNode = graph.nodes[
|
|
1586
|
+
graph.triggerComponentId
|
|
1587
|
+
] as SimulatedNode;
|
|
1588
|
+
|
|
1589
|
+
return trigger.arguments;
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
const triggerSelectOf: TriggerArgumentsOfFunction = (
|
|
1593
|
+
templateId: string,
|
|
1594
|
+
): JSONObject => {
|
|
1595
|
+
return triggerArgumentsOf(templateId)["select"] as JSONObject;
|
|
1596
|
+
};
|
|
1597
|
+
|
|
1598
|
+
type FiresOnUpdateFunction = (
|
|
1599
|
+
templateId: string,
|
|
1600
|
+
updatedFields: JSONObject,
|
|
1601
|
+
) => boolean;
|
|
1602
|
+
|
|
1603
|
+
/*
|
|
1604
|
+
* The filter OnTriggerBaseModel.initTrigger
|
|
1605
|
+
* (packages/Common/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.ts)
|
|
1606
|
+
* puts in front of an on-update workflow: with a Listen On set, the workflow
|
|
1607
|
+
* runs only when one of its keys is truthy among the fields the update
|
|
1608
|
+
* changed. Keys are compared exactly, so an edit form that sends a relation
|
|
1609
|
+
* is only heard by a Listen On naming the relation.
|
|
1610
|
+
*/
|
|
1611
|
+
const firesOnUpdate: FiresOnUpdateFunction = (
|
|
1612
|
+
templateId: string,
|
|
1613
|
+
updatedFields: JSONObject,
|
|
1614
|
+
): boolean => {
|
|
1615
|
+
const listenOn: JSONObject | undefined = triggerArgumentsOf(templateId)[
|
|
1616
|
+
"listen-on"
|
|
1617
|
+
] as JSONObject | undefined;
|
|
1618
|
+
|
|
1619
|
+
if (
|
|
1620
|
+
!listenOn ||
|
|
1621
|
+
Object.keys(listenOn).length === 0 ||
|
|
1622
|
+
Object.keys(updatedFields).length === 0
|
|
1623
|
+
) {
|
|
1624
|
+
return true;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
return Object.keys(listenOn).some((key: string) => {
|
|
1628
|
+
return Boolean(updatedFields[key]);
|
|
1629
|
+
});
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
const CLONE_KEY: string = "OPS-30";
|
|
1633
|
+
|
|
1634
|
+
type FirstIssueOfFunction = (search: JSONObject) => JSONObject;
|
|
1635
|
+
|
|
1636
|
+
const firstIssueOf: FirstIssueOfFunction = (search: JSONObject): JSONObject => {
|
|
1637
|
+
return (search["issues"] as Array<JSONObject>)[0] as JSONObject;
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
/*
|
|
1641
|
+
* Cloning an issue in Jira copies its labels, so the clone claims the record
|
|
1642
|
+
* too. The search sorts oldest first; the original comes back first.
|
|
1643
|
+
*/
|
|
1644
|
+
const CLONED_SEARCH: JSONObject = jiraSearchResponse({
|
|
1645
|
+
issues: [
|
|
1646
|
+
firstIssueOf(jiraSearchResponse()),
|
|
1647
|
+
{
|
|
1648
|
+
...firstIssueOf(jiraSearchResponse({ key: CLONE_KEY })),
|
|
1649
|
+
id: "10071",
|
|
1650
|
+
self: `${JIRA_SITE}/rest/api/3/issue/10071`,
|
|
1651
|
+
},
|
|
1652
|
+
],
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
type KindLogFunction = (kind: SimulatedKind) => string;
|
|
1656
|
+
|
|
1657
|
+
const cloneSkippedLog: KindLogFunction = (kind: SimulatedKind): string => {
|
|
1658
|
+
return `ℹ️ More than one Jira issue is labelled ${kind.label()} (${JIRA_ISSUE_KEY}, ${CLONE_KEY}). A cloned issue copies the label: remove it from every issue except the one filed for the ${kind.noun}.`;
|
|
1659
|
+
};
|
|
1660
|
+
|
|
1661
|
+
/** A OneUptime -> Jira template whose label search found nothing. */
|
|
1662
|
+
const noLinkedIssueLog: KindLogFunction = (kind: SimulatedKind): string => {
|
|
1663
|
+
return `ℹ️ No Jira issue is labelled ${kind.label()}, or the Jira credentials cannot see it.`;
|
|
1664
|
+
};
|
|
1665
|
+
|
|
1666
|
+
/** A Jira -> OneUptime template given an issue that carries no label of its kind. */
|
|
1667
|
+
const notLinkedLog: KindLogFunction = (kind: SimulatedKind): string => {
|
|
1668
|
+
return `ℹ️ Jira issue ${JIRA_ISSUE_KEY} is not linked to an ${kind.noun}: it has no ${kind.labelPrefix}<id> label.`;
|
|
1669
|
+
};
|
|
1670
|
+
|
|
1671
|
+
/** A create-from-Jira template given an issue that is already linked to anything. */
|
|
1672
|
+
const alreadyLinkedLog: KindLogFunction = (kind: SimulatedKind): string => {
|
|
1673
|
+
return `ℹ️ Jira issue ${JIRA_ISSUE_KEY} is already linked to OneUptime, so no ${kind.noun} was ${kind.created}.`;
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
const NOTE_FROM_JIRA_LOG: string =
|
|
1677
|
+
"ℹ️ This note came from Jira, so it was not posted back.";
|
|
1678
|
+
|
|
1679
|
+
const COMMENT_FROM_ONEUPTIME_LOG: string =
|
|
1680
|
+
"ℹ️ This comment was posted by OneUptime, so it was not copied back.";
|
|
1681
|
+
|
|
1682
|
+
const NOTHING_WORTH_A_NOTE_LOG: string = `ℹ️ Nothing that changed on Jira issue ${JIRA_ISSUE_KEY} is worth a note.`;
|
|
1683
|
+
|
|
1684
|
+
type CallOfFunction = (
|
|
1685
|
+
trace: SimulationTrace,
|
|
1686
|
+
metadataId: string,
|
|
1687
|
+
) => DatabaseCall;
|
|
1688
|
+
|
|
1689
|
+
const callOf: CallOfFunction = (
|
|
1690
|
+
trace: SimulationTrace,
|
|
1691
|
+
metadataId: string,
|
|
1692
|
+
): DatabaseCall => {
|
|
1693
|
+
const calls: Array<DatabaseCall> = trace.databaseCalls.filter(
|
|
1694
|
+
(call: DatabaseCall) => {
|
|
1695
|
+
return call.metadataId === metadataId;
|
|
1696
|
+
},
|
|
1697
|
+
);
|
|
1698
|
+
|
|
1699
|
+
expect(calls).toHaveLength(1);
|
|
1700
|
+
|
|
1701
|
+
return calls[0] as DatabaseCall;
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1704
|
+
type JsonOfFunction = (
|
|
1705
|
+
trace: SimulationTrace,
|
|
1706
|
+
metadataId: string,
|
|
1707
|
+
) => JSONObject;
|
|
1708
|
+
|
|
1709
|
+
/** The fields a create step wrote. */
|
|
1710
|
+
const jsonOf: JsonOfFunction = (
|
|
1711
|
+
trace: SimulationTrace,
|
|
1712
|
+
metadataId: string,
|
|
1713
|
+
): JSONObject => {
|
|
1714
|
+
return callOf(trace, metadataId).args["json"] as JSONObject;
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1717
|
+
type MetadataIdsOfFunction = (trace: SimulationTrace) => Array<string>;
|
|
1718
|
+
|
|
1719
|
+
const metadataIdsOf: MetadataIdsOfFunction = (
|
|
1720
|
+
trace: SimulationTrace,
|
|
1721
|
+
): Array<string> => {
|
|
1722
|
+
return trace.databaseCalls.map((call: DatabaseCall) => {
|
|
1723
|
+
return call.metadataId;
|
|
1724
|
+
});
|
|
1725
|
+
};
|
|
1726
|
+
|
|
1727
|
+
type AddedLabelsOfFunction = (trace: SimulationTrace) => Array<string>;
|
|
1728
|
+
|
|
1729
|
+
/** The labels a create-from-Jira template's PUT added to the issue. */
|
|
1730
|
+
const addedLabelsOf: AddedLabelsOfFunction = (
|
|
1731
|
+
trace: SimulationTrace,
|
|
1732
|
+
): Array<string> => {
|
|
1733
|
+
const edit: JiraRequest | undefined = trace.requests.find(
|
|
1734
|
+
(request: JiraRequest) => {
|
|
1735
|
+
return request.method === "PUT";
|
|
1736
|
+
},
|
|
1737
|
+
);
|
|
1738
|
+
|
|
1739
|
+
expect(edit).toBeDefined();
|
|
1740
|
+
|
|
1741
|
+
return (
|
|
1742
|
+
((edit!.body as JSONObject)["update"] as JSONObject)[
|
|
1743
|
+
"labels"
|
|
1744
|
+
] as Array<JSONObject>
|
|
1745
|
+
).map((operation: JSONObject) => {
|
|
1746
|
+
return operation["add"] as string;
|
|
1747
|
+
});
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
type PostedCommentOfFunction = (trace: SimulationTrace) => JSONObject;
|
|
1751
|
+
|
|
1752
|
+
/** The ADF document a comment template posted. */
|
|
1753
|
+
const postedCommentOf: PostedCommentOfFunction = (
|
|
1754
|
+
trace: SimulationTrace,
|
|
1755
|
+
): JSONObject => {
|
|
1756
|
+
const post: JiraRequest | undefined = trace.requests.find(
|
|
1757
|
+
(request: JiraRequest) => {
|
|
1758
|
+
return request.url.endsWith("/comment");
|
|
1759
|
+
},
|
|
1760
|
+
);
|
|
1761
|
+
|
|
1762
|
+
expect(post).toBeDefined();
|
|
1763
|
+
|
|
1764
|
+
return (post!.body as JSONObject)["body"] as JSONObject;
|
|
1765
|
+
};
|
|
1766
|
+
|
|
1767
|
+
type IssueFieldsOfFunction = (trace: SimulationTrace) => JSONObject;
|
|
1768
|
+
|
|
1769
|
+
/** The fields of the issue a create-issue template filed. */
|
|
1770
|
+
const issueFieldsOf: IssueFieldsOfFunction = (
|
|
1771
|
+
trace: SimulationTrace,
|
|
1772
|
+
): JSONObject => {
|
|
1773
|
+
return (trace.requests[0]!.body as JSONObject)["fields"] as JSONObject;
|
|
1774
|
+
};
|
|
1775
|
+
|
|
1776
|
+
interface WebhookScenarioProps {
|
|
1777
|
+
event: JSONObject;
|
|
1778
|
+
/** The issue's labels as Jira's GET returns them; the kind's own link by default. */
|
|
1779
|
+
labels?: Array<string> | undefined;
|
|
1780
|
+
/** What Find One returns; the kind's fixture record by default, null for "not here". */
|
|
1781
|
+
record?: JSONObject | null | undefined;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
type WebhookScenarioFunction = (
|
|
1785
|
+
kind: SimulatedKind,
|
|
1786
|
+
props: WebhookScenarioProps,
|
|
1787
|
+
) => Scenario;
|
|
1788
|
+
|
|
1789
|
+
/* A Jira comment delivered to the kind's comment-to-note template. */
|
|
1790
|
+
const commentScenario: WebhookScenarioFunction = (
|
|
1791
|
+
kind: SimulatedKind,
|
|
1792
|
+
props: WebhookScenarioProps,
|
|
1793
|
+
): Scenario => {
|
|
1794
|
+
return {
|
|
1795
|
+
templateId: kind.templates.commentToNote,
|
|
1796
|
+
trigger: webhookDelivery(props.event),
|
|
1797
|
+
jira: jiraRoutes({
|
|
1798
|
+
[GET_LABELS]: ok(
|
|
1799
|
+
jiraIssueLabelsResponse(props.labels || linkedLabelsOf(kind)),
|
|
1800
|
+
),
|
|
1801
|
+
}),
|
|
1802
|
+
database: noteDatabase(
|
|
1803
|
+
kind,
|
|
1804
|
+
props.record === undefined ? linkedRecordOf(kind) : props.record,
|
|
1805
|
+
),
|
|
1806
|
+
};
|
|
1807
|
+
};
|
|
1808
|
+
|
|
1809
|
+
/* An issue_updated delivered to the kind's issue-changes template. It asks Jira nothing. */
|
|
1810
|
+
const issueChangesScenario: WebhookScenarioFunction = (
|
|
1811
|
+
kind: SimulatedKind,
|
|
1812
|
+
props: WebhookScenarioProps,
|
|
1813
|
+
): Scenario => {
|
|
1814
|
+
return {
|
|
1815
|
+
templateId: kind.templates.issueChangesToNote,
|
|
1816
|
+
trigger: webhookDelivery(props.event),
|
|
1817
|
+
database: noteDatabase(
|
|
1818
|
+
kind,
|
|
1819
|
+
props.record === undefined ? linkedRecordOf(kind) : props.record,
|
|
1820
|
+
),
|
|
1821
|
+
};
|
|
1822
|
+
};
|
|
1823
|
+
|
|
1824
|
+
type RunWebhookFunction = (
|
|
1825
|
+
kind: SimulatedKind,
|
|
1826
|
+
props: WebhookScenarioProps,
|
|
1827
|
+
) => Promise<SimulationTrace>;
|
|
1828
|
+
|
|
1829
|
+
/* Shared by the comment suite and the echo, cross-kind and security suites below. */
|
|
1830
|
+
const runComment: RunWebhookFunction = async (
|
|
1831
|
+
kind: SimulatedKind,
|
|
1832
|
+
props: WebhookScenarioProps,
|
|
1833
|
+
): Promise<SimulationTrace> => {
|
|
1834
|
+
return await runTemplate(commentScenario(kind, props));
|
|
1835
|
+
};
|
|
1836
|
+
|
|
1837
|
+
const runIssueChanges: RunWebhookFunction = async (
|
|
1838
|
+
kind: SimulatedKind,
|
|
1839
|
+
props: WebhookScenarioProps,
|
|
1840
|
+
): Promise<SimulationTrace> => {
|
|
1841
|
+
return await runTemplate(issueChangesScenario(kind, props));
|
|
1842
|
+
};
|
|
1843
|
+
|
|
1844
|
+
type RunCreateFromIssueFunction = (
|
|
1845
|
+
kind: SimulatedKind,
|
|
1846
|
+
props?: {
|
|
1847
|
+
event?: JSONObject | undefined;
|
|
1848
|
+
labels?: Array<string> | undefined;
|
|
1849
|
+
},
|
|
1850
|
+
) => Promise<SimulationTrace>;
|
|
1851
|
+
|
|
1852
|
+
/*
|
|
1853
|
+
* A jira:issue_created delivered to the kind's create-from-Jira template, for
|
|
1854
|
+
* an issue Jira says carries the given labels (none by default).
|
|
1855
|
+
*/
|
|
1856
|
+
const runCreateFromIssue: RunCreateFromIssueFunction = async (
|
|
1857
|
+
kind: SimulatedKind,
|
|
1858
|
+
props?: {
|
|
1859
|
+
event?: JSONObject | undefined;
|
|
1860
|
+
labels?: Array<string> | undefined;
|
|
1861
|
+
},
|
|
1862
|
+
): Promise<SimulationTrace> => {
|
|
1863
|
+
return await runTemplate({
|
|
1864
|
+
templateId: kind.templates.createFromIssue,
|
|
1865
|
+
trigger: webhookDelivery(props?.event || jiraIssueCreatedEvent()),
|
|
1866
|
+
jira: createFromIssueJira({ labels: props?.labels }),
|
|
1867
|
+
database: createDatabase(kind),
|
|
1868
|
+
});
|
|
1869
|
+
};
|
|
1870
|
+
|
|
1871
|
+
type RunCreateIssueFunction = (kind: SimulatedKind) => Promise<SimulationTrace>;
|
|
1872
|
+
|
|
1873
|
+
/* The kind's create-issue template, filing its fixture record. */
|
|
1874
|
+
const runCreateIssue: RunCreateIssueFunction = async (
|
|
1875
|
+
kind: SimulatedKind,
|
|
1876
|
+
): Promise<SimulationTrace> => {
|
|
1877
|
+
return await runTemplate({
|
|
1878
|
+
templateId: kind.templates.createIssue,
|
|
1879
|
+
trigger: onRecord(kind.model()),
|
|
1880
|
+
jira: jiraRoutes({ [CREATE_ISSUE]: ISSUE_CREATED }),
|
|
1881
|
+
});
|
|
1882
|
+
};
|
|
1883
|
+
|
|
1884
|
+
type RunNoteToCommentFunction = (
|
|
1885
|
+
templateId: string,
|
|
1886
|
+
note: JSONObject,
|
|
1887
|
+
) => Promise<SimulationTrace>;
|
|
1888
|
+
|
|
1889
|
+
/* A note-to-comment template, meeting a linked issue that accepts the comment. */
|
|
1890
|
+
const runNoteToComment: RunNoteToCommentFunction = async (
|
|
1891
|
+
templateId: string,
|
|
1892
|
+
note: JSONObject,
|
|
1893
|
+
): Promise<SimulationTrace> => {
|
|
1894
|
+
return await runTemplate({
|
|
1895
|
+
templateId: templateId,
|
|
1896
|
+
trigger: onRecord(note),
|
|
1897
|
+
jira: jiraRoutes({
|
|
1898
|
+
[SEARCH]: ok(jiraSearchResponse()),
|
|
1899
|
+
[COMMENT]: COMMENT_CREATED,
|
|
1900
|
+
}),
|
|
1901
|
+
});
|
|
1902
|
+
};
|
|
1903
|
+
|
|
1904
|
+
/* ------------------------------- The table itself ------------------------------- */
|
|
1905
|
+
|
|
1906
|
+
describe("simulated: every Jira template is simulated", () => {
|
|
1907
|
+
test("the two kinds between them name every Jira template, and nothing else", () => {
|
|
1908
|
+
const simulated: Array<string> = [
|
|
1909
|
+
...templateIdsOf(INCIDENT),
|
|
1910
|
+
...templateIdsOf(ALERT),
|
|
1911
|
+
].sort();
|
|
1912
|
+
const jira: Array<string> = getWorkflowTemplatesByCategory(
|
|
1913
|
+
WorkflowTemplateCategory.Jira,
|
|
1914
|
+
)
|
|
1915
|
+
.map((template: WorkflowTemplate) => {
|
|
1916
|
+
return template.id;
|
|
1917
|
+
})
|
|
1918
|
+
.sort();
|
|
1919
|
+
|
|
1920
|
+
expect(simulated).toEqual(jira);
|
|
1921
|
+
expect(templateIdsOf(INCIDENT)).toHaveLength(9);
|
|
1922
|
+
expect(templateIdsOf(ALERT)).toHaveLength(8);
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
test("the label prefixes this file writes out are the ones the templates export", () => {
|
|
1926
|
+
expect(INCIDENT.labelPrefix).toBe(JIRA_INCIDENT_LABEL_PREFIX);
|
|
1927
|
+
expect(ALERT.labelPrefix).toBe(JIRA_ALERT_LABEL_PREFIX);
|
|
1928
|
+
expect(INCIDENT.label()).toBe(
|
|
1929
|
+
`${JIRA_INCIDENT_LABEL_PREFIX}${INCIDENT_ID}`,
|
|
1930
|
+
);
|
|
1931
|
+
expect(ALERT.label()).toBe(`${JIRA_ALERT_LABEL_PREFIX}${ALERT_ID}`);
|
|
1932
|
+
});
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
/* ------------------------------- OneUptime -> Jira ------------------------------- */
|
|
1936
|
+
|
|
1937
|
+
type ExpectedTextFunction = (kind: SimulatedKind) => string;
|
|
1938
|
+
|
|
1939
|
+
const expectedIssueDescription: ExpectedTextFunction = (
|
|
1940
|
+
kind: SimulatedKind,
|
|
1941
|
+
): string => {
|
|
1942
|
+
return [
|
|
1943
|
+
`${kind.number} was ${kind.created} in OneUptime.`,
|
|
1944
|
+
`Severity: ${kind.defaultSeverityName}`,
|
|
1945
|
+
`State: ${kind.createdState["name"] as string}`,
|
|
1946
|
+
"",
|
|
1947
|
+
kind.defaultDescription,
|
|
1948
|
+
].join("\n");
|
|
1949
|
+
};
|
|
1950
|
+
|
|
1951
|
+
/** The record's page in the dashboard, which the issue links back to. */
|
|
1952
|
+
const recordUrlOf: ExpectedTextFunction = (kind: SimulatedKind): string => {
|
|
1953
|
+
return `${ONEUPTIME_URL}/dashboard/${PROJECT_ID}/${kind.dashboardPath}/${kind.id}`;
|
|
1954
|
+
};
|
|
1955
|
+
|
|
1956
|
+
describe.each(KINDS)(
|
|
1957
|
+
"simulated: create a Jira issue when an $noun is $created",
|
|
1958
|
+
(kind: SimulatedKind) => {
|
|
1959
|
+
const TEMPLATE: string = kind.templates.createIssue;
|
|
1960
|
+
|
|
1961
|
+
test(`files one labelled issue, with a readable ADF description that links back to the ${kind.noun}`, async () => {
|
|
1962
|
+
const trace: SimulationTrace = await runTemplate({
|
|
1963
|
+
templateId: TEMPLATE,
|
|
1964
|
+
trigger: onRecord(kind.model()),
|
|
1965
|
+
jira: jiraRoutes({ [CREATE_ISSUE]: ISSUE_CREATED }),
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
expect(trace.executed).toEqual([
|
|
1969
|
+
kind.steps.onCreate,
|
|
1970
|
+
"prepare-issue-1",
|
|
1971
|
+
"if-create-1",
|
|
1972
|
+
"create-issue-1",
|
|
1973
|
+
"log-created",
|
|
1974
|
+
]);
|
|
1975
|
+
|
|
1976
|
+
expect(trace.requests).toEqual([
|
|
1977
|
+
{
|
|
1978
|
+
method: "POST",
|
|
1979
|
+
url: `${JIRA_SITE}/rest/api/3/issue`,
|
|
1980
|
+
headers: AUTHORIZATION,
|
|
1981
|
+
body: {
|
|
1982
|
+
fields: {
|
|
1983
|
+
project: { key: "OPS" },
|
|
1984
|
+
issuetype: { name: "Task" },
|
|
1985
|
+
summary: `[OneUptime] ${kind.number}: ${kind.defaultTitle}`,
|
|
1986
|
+
labels: [JIRA_LINK_LABEL, kind.label()],
|
|
1987
|
+
description: {
|
|
1988
|
+
type: "doc",
|
|
1989
|
+
version: 1,
|
|
1990
|
+
content: [
|
|
1991
|
+
{
|
|
1992
|
+
type: "paragraph",
|
|
1993
|
+
content: [
|
|
1994
|
+
{ type: "text", text: expectedIssueDescription(kind) },
|
|
1995
|
+
],
|
|
1996
|
+
},
|
|
1997
|
+
{
|
|
1998
|
+
type: "paragraph",
|
|
1999
|
+
content: [
|
|
2000
|
+
{
|
|
2001
|
+
type: "text",
|
|
2002
|
+
text: `Open this ${kind.noun} in OneUptime`,
|
|
2003
|
+
marks: [
|
|
2004
|
+
{
|
|
2005
|
+
type: "link",
|
|
2006
|
+
attrs: { href: recordUrlOf(kind) },
|
|
2007
|
+
},
|
|
2008
|
+
],
|
|
2009
|
+
},
|
|
2010
|
+
],
|
|
2011
|
+
},
|
|
2012
|
+
],
|
|
2013
|
+
},
|
|
2014
|
+
},
|
|
2015
|
+
},
|
|
2016
|
+
},
|
|
2017
|
+
]);
|
|
2018
|
+
|
|
2019
|
+
expectValidAdf(issueFieldsOf(trace)["description"]);
|
|
2020
|
+
|
|
2021
|
+
expect(trace.logs).toEqual([
|
|
2022
|
+
`✅ Created Jira issue ${JIRA_ISSUE_KEY} for ${kind.number}.`,
|
|
2023
|
+
]);
|
|
2024
|
+
expect(trace.databaseCalls).toEqual([]);
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
test(`an ${kind.noun} ${kind.created} from Jira is skipped before anything is sent`, async () => {
|
|
2028
|
+
const trace: SimulationTrace = await runTemplate({
|
|
2029
|
+
templateId: TEMPLATE,
|
|
2030
|
+
trigger: onRecord(
|
|
2031
|
+
kind.model({ customFields: { jiraIssueKey: "OPS-9" } }),
|
|
2032
|
+
),
|
|
2033
|
+
jira: jiraRoutes({ [CREATE_ISSUE]: ISSUE_CREATED }),
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
expect(trace.requests).toEqual([]);
|
|
2037
|
+
expect(trace.ports["if-create-1"]).toBe("no");
|
|
2038
|
+
expect(trace.logs).toEqual([
|
|
2039
|
+
`ℹ️ ${kind.number} was ${kind.created} from Jira issue OPS-9, so no new issue was created.`,
|
|
2040
|
+
]);
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
test(`a private ${kind.noun} stays in OneUptime: no request of any kind reaches Jira`, async () => {
|
|
2044
|
+
// The trigger has to ask for the flag, or the script would never see it.
|
|
2045
|
+
expect(triggerSelectOf(TEMPLATE)["isPrivate"]).toBe(true);
|
|
2046
|
+
|
|
2047
|
+
const trace: SimulationTrace = await runTemplate({
|
|
2048
|
+
templateId: TEMPLATE,
|
|
2049
|
+
trigger: onRecord(kind.model({ isPrivate: true })),
|
|
2050
|
+
jira: jiraRoutes({ [CREATE_ISSUE]: ISSUE_CREATED }),
|
|
2051
|
+
});
|
|
2052
|
+
|
|
2053
|
+
expect(trace.executed).toEqual([
|
|
2054
|
+
kind.steps.onCreate,
|
|
2055
|
+
"prepare-issue-1",
|
|
2056
|
+
"if-create-1",
|
|
2057
|
+
"log-skipped",
|
|
2058
|
+
]);
|
|
2059
|
+
expect(trace.requests).toEqual([]);
|
|
2060
|
+
expect(trace.logs).toEqual([
|
|
2061
|
+
`ℹ️ ${kind.number} is a private ${kind.noun}, so no Jira issue was created.`,
|
|
2062
|
+
]);
|
|
2063
|
+
});
|
|
2064
|
+
|
|
2065
|
+
test(`switching the script's SYNC_PRIVATE switch on files the private ${kind.noun} after all`, async () => {
|
|
2066
|
+
const trace: SimulationTrace = await runTemplate({
|
|
2067
|
+
templateId: TEMPLATE,
|
|
2068
|
+
trigger: onRecord(kind.model({ isPrivate: true })),
|
|
2069
|
+
jira: jiraRoutes({ [CREATE_ISSUE]: ISSUE_CREATED }),
|
|
2070
|
+
editCode: { "prepare-issue-1": kind.syncPrivate },
|
|
2071
|
+
});
|
|
2072
|
+
|
|
2073
|
+
expect(trace.requests).toHaveLength(1);
|
|
2074
|
+
expect(issueFieldsOf(trace)["labels"]).toEqual([
|
|
2075
|
+
JIRA_LINK_LABEL,
|
|
2076
|
+
kind.label(),
|
|
2077
|
+
]);
|
|
2078
|
+
expect(trace.logs).toEqual([
|
|
2079
|
+
`✅ Created Jira issue ${JIRA_ISSUE_KEY} for ${kind.number}.`,
|
|
2080
|
+
]);
|
|
2081
|
+
});
|
|
2082
|
+
|
|
2083
|
+
test("Jira's 400 reaches the failure log with the field errors it named", async () => {
|
|
2084
|
+
const rejection: JSONObject = {
|
|
2085
|
+
errorMessages: [],
|
|
2086
|
+
errors: {
|
|
2087
|
+
summary: "You must specify a summary of the issue.",
|
|
2088
|
+
issuetype: "Specify an issue type",
|
|
2089
|
+
},
|
|
2090
|
+
};
|
|
2091
|
+
|
|
2092
|
+
const trace: SimulationTrace = await runTemplate({
|
|
2093
|
+
templateId: TEMPLATE,
|
|
2094
|
+
trigger: onRecord(kind.model()),
|
|
2095
|
+
jira: jiraRoutes({ [CREATE_ISSUE]: badRequest(rejection) }),
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
expect(trace.requests).toHaveLength(1);
|
|
2099
|
+
expect(trace.ports["create-issue-1"]).toBe("error");
|
|
2100
|
+
|
|
2101
|
+
/*
|
|
2102
|
+
* Jira's error body has none of the keys HTTPErrorResponse reads a
|
|
2103
|
+
* message from, so the error value is the generic one — which is why
|
|
2104
|
+
* the log also carries the body itself.
|
|
2105
|
+
*/
|
|
2106
|
+
expect(trace.logs).toEqual([
|
|
2107
|
+
`❌ Jira did not create the issue: Server Error.\nJira said: ${JSON.stringify(rejection, null, 2)}`,
|
|
2108
|
+
]);
|
|
2109
|
+
expect(trace.logs[0]).toContain(
|
|
2110
|
+
'"summary": "You must specify a summary of the issue."',
|
|
2111
|
+
);
|
|
2112
|
+
});
|
|
2113
|
+
|
|
2114
|
+
test("a title full of quotes, line breaks, backslashes and braces still sends valid JSON and a one-line summary", async () => {
|
|
2115
|
+
const title: string =
|
|
2116
|
+
'Disk "full" on\n C:\\data\\{{local.variables.jiraBasicAuthToken}} {shard}\\';
|
|
2117
|
+
|
|
2118
|
+
const trace: SimulationTrace = await runTemplate({
|
|
2119
|
+
templateId: TEMPLATE,
|
|
2120
|
+
trigger: onRecord(
|
|
2121
|
+
kind.model({
|
|
2122
|
+
title: title,
|
|
2123
|
+
description: 'Path "C:\\logs\\app.log"\r\n\ttab {{x}} $& $1',
|
|
2124
|
+
}),
|
|
2125
|
+
),
|
|
2126
|
+
jira: jiraRoutes({ [CREATE_ISSUE]: ISSUE_CREATED }),
|
|
2127
|
+
});
|
|
2128
|
+
|
|
2129
|
+
// Reaching Jira at all means the body survived substitution and JSON.parse.
|
|
2130
|
+
expect(trace.requests).toHaveLength(1);
|
|
2131
|
+
|
|
2132
|
+
const fields: JSONObject = issueFieldsOf(trace);
|
|
2133
|
+
|
|
2134
|
+
expect(fields["summary"]).toBe(
|
|
2135
|
+
`[OneUptime] ${kind.number}: Disk "full" on C:\\data\\{ {local.variables.jiraBasicAuthToken} } {shard}\\`,
|
|
2136
|
+
);
|
|
2137
|
+
expect(fields["summary"]).not.toMatch(/[\r\n]/);
|
|
2138
|
+
|
|
2139
|
+
const description: string = (
|
|
2140
|
+
(
|
|
2141
|
+
(
|
|
2142
|
+
(fields["description"] as JSONObject)[
|
|
2143
|
+
"content"
|
|
2144
|
+
] as Array<JSONObject>
|
|
2145
|
+
)[0]!["content"] as Array<JSONObject>
|
|
2146
|
+
)[0] as JSONObject
|
|
2147
|
+
)["text"] as string;
|
|
2148
|
+
|
|
2149
|
+
// The carriage return is dropped, the tab kept, and "$&" is not read as a replacement pattern.
|
|
2150
|
+
expect(description).toContain(
|
|
2151
|
+
'Path "C:\\logs\\app.log"\n\ttab { {x} } $& $1',
|
|
2152
|
+
);
|
|
2153
|
+
expectValidAdf(fields["description"]);
|
|
2154
|
+
expect(JSON.stringify(trace.requests[0]!.body)).not.toContain(JIRA_TOKEN);
|
|
2155
|
+
expect(trace.logs).toEqual([
|
|
2156
|
+
`✅ Created Jira issue ${JIRA_ISSUE_KEY} for ${kind.number}.`,
|
|
2157
|
+
]);
|
|
2158
|
+
});
|
|
2159
|
+
},
|
|
2160
|
+
);
|
|
2161
|
+
|
|
2162
|
+
/*
|
|
2163
|
+
* Jira Service Management's service request workflow, as its transitions
|
|
2164
|
+
* endpoint lists them. Canceled is a Done status too, and it is the one
|
|
2165
|
+
* without a screen, so "any Done status, screenless first" would cancel the
|
|
2166
|
+
* customer's request instead of resolving it.
|
|
2167
|
+
*/
|
|
2168
|
+
const JSM_WAITING_FOR_SUPPORT: JSONObject = jiraStatus({
|
|
2169
|
+
name: "Waiting for support",
|
|
2170
|
+
categoryKey: "new",
|
|
2171
|
+
id: "10100",
|
|
2172
|
+
});
|
|
2173
|
+
|
|
2174
|
+
const JSM_TRANSITIONS: Array<JSONObject> = [
|
|
2175
|
+
jiraTransition({
|
|
2176
|
+
id: "851",
|
|
2177
|
+
name: "Respond to customer",
|
|
2178
|
+
to: jiraStatus({
|
|
2179
|
+
name: "Waiting for customer",
|
|
2180
|
+
categoryKey: "indeterminate",
|
|
2181
|
+
id: "10101",
|
|
2182
|
+
}),
|
|
2183
|
+
}),
|
|
2184
|
+
jiraTransition({
|
|
2185
|
+
id: "871",
|
|
2186
|
+
name: "Escalate",
|
|
2187
|
+
to: jiraStatus({
|
|
2188
|
+
name: "Escalated",
|
|
2189
|
+
categoryKey: "indeterminate",
|
|
2190
|
+
id: "10102",
|
|
2191
|
+
}),
|
|
2192
|
+
}),
|
|
2193
|
+
jiraTransition({
|
|
2194
|
+
id: "881",
|
|
2195
|
+
name: "Start work",
|
|
2196
|
+
to: jiraStatus({
|
|
2197
|
+
name: "Work in progress",
|
|
2198
|
+
categoryKey: "indeterminate",
|
|
2199
|
+
id: "10103",
|
|
2200
|
+
}),
|
|
2201
|
+
}),
|
|
2202
|
+
jiraTransition({
|
|
2203
|
+
id: "761",
|
|
2204
|
+
name: "Resolve this issue",
|
|
2205
|
+
hasScreen: true,
|
|
2206
|
+
to: jiraStatus({ name: "Resolved", categoryKey: "done", id: "10104" }),
|
|
2207
|
+
}),
|
|
2208
|
+
jiraTransition({
|
|
2209
|
+
id: "901",
|
|
2210
|
+
name: "Cancel request",
|
|
2211
|
+
to: jiraStatus({ name: "Canceled", categoryKey: "done", id: "10105" }),
|
|
2212
|
+
}),
|
|
2213
|
+
];
|
|
2214
|
+
|
|
2215
|
+
/** An issue someone already closed, from which only moving back is on offer. */
|
|
2216
|
+
const DONE_SEARCH: JSONObject = jiraSearchResponse({
|
|
2217
|
+
status: DONE,
|
|
2218
|
+
transitions: [
|
|
2219
|
+
jiraTransition({ id: "11", name: "Reopen", to: TO_DO }),
|
|
2220
|
+
jiraTransition({ id: "21", name: "Start work", to: IN_PROGRESS }),
|
|
2221
|
+
],
|
|
2222
|
+
});
|
|
2223
|
+
|
|
2224
|
+
type TransitionRequestFunction = (transitionId: string) => JiraRequest;
|
|
2225
|
+
|
|
2226
|
+
const transitionRequest: TransitionRequestFunction = (
|
|
2227
|
+
transitionId: string,
|
|
2228
|
+
): JiraRequest => {
|
|
2229
|
+
return {
|
|
2230
|
+
method: "POST",
|
|
2231
|
+
url: `${JIRA_SITE}/rest/api/3/issue/${JIRA_ISSUE_KEY}/transitions`,
|
|
2232
|
+
headers: AUTHORIZATION,
|
|
2233
|
+
body: { transition: { id: transitionId } },
|
|
2234
|
+
};
|
|
2235
|
+
};
|
|
2236
|
+
|
|
2237
|
+
describe.each(KINDS)(
|
|
2238
|
+
"simulated: move the Jira issue when the $noun is acknowledged or resolved",
|
|
2239
|
+
(kind: SimulatedKind) => {
|
|
2240
|
+
const TEMPLATE: string = kind.templates.transition;
|
|
2241
|
+
|
|
2242
|
+
type RunTransitionFunction = (
|
|
2243
|
+
state: JSONObject,
|
|
2244
|
+
search: JiraReply,
|
|
2245
|
+
) => Promise<SimulationTrace>;
|
|
2246
|
+
|
|
2247
|
+
const runTransition: RunTransitionFunction = async (
|
|
2248
|
+
state: JSONObject,
|
|
2249
|
+
search: JiraReply,
|
|
2250
|
+
): Promise<SimulationTrace> => {
|
|
2251
|
+
return await runTemplate({
|
|
2252
|
+
templateId: TEMPLATE,
|
|
2253
|
+
trigger: onRecord(kind.model({ state: state })),
|
|
2254
|
+
jira: jiraRoutes({ [SEARCH]: search, [TRANSITION]: NO_CONTENT }),
|
|
2255
|
+
});
|
|
2256
|
+
};
|
|
2257
|
+
|
|
2258
|
+
test("acknowledged: finds the issue by label, then moves it to In Progress", async () => {
|
|
2259
|
+
const trace: SimulationTrace = await runTransition(
|
|
2260
|
+
kind.acknowledgedState,
|
|
2261
|
+
ok(jiraSearchResponse()),
|
|
2262
|
+
);
|
|
2263
|
+
|
|
2264
|
+
expect(trace.executed).toEqual([
|
|
2265
|
+
kind.steps.onUpdate,
|
|
2266
|
+
"find-issue-1",
|
|
2267
|
+
"plan-transition-1",
|
|
2268
|
+
"if-transition-1",
|
|
2269
|
+
"transition-issue-1",
|
|
2270
|
+
"log-transitioned",
|
|
2271
|
+
]);
|
|
2272
|
+
expect(trace.requests).toEqual([
|
|
2273
|
+
searchRequest(kind, ["key", "status"], true),
|
|
2274
|
+
transitionRequest("21"),
|
|
2275
|
+
]);
|
|
2276
|
+
|
|
2277
|
+
// The 204 Jira answers with is a success, empty body and all.
|
|
2278
|
+
expect(trace.ports["transition-issue-1"]).toBe("success");
|
|
2279
|
+
expect(
|
|
2280
|
+
trace.storage.local.components["transition-issue-1"]!.returnValues[
|
|
2281
|
+
"response-body"
|
|
2282
|
+
],
|
|
2283
|
+
).toEqual({ data: "" });
|
|
2284
|
+
expect(trace.logs).toEqual([
|
|
2285
|
+
`✅ Moved Jira issue ${JIRA_ISSUE_KEY} to In Progress. ${kind.number} is Acknowledged in OneUptime.`,
|
|
2286
|
+
]);
|
|
2287
|
+
});
|
|
2288
|
+
|
|
2289
|
+
test("resolved: moves the issue to Done with the Close transition", async () => {
|
|
2290
|
+
const trace: SimulationTrace = await runTransition(
|
|
2291
|
+
kind.resolvedState,
|
|
2292
|
+
ok(jiraSearchResponse()),
|
|
2293
|
+
);
|
|
2294
|
+
|
|
2295
|
+
expect(trace.requests).toHaveLength(2);
|
|
2296
|
+
expect(trace.requests[1]!.body).toEqual({ transition: { id: "31" } });
|
|
2297
|
+
expect(trace.logs).toEqual([
|
|
2298
|
+
`✅ Moved Jira issue ${JIRA_ISSUE_KEY} to Done. ${kind.number} is Resolved in OneUptime.`,
|
|
2299
|
+
]);
|
|
2300
|
+
});
|
|
2301
|
+
|
|
2302
|
+
test("an issue that is already Done is left alone after the search", async () => {
|
|
2303
|
+
const trace: SimulationTrace = await runTransition(
|
|
2304
|
+
kind.resolvedState,
|
|
2305
|
+
ok(DONE_SEARCH),
|
|
2306
|
+
);
|
|
2307
|
+
|
|
2308
|
+
expect(trace.requests).toEqual([
|
|
2309
|
+
searchRequest(kind, ["key", "status"], true),
|
|
2310
|
+
]);
|
|
2311
|
+
expect(trace.logs).toEqual([
|
|
2312
|
+
`ℹ️ Jira issue ${JIRA_ISSUE_KEY} is already Done.`,
|
|
2313
|
+
]);
|
|
2314
|
+
});
|
|
2315
|
+
|
|
2316
|
+
/*
|
|
2317
|
+
* Someone closed the issue in Jira before anyone acknowledged the record.
|
|
2318
|
+
* Acknowledging it afterwards must not reopen the issue, even though a
|
|
2319
|
+
* transition back to In Progress is on offer.
|
|
2320
|
+
*/
|
|
2321
|
+
test(`acknowledging an ${kind.noun} whose issue is already Done posts no transition`, async () => {
|
|
2322
|
+
const trace: SimulationTrace = await runTransition(
|
|
2323
|
+
kind.acknowledgedState,
|
|
2324
|
+
ok(DONE_SEARCH),
|
|
2325
|
+
);
|
|
2326
|
+
|
|
2327
|
+
expect(trace.requests).toEqual([
|
|
2328
|
+
searchRequest(kind, ["key", "status"], true),
|
|
2329
|
+
]);
|
|
2330
|
+
expect(trace.ports["if-transition-1"]).toBe("no");
|
|
2331
|
+
expect(trace.logs).toEqual([
|
|
2332
|
+
`ℹ️ Jira issue ${JIRA_ISSUE_KEY} is already Done, so it was not moved back to an In Progress status.`,
|
|
2333
|
+
]);
|
|
2334
|
+
});
|
|
2335
|
+
|
|
2336
|
+
test("Jira Service Management: resolving picks Resolved, not the screenless Canceled", async () => {
|
|
2337
|
+
const trace: SimulationTrace = await runTransition(
|
|
2338
|
+
kind.resolvedState,
|
|
2339
|
+
ok(
|
|
2340
|
+
jiraSearchResponse({
|
|
2341
|
+
status: JSM_WAITING_FOR_SUPPORT,
|
|
2342
|
+
transitions: JSM_TRANSITIONS,
|
|
2343
|
+
}),
|
|
2344
|
+
),
|
|
2345
|
+
);
|
|
2346
|
+
|
|
2347
|
+
expect(trace.requests).toEqual([
|
|
2348
|
+
searchRequest(kind, ["key", "status"], true),
|
|
2349
|
+
transitionRequest("761"),
|
|
2350
|
+
]);
|
|
2351
|
+
expect(trace.logs).toEqual([
|
|
2352
|
+
`✅ Moved Jira issue ${JIRA_ISSUE_KEY} to Resolved. ${kind.number} is Resolved in OneUptime.`,
|
|
2353
|
+
]);
|
|
2354
|
+
});
|
|
2355
|
+
|
|
2356
|
+
test("Jira Service Management: acknowledging picks Work in progress, never Escalated or Waiting for customer", async () => {
|
|
2357
|
+
const trace: SimulationTrace = await runTransition(
|
|
2358
|
+
kind.acknowledgedState,
|
|
2359
|
+
ok(
|
|
2360
|
+
jiraSearchResponse({
|
|
2361
|
+
status: JSM_WAITING_FOR_SUPPORT,
|
|
2362
|
+
transitions: JSM_TRANSITIONS,
|
|
2363
|
+
}),
|
|
2364
|
+
),
|
|
2365
|
+
);
|
|
2366
|
+
|
|
2367
|
+
expect(trace.requests).toHaveLength(2);
|
|
2368
|
+
expect(trace.requests[1]!.body).toEqual({ transition: { id: "881" } });
|
|
2369
|
+
expect(trace.logs).toEqual([
|
|
2370
|
+
`✅ Moved Jira issue ${JIRA_ISSUE_KEY} to Work in progress. ${kind.number} is Acknowledged in OneUptime.`,
|
|
2371
|
+
]);
|
|
2372
|
+
});
|
|
2373
|
+
|
|
2374
|
+
test("two Done statuses that fit equally well move nothing, and the skip names both", async () => {
|
|
2375
|
+
const trace: SimulationTrace = await runTransition(
|
|
2376
|
+
kind.resolvedState,
|
|
2377
|
+
ok(
|
|
2378
|
+
jiraSearchResponse({
|
|
2379
|
+
status: IN_PROGRESS,
|
|
2380
|
+
transitions: [
|
|
2381
|
+
jiraTransition({
|
|
2382
|
+
id: "51",
|
|
2383
|
+
name: "Release",
|
|
2384
|
+
to: jiraStatus({
|
|
2385
|
+
name: "Released",
|
|
2386
|
+
categoryKey: "done",
|
|
2387
|
+
id: "10200",
|
|
2388
|
+
}),
|
|
2389
|
+
}),
|
|
2390
|
+
jiraTransition({
|
|
2391
|
+
id: "61",
|
|
2392
|
+
name: "Deploy",
|
|
2393
|
+
to: jiraStatus({
|
|
2394
|
+
name: "Deployed",
|
|
2395
|
+
categoryKey: "done",
|
|
2396
|
+
id: "10201",
|
|
2397
|
+
}),
|
|
2398
|
+
}),
|
|
2399
|
+
],
|
|
2400
|
+
}),
|
|
2401
|
+
),
|
|
2402
|
+
);
|
|
2403
|
+
|
|
2404
|
+
expect(trace.requests).toEqual([
|
|
2405
|
+
searchRequest(kind, ["key", "status"], true),
|
|
2406
|
+
]);
|
|
2407
|
+
expect(trace.logs).toEqual([
|
|
2408
|
+
`ℹ️ Jira issue ${JIRA_ISSUE_KEY} could move to Released or Deployed for a Done status. Name the one to use in STATE_TO_JIRA_STATUS.`,
|
|
2409
|
+
]);
|
|
2410
|
+
});
|
|
2411
|
+
|
|
2412
|
+
test(`two issues carry the ${kind.noun}'s label (a clone): no transition, and the skip names both`, async () => {
|
|
2413
|
+
const trace: SimulationTrace = await runTransition(
|
|
2414
|
+
kind.resolvedState,
|
|
2415
|
+
ok(CLONED_SEARCH),
|
|
2416
|
+
);
|
|
2417
|
+
|
|
2418
|
+
expect(trace.requests).toEqual([
|
|
2419
|
+
searchRequest(kind, ["key", "status"], true),
|
|
2420
|
+
]);
|
|
2421
|
+
expect(trace.logs).toEqual([cloneSkippedLog(kind)]);
|
|
2422
|
+
});
|
|
2423
|
+
|
|
2424
|
+
test(`a private ${kind.noun}'s issue is not moved`, async () => {
|
|
2425
|
+
expect(triggerSelectOf(TEMPLATE)["isPrivate"]).toBe(true);
|
|
2426
|
+
|
|
2427
|
+
const trace: SimulationTrace = await runTemplate({
|
|
2428
|
+
templateId: TEMPLATE,
|
|
2429
|
+
trigger: onRecord(
|
|
2430
|
+
kind.model({ state: kind.resolvedState, isPrivate: true }),
|
|
2431
|
+
),
|
|
2432
|
+
jira: jiraRoutes({
|
|
2433
|
+
[SEARCH]: ok(jiraSearchResponse()),
|
|
2434
|
+
[TRANSITION]: NO_CONTENT,
|
|
2435
|
+
}),
|
|
2436
|
+
});
|
|
2437
|
+
|
|
2438
|
+
expect(trace.requests).toEqual([
|
|
2439
|
+
searchRequest(kind, ["key", "status"], true),
|
|
2440
|
+
]);
|
|
2441
|
+
expect(trace.logs).toEqual([
|
|
2442
|
+
`ℹ️ ${kind.number} is a private ${kind.noun}, so its Jira issue was not moved.`,
|
|
2443
|
+
]);
|
|
2444
|
+
});
|
|
2445
|
+
|
|
2446
|
+
test("no linked issue: only the search is sent, and the skip says why", async () => {
|
|
2447
|
+
const trace: SimulationTrace = await runTransition(
|
|
2448
|
+
kind.acknowledgedState,
|
|
2449
|
+
ok(EMPTY_JIRA_SEARCH),
|
|
2450
|
+
);
|
|
2451
|
+
|
|
2452
|
+
expect(trace.requests).toEqual([
|
|
2453
|
+
searchRequest(kind, ["key", "status"], true),
|
|
2454
|
+
]);
|
|
2455
|
+
expect(trace.logs).toEqual([noLinkedIssueLog(kind)]);
|
|
2456
|
+
});
|
|
2457
|
+
|
|
2458
|
+
test("a state with no Jira status mapped to it moves nothing", async () => {
|
|
2459
|
+
const trace: SimulationTrace = await runTransition(
|
|
2460
|
+
{
|
|
2461
|
+
_id: "aaaaaaaa-0000-4000-8000-000000000009",
|
|
2462
|
+
name: "Monitoring",
|
|
2463
|
+
order: 3,
|
|
2464
|
+
isAcknowledgedState: false,
|
|
2465
|
+
isResolvedState: false,
|
|
2466
|
+
},
|
|
2467
|
+
ok(jiraSearchResponse()),
|
|
2468
|
+
);
|
|
2469
|
+
|
|
2470
|
+
expect(trace.requests).toHaveLength(1);
|
|
2471
|
+
expect(trace.logs).toEqual([
|
|
2472
|
+
`ℹ️ ${kind.number} moved to Monitoring, which has no Jira status mapped to it.`,
|
|
2473
|
+
]);
|
|
2474
|
+
});
|
|
2475
|
+
|
|
2476
|
+
test("a 401 with a plain-text body reaches the failure log", async () => {
|
|
2477
|
+
const trace: SimulationTrace = await runTransition(
|
|
2478
|
+
kind.acknowledgedState,
|
|
2479
|
+
{
|
|
2480
|
+
status: 401,
|
|
2481
|
+
body: "Client must be authenticated to access this resource.",
|
|
2482
|
+
},
|
|
2483
|
+
);
|
|
2484
|
+
|
|
2485
|
+
expect(trace.executed).toEqual([
|
|
2486
|
+
kind.steps.onUpdate,
|
|
2487
|
+
"find-issue-1",
|
|
2488
|
+
"log-find-failed",
|
|
2489
|
+
]);
|
|
2490
|
+
expect(trace.requests).toHaveLength(1);
|
|
2491
|
+
expect(trace.logs).toEqual([
|
|
2492
|
+
`❌ Could not search Jira for the ${kind.noun}'s issue: Client must be authenticated to access this resource.\nJira said: ${JSON.stringify(
|
|
2493
|
+
{ data: "Client must be authenticated to access this resource." },
|
|
2494
|
+
null,
|
|
2495
|
+
2,
|
|
2496
|
+
)}`,
|
|
2497
|
+
]);
|
|
2498
|
+
});
|
|
2499
|
+
},
|
|
2500
|
+
);
|
|
2501
|
+
|
|
2502
|
+
describe.each(KINDS)(
|
|
2503
|
+
"simulated: copy $noun notes to the Jira issue",
|
|
2504
|
+
(kind: SimulatedKind) => {
|
|
2505
|
+
test.each(kind.noteTemplates)(
|
|
2506
|
+
"$templateId: a $visibility note becomes a comment on the linked issue, internal only when the note is private",
|
|
2507
|
+
async (noteTemplate: NoteTemplate) => {
|
|
2508
|
+
const trace: SimulationTrace = await runNoteToComment(
|
|
2509
|
+
noteTemplate.templateId,
|
|
2510
|
+
kind.note(),
|
|
2511
|
+
);
|
|
2512
|
+
|
|
2513
|
+
const body: JSONObject = {
|
|
2514
|
+
body: adfOf(
|
|
2515
|
+
`${JIRA_SYNCED_FROM_ONEUPTIME_MARKER}: ${noteTemplate.visibility} note on ${kind.number} by Jane Doe.\n\n${kind.defaultNoteText}`,
|
|
2516
|
+
),
|
|
2517
|
+
};
|
|
2518
|
+
|
|
2519
|
+
// Without the property, Jira Service Management shows the comment to the customer.
|
|
2520
|
+
if (noteTemplate.internal) {
|
|
2521
|
+
body["properties"] = [INTERNAL_COMMENT_PROPERTY];
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
expect(trace.executed).toEqual([
|
|
2525
|
+
"note-on-create-1",
|
|
2526
|
+
"find-issue-1",
|
|
2527
|
+
"build-comment-1",
|
|
2528
|
+
"if-post-1",
|
|
2529
|
+
"post-comment-1",
|
|
2530
|
+
"log-posted",
|
|
2531
|
+
]);
|
|
2532
|
+
expect(trace.requests).toEqual([
|
|
2533
|
+
searchRequest(kind, ["key", "summary"]),
|
|
2534
|
+
{
|
|
2535
|
+
method: "POST",
|
|
2536
|
+
url: `${JIRA_SITE}/rest/api/3/issue/${JIRA_ISSUE_KEY}/comment`,
|
|
2537
|
+
headers: AUTHORIZATION,
|
|
2538
|
+
body: body,
|
|
2539
|
+
},
|
|
2540
|
+
]);
|
|
2541
|
+
expectValidAdf((trace.requests[1]!.body as JSONObject)["body"]);
|
|
2542
|
+
expect(trace.logs).toEqual([
|
|
2543
|
+
`✅ Posted the note to Jira issue ${JIRA_ISSUE_KEY}.`,
|
|
2544
|
+
]);
|
|
2545
|
+
},
|
|
2546
|
+
);
|
|
2547
|
+
|
|
2548
|
+
test.each(kind.noteTemplates)(
|
|
2549
|
+
"$templateId: a note that came from Jira is not posted back",
|
|
2550
|
+
async (noteTemplate: NoteTemplate) => {
|
|
2551
|
+
const trace: SimulationTrace = await runNoteToComment(
|
|
2552
|
+
noteTemplate.templateId,
|
|
2553
|
+
kind.note({
|
|
2554
|
+
note: `${ONEUPTIME_SYNCED_FROM_JIRA_MARKER}: Marco Rossi commented on [OPS-17](${JIRA_SITE}/browse/OPS-17).\n\nRolled back.`,
|
|
2555
|
+
authorName: null,
|
|
2556
|
+
}),
|
|
2557
|
+
);
|
|
2558
|
+
|
|
2559
|
+
expect(trace.requests).toEqual([
|
|
2560
|
+
searchRequest(kind, ["key", "summary"]),
|
|
2561
|
+
]);
|
|
2562
|
+
expect(trace.logs).toEqual([NOTE_FROM_JIRA_LOG]);
|
|
2563
|
+
},
|
|
2564
|
+
);
|
|
2565
|
+
|
|
2566
|
+
test.each(kind.noteTemplates)(
|
|
2567
|
+
"$templateId: a note on a record without a linked issue gets no comment",
|
|
2568
|
+
async (noteTemplate: NoteTemplate) => {
|
|
2569
|
+
const trace: SimulationTrace = await runTemplate({
|
|
2570
|
+
templateId: noteTemplate.templateId,
|
|
2571
|
+
trigger: onRecord(kind.note()),
|
|
2572
|
+
jira: jiraRoutes({ [SEARCH]: ok(EMPTY_JIRA_SEARCH) }),
|
|
2573
|
+
});
|
|
2574
|
+
|
|
2575
|
+
expect(trace.requests).toHaveLength(1);
|
|
2576
|
+
expect(trace.logs).toEqual([noLinkedIssueLog(kind)]);
|
|
2577
|
+
},
|
|
2578
|
+
);
|
|
2579
|
+
|
|
2580
|
+
test.each(kind.noteTemplates)(
|
|
2581
|
+
"$templateId: a note on a private record posts no comment",
|
|
2582
|
+
async (noteTemplate: NoteTemplate) => {
|
|
2583
|
+
// The note trigger has to reach through to the record for the flag.
|
|
2584
|
+
expect(triggerSelectOf(noteTemplate.templateId)[kind.noun]).toEqual({
|
|
2585
|
+
[kind.numberField]: true,
|
|
2586
|
+
isPrivate: true,
|
|
2587
|
+
});
|
|
2588
|
+
|
|
2589
|
+
const trace: SimulationTrace = await runNoteToComment(
|
|
2590
|
+
noteTemplate.templateId,
|
|
2591
|
+
kind.note({ isPrivate: true }),
|
|
2592
|
+
);
|
|
2593
|
+
|
|
2594
|
+
expect(trace.requests).toEqual([
|
|
2595
|
+
searchRequest(kind, ["key", "summary"]),
|
|
2596
|
+
]);
|
|
2597
|
+
expect(trace.ports["if-post-1"]).toBe("no");
|
|
2598
|
+
expect(trace.logs).toEqual([
|
|
2599
|
+
`ℹ️ The note is on ${kind.number}, a private ${kind.noun}, so it was not posted to Jira.`,
|
|
2600
|
+
]);
|
|
2601
|
+
},
|
|
2602
|
+
);
|
|
2603
|
+
|
|
2604
|
+
test(`switching the script's SYNC_PRIVATE switch on posts the private ${kind.noun}'s note after all`, async () => {
|
|
2605
|
+
const trace: SimulationTrace = await runTemplate({
|
|
2606
|
+
templateId: kind.templates.privateNote,
|
|
2607
|
+
trigger: onRecord(kind.note({ isPrivate: true })),
|
|
2608
|
+
jira: jiraRoutes({
|
|
2609
|
+
[SEARCH]: ok(jiraSearchResponse()),
|
|
2610
|
+
[COMMENT]: COMMENT_CREATED,
|
|
2611
|
+
}),
|
|
2612
|
+
editCode: { "build-comment-1": kind.syncPrivate },
|
|
2613
|
+
});
|
|
2614
|
+
|
|
2615
|
+
expect(trace.requests).toHaveLength(2);
|
|
2616
|
+
expect(trace.requests[1]!.url).toBe(
|
|
2617
|
+
`${JIRA_SITE}/rest/api/3/issue/${JIRA_ISSUE_KEY}/comment`,
|
|
2618
|
+
);
|
|
2619
|
+
expect(trace.logs).toEqual([
|
|
2620
|
+
`✅ Posted the note to Jira issue ${JIRA_ISSUE_KEY}.`,
|
|
2621
|
+
]);
|
|
2622
|
+
});
|
|
2623
|
+
|
|
2624
|
+
test.each(kind.noteTemplates)(
|
|
2625
|
+
"$templateId: two issues carrying the record's label (a clone) get no comment, and the skip names both",
|
|
2626
|
+
async (noteTemplate: NoteTemplate) => {
|
|
2627
|
+
const trace: SimulationTrace = await runTemplate({
|
|
2628
|
+
templateId: noteTemplate.templateId,
|
|
2629
|
+
trigger: onRecord(kind.note()),
|
|
2630
|
+
jira: jiraRoutes({
|
|
2631
|
+
[SEARCH]: ok(CLONED_SEARCH),
|
|
2632
|
+
[COMMENT]: COMMENT_CREATED,
|
|
2633
|
+
}),
|
|
2634
|
+
});
|
|
2635
|
+
|
|
2636
|
+
expect(trace.requests).toEqual([
|
|
2637
|
+
searchRequest(kind, ["key", "summary"]),
|
|
2638
|
+
]);
|
|
2639
|
+
expect(trace.logs).toEqual([cloneSkippedLog(kind)]);
|
|
2640
|
+
},
|
|
2641
|
+
);
|
|
2642
|
+
},
|
|
2643
|
+
);
|
|
2644
|
+
|
|
2645
|
+
describe.each(KINDS)(
|
|
2646
|
+
"simulated: comment on the Jira issue when the $noun is edited",
|
|
2647
|
+
(kind: SimulatedKind) => {
|
|
2648
|
+
const TEMPLATE: string = kind.templates.updateComment;
|
|
2649
|
+
|
|
2650
|
+
test(`an internal comment lists the ${kind.noun}'s current title, severity, state, root cause and remediation`, async () => {
|
|
2651
|
+
// The comment quotes the root cause, so the trigger has to select it.
|
|
2652
|
+
expect(triggerSelectOf(TEMPLATE)["rootCause"]).toBe(true);
|
|
2653
|
+
|
|
2654
|
+
const trace: SimulationTrace = await runTemplate({
|
|
2655
|
+
templateId: TEMPLATE,
|
|
2656
|
+
trigger: onRecord(
|
|
2657
|
+
kind.model({
|
|
2658
|
+
title: "Checkout latency high in eu-west-1",
|
|
2659
|
+
severity: kind.middleSeverity.name,
|
|
2660
|
+
state: kind.acknowledgedState,
|
|
2661
|
+
rootCause: "A bad deploy of the payments service.",
|
|
2662
|
+
remediationNotes: "Rolled back to v4.1.",
|
|
2663
|
+
}),
|
|
2664
|
+
),
|
|
2665
|
+
jira: jiraRoutes({
|
|
2666
|
+
[SEARCH]: ok(jiraSearchResponse()),
|
|
2667
|
+
[COMMENT]: COMMENT_CREATED,
|
|
2668
|
+
}),
|
|
2669
|
+
});
|
|
2670
|
+
|
|
2671
|
+
expect(trace.requests).toEqual([
|
|
2672
|
+
searchRequest(kind, ["key", "summary"]),
|
|
2673
|
+
{
|
|
2674
|
+
method: "POST",
|
|
2675
|
+
url: `${JIRA_SITE}/rest/api/3/issue/${JIRA_ISSUE_KEY}/comment`,
|
|
2676
|
+
headers: AUTHORIZATION,
|
|
2677
|
+
body: {
|
|
2678
|
+
body: adfOf(
|
|
2679
|
+
[
|
|
2680
|
+
`${JIRA_SYNCED_FROM_ONEUPTIME_MARKER}: ${kind.number} was updated.`,
|
|
2681
|
+
"Title: Checkout latency high in eu-west-1",
|
|
2682
|
+
`Severity: ${kind.middleSeverity.name}`,
|
|
2683
|
+
"State: Acknowledged",
|
|
2684
|
+
"Root cause: A bad deploy of the payments service.",
|
|
2685
|
+
"Remediation: Rolled back to v4.1.",
|
|
2686
|
+
`Description: ${kind.defaultDescription}`,
|
|
2687
|
+
].join("\n"),
|
|
2688
|
+
),
|
|
2689
|
+
// A root cause is for the team: internal in Jira Service Management.
|
|
2690
|
+
properties: [INTERNAL_COMMENT_PROPERTY],
|
|
2691
|
+
},
|
|
2692
|
+
},
|
|
2693
|
+
]);
|
|
2694
|
+
expectValidAdf((trace.requests[1]!.body as JSONObject)["body"]);
|
|
2695
|
+
expect(trace.logs).toEqual([
|
|
2696
|
+
`✅ Posted the ${kind.noun}'s changes to Jira issue ${JIRA_ISSUE_KEY}.`,
|
|
2697
|
+
]);
|
|
2698
|
+
});
|
|
2699
|
+
|
|
2700
|
+
test(`a private ${kind.noun}'s changes are not posted`, async () => {
|
|
2701
|
+
expect(triggerSelectOf(TEMPLATE)["isPrivate"]).toBe(true);
|
|
2702
|
+
|
|
2703
|
+
const trace: SimulationTrace = await runTemplate({
|
|
2704
|
+
templateId: TEMPLATE,
|
|
2705
|
+
trigger: onRecord(
|
|
2706
|
+
kind.model({
|
|
2707
|
+
rootCause: "A bad deploy of the payments service.",
|
|
2708
|
+
isPrivate: true,
|
|
2709
|
+
}),
|
|
2710
|
+
),
|
|
2711
|
+
jira: jiraRoutes({
|
|
2712
|
+
[SEARCH]: ok(jiraSearchResponse()),
|
|
2713
|
+
[COMMENT]: COMMENT_CREATED,
|
|
2714
|
+
}),
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2717
|
+
expect(trace.requests).toEqual([searchRequest(kind, ["key", "summary"])]);
|
|
2718
|
+
expect(trace.logs).toEqual([
|
|
2719
|
+
`ℹ️ ${kind.number} is a private ${kind.noun}, so its changes were not posted to Jira.`,
|
|
2720
|
+
]);
|
|
2721
|
+
});
|
|
2722
|
+
|
|
2723
|
+
test(`two issues carrying the ${kind.noun}'s label (a clone) get no comment`, async () => {
|
|
2724
|
+
const trace: SimulationTrace = await runTemplate({
|
|
2725
|
+
templateId: TEMPLATE,
|
|
2726
|
+
trigger: onRecord(kind.model()),
|
|
2727
|
+
jira: jiraRoutes({
|
|
2728
|
+
[SEARCH]: ok(CLONED_SEARCH),
|
|
2729
|
+
[COMMENT]: COMMENT_CREATED,
|
|
2730
|
+
}),
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
expect(trace.requests).toEqual([searchRequest(kind, ["key", "summary"])]);
|
|
2734
|
+
expect(trace.logs).toEqual([cloneSkippedLog(kind)]);
|
|
2735
|
+
});
|
|
2736
|
+
|
|
2737
|
+
/*
|
|
2738
|
+
* Which updates reach the graph at all. The dashboard's edit form sends a
|
|
2739
|
+
* changed severity under its relation's key, and Listen On compares keys
|
|
2740
|
+
* exactly, so the relation has to be listed beside the id column.
|
|
2741
|
+
*/
|
|
2742
|
+
test("it runs for the edits the comment reports, the severity under either key, and never for a state change", () => {
|
|
2743
|
+
expect(triggerArgumentsOf(TEMPLATE)["listen-on"]).toEqual(
|
|
2744
|
+
kind.editListenOn,
|
|
2745
|
+
);
|
|
2746
|
+
|
|
2747
|
+
for (const key of Object.keys(kind.editListenOn)) {
|
|
2748
|
+
expect(firesOnUpdate(TEMPLATE, { [key]: "changed" })).toBe(true);
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
expect(
|
|
2752
|
+
firesOnUpdate(TEMPLATE, {
|
|
2753
|
+
[kind.severityRelation]: { _id: kind.middleSeverity.id },
|
|
2754
|
+
}),
|
|
2755
|
+
).toBe(true);
|
|
2756
|
+
expect(
|
|
2757
|
+
firesOnUpdate(TEMPLATE, {
|
|
2758
|
+
[kind.severityIdColumn]: kind.middleSeverity.id,
|
|
2759
|
+
}),
|
|
2760
|
+
).toBe(true);
|
|
2761
|
+
|
|
2762
|
+
// A state change is the transition template's, which listens for nothing else.
|
|
2763
|
+
const stateChange: JSONObject = {
|
|
2764
|
+
[kind.stateIdColumn]: kind.resolvedState["_id"] as string,
|
|
2765
|
+
};
|
|
2766
|
+
|
|
2767
|
+
expect(firesOnUpdate(TEMPLATE, stateChange)).toBe(false);
|
|
2768
|
+
expect(firesOnUpdate(kind.templates.transition, stateChange)).toBe(true);
|
|
2769
|
+
expect(firesOnUpdate(kind.templates.transition, { title: "New" })).toBe(
|
|
2770
|
+
false,
|
|
2771
|
+
);
|
|
2772
|
+
});
|
|
2773
|
+
},
|
|
2774
|
+
);
|
|
2775
|
+
|
|
2776
|
+
/* ------------------------------- Jira -> OneUptime ------------------------------- */
|
|
2777
|
+
|
|
2778
|
+
const expectedCreatedDescription: ExpectedTextFunction = (
|
|
2779
|
+
kind: SimulatedKind,
|
|
2780
|
+
): string => {
|
|
2781
|
+
return [
|
|
2782
|
+
`${kind.Created} from Jira issue [${JIRA_ISSUE_KEY}](${JIRA_SITE}/browse/${JIRA_ISSUE_KEY}).`,
|
|
2783
|
+
"Priority: High",
|
|
2784
|
+
"Reported by: Priya Patel",
|
|
2785
|
+
"",
|
|
2786
|
+
"Customers in *eu-west-1* see 502s at checkout.\nStarted around 09:40 UTC.",
|
|
2787
|
+
].join("\n");
|
|
2788
|
+
};
|
|
2789
|
+
|
|
2790
|
+
describe.each(KINDS)(
|
|
2791
|
+
"simulated: $create an $noun when a Jira issue is created",
|
|
2792
|
+
(kind: SimulatedKind) => {
|
|
2793
|
+
const TEMPLATE: string = kind.templates.createFromIssue;
|
|
2794
|
+
|
|
2795
|
+
type RunCreateFunction = (props: {
|
|
2796
|
+
event: JSONObject;
|
|
2797
|
+
database?: FakeDatabase | undefined;
|
|
2798
|
+
/** The issue's labels as Jira's GET returns them; none by default. */
|
|
2799
|
+
labels?: Array<string> | undefined;
|
|
2800
|
+
getIssue?: JiraReply | undefined;
|
|
2801
|
+
edit?: JiraReply | undefined;
|
|
2802
|
+
}) => Promise<SimulationTrace>;
|
|
2803
|
+
|
|
2804
|
+
const runCreate: RunCreateFunction = async (props: {
|
|
2805
|
+
event: JSONObject;
|
|
2806
|
+
database?: FakeDatabase | undefined;
|
|
2807
|
+
labels?: Array<string> | undefined;
|
|
2808
|
+
getIssue?: JiraReply | undefined;
|
|
2809
|
+
edit?: JiraReply | undefined;
|
|
2810
|
+
}): Promise<SimulationTrace> => {
|
|
2811
|
+
return await runTemplate({
|
|
2812
|
+
templateId: TEMPLATE,
|
|
2813
|
+
trigger: webhookDelivery(props.event),
|
|
2814
|
+
jira: createFromIssueJira({
|
|
2815
|
+
labels: props.labels,
|
|
2816
|
+
getIssue: props.getIssue,
|
|
2817
|
+
edit: props.edit,
|
|
2818
|
+
}),
|
|
2819
|
+
database: props.database || createDatabase(kind),
|
|
2820
|
+
});
|
|
2821
|
+
};
|
|
2822
|
+
|
|
2823
|
+
test(`${kind.create}s an ${kind.noun} at the severity the priority maps to, then labels the issue with its id`, async () => {
|
|
2824
|
+
const trace: SimulationTrace = await runCreate({
|
|
2825
|
+
event: jiraIssueCreatedEvent(),
|
|
2826
|
+
});
|
|
2827
|
+
|
|
2828
|
+
expect(trace.executed).toEqual([
|
|
2829
|
+
"webhook-1",
|
|
2830
|
+
"find-severities-1",
|
|
2831
|
+
kind.steps.prepareRecord,
|
|
2832
|
+
kind.steps.ifCreate,
|
|
2833
|
+
"get-issue-1",
|
|
2834
|
+
"confirm-unlinked-1",
|
|
2835
|
+
"if-unlinked-1",
|
|
2836
|
+
kind.steps.createRecord,
|
|
2837
|
+
"link-issue-1",
|
|
2838
|
+
"log-linked",
|
|
2839
|
+
]);
|
|
2840
|
+
|
|
2841
|
+
expect(trace.databaseCalls).toEqual([
|
|
2842
|
+
{
|
|
2843
|
+
componentId: "find-severities-1",
|
|
2844
|
+
metadataId: kind.components.severityFindMany,
|
|
2845
|
+
args: {
|
|
2846
|
+
query: { _id: { _type: "NotNull", value: null } },
|
|
2847
|
+
select: { _id: true, name: true, order: true },
|
|
2848
|
+
limit: 50,
|
|
2849
|
+
},
|
|
2850
|
+
},
|
|
2851
|
+
{
|
|
2852
|
+
componentId: kind.steps.createRecord,
|
|
2853
|
+
metadataId: kind.components.createOne,
|
|
2854
|
+
args: {
|
|
2855
|
+
json: {
|
|
2856
|
+
// High is the most severe priority band, and severities are ordered 1 = most severe.
|
|
2857
|
+
[kind.severityIdColumn]: kind.mostSevere.id,
|
|
2858
|
+
customFields: { jiraIssueKey: JIRA_ISSUE_KEY },
|
|
2859
|
+
...kind.quietCreateFields,
|
|
2860
|
+
title: "Checkout API returns 502 for EU customers",
|
|
2861
|
+
description: expectedCreatedDescription(kind),
|
|
2862
|
+
},
|
|
2863
|
+
},
|
|
2864
|
+
},
|
|
2865
|
+
]);
|
|
2866
|
+
|
|
2867
|
+
/*
|
|
2868
|
+
* Jira is asked for the issue's labels before anything is written, and
|
|
2869
|
+
* the label added afterwards carries the id the database just gave the
|
|
2870
|
+
* record — nothing else could.
|
|
2871
|
+
*/
|
|
2872
|
+
expect(trace.requests).toEqual([
|
|
2873
|
+
GET_LABELS_REQUEST,
|
|
2874
|
+
linkLabelsRequest(kind),
|
|
2875
|
+
]);
|
|
2876
|
+
expect(trace.logs).toEqual([
|
|
2877
|
+
`✅ ${kind.Created} an ${kind.noun} with severity ${kind.mostSevere.name} for Jira issue ${JIRA_ISSUE_KEY}, and labelled the issue to link them.`,
|
|
2878
|
+
]);
|
|
2879
|
+
});
|
|
2880
|
+
|
|
2881
|
+
test("a Low priority lands on the least severe severity", async () => {
|
|
2882
|
+
const trace: SimulationTrace = await runCreate({
|
|
2883
|
+
event: jiraIssueCreatedEvent({ priority: "Low" }),
|
|
2884
|
+
});
|
|
2885
|
+
|
|
2886
|
+
expect(
|
|
2887
|
+
jsonOf(trace, kind.components.createOne)[kind.severityIdColumn],
|
|
2888
|
+
).toBe(kind.leastSevere.id);
|
|
2889
|
+
expect(trace.logs[0]).toContain(`with severity ${kind.leastSevere.name}`);
|
|
2890
|
+
});
|
|
2891
|
+
|
|
2892
|
+
test(`a Medium priority lands in the middle of the range: ${kind.middleSeverity.name}`, async () => {
|
|
2893
|
+
const trace: SimulationTrace = await runCreate({
|
|
2894
|
+
event: jiraIssueCreatedEvent({ priority: "Medium" }),
|
|
2895
|
+
});
|
|
2896
|
+
|
|
2897
|
+
expect(
|
|
2898
|
+
jsonOf(trace, kind.components.createOne)[kind.severityIdColumn],
|
|
2899
|
+
).toBe(kind.middleSeverity.id);
|
|
2900
|
+
expect(trace.logs[0]).toContain(
|
|
2901
|
+
`with severity ${kind.middleSeverity.name}`,
|
|
2902
|
+
);
|
|
2903
|
+
});
|
|
2904
|
+
|
|
2905
|
+
test(`an issue whose payload shows it linked ${kind.create}s nothing, and Jira is not even asked`, async () => {
|
|
2906
|
+
const trace: SimulationTrace = await runCreate({
|
|
2907
|
+
event: jiraIssueCreatedEvent({ labels: linkedLabelsOf(kind) }),
|
|
2908
|
+
});
|
|
2909
|
+
|
|
2910
|
+
expect(metadataIdsOf(trace)).toEqual([kind.components.severityFindMany]);
|
|
2911
|
+
expect(trace.requests).toEqual([]);
|
|
2912
|
+
expect(trace.logs).toEqual([alreadyLinkedLog(kind)]);
|
|
2913
|
+
});
|
|
2914
|
+
|
|
2915
|
+
/*
|
|
2916
|
+
* The payload is only the sender's word. Anyone holding the workflow's
|
|
2917
|
+
* URL can post an issue_created that shows no labels, and a delivery Jira
|
|
2918
|
+
* retries still carries the labels the issue had before the first
|
|
2919
|
+
* delivery linked it. What Jira returns now is what decides.
|
|
2920
|
+
*/
|
|
2921
|
+
test.each([
|
|
2922
|
+
[`linked to this ${kind.noun}`, linkedLabelsOf(kind)],
|
|
2923
|
+
[`claimed by another ${kind.noun}`, [kind.label(kind.otherId)]],
|
|
2924
|
+
["carrying only the oneuptime label", [JIRA_LINK_LABEL]],
|
|
2925
|
+
])(
|
|
2926
|
+
`a payload showing no labels, for an issue Jira says is %s, ${kind.create}s nothing`,
|
|
2927
|
+
async (_description: string, labels: Array<string>) => {
|
|
2928
|
+
const trace: SimulationTrace = await runCreate({
|
|
2929
|
+
event: jiraIssueCreatedEvent(),
|
|
2930
|
+
labels: labels,
|
|
2931
|
+
});
|
|
2932
|
+
|
|
2933
|
+
expect(trace.executed).toEqual([
|
|
2934
|
+
"webhook-1",
|
|
2935
|
+
"find-severities-1",
|
|
2936
|
+
kind.steps.prepareRecord,
|
|
2937
|
+
kind.steps.ifCreate,
|
|
2938
|
+
"get-issue-1",
|
|
2939
|
+
"confirm-unlinked-1",
|
|
2940
|
+
"if-unlinked-1",
|
|
2941
|
+
"log-already-linked",
|
|
2942
|
+
]);
|
|
2943
|
+
// No record is written, and the issue's labels are not touched.
|
|
2944
|
+
expect(metadataIdsOf(trace)).toEqual([
|
|
2945
|
+
kind.components.severityFindMany,
|
|
2946
|
+
]);
|
|
2947
|
+
expect(trace.requests).toEqual([GET_LABELS_REQUEST]);
|
|
2948
|
+
expect(trace.logs).toEqual([alreadyLinkedLog(kind)]);
|
|
2949
|
+
},
|
|
2950
|
+
);
|
|
2951
|
+
|
|
2952
|
+
test(`an issue Jira will not return (a forged key, or credentials that cannot see it) ${kind.create}s nothing`, async () => {
|
|
2953
|
+
const notFound: JSONObject = {
|
|
2954
|
+
errorMessages: [
|
|
2955
|
+
"Issue does not exist or you do not have permission to see it.",
|
|
2956
|
+
],
|
|
2957
|
+
errors: {},
|
|
2958
|
+
};
|
|
2959
|
+
|
|
2960
|
+
const trace: SimulationTrace = await runCreate({
|
|
2961
|
+
event: jiraIssueCreatedEvent(),
|
|
2962
|
+
getIssue: { status: 404, body: notFound },
|
|
2963
|
+
});
|
|
2964
|
+
|
|
2965
|
+
expect(trace.executed).toEqual([
|
|
2966
|
+
"webhook-1",
|
|
2967
|
+
"find-severities-1",
|
|
2968
|
+
kind.steps.prepareRecord,
|
|
2969
|
+
kind.steps.ifCreate,
|
|
2970
|
+
"get-issue-1",
|
|
2971
|
+
"log-get-issue-failed",
|
|
2972
|
+
]);
|
|
2973
|
+
expect(metadataIdsOf(trace)).toEqual([kind.components.severityFindMany]);
|
|
2974
|
+
expect(trace.requests).toEqual([GET_LABELS_REQUEST]);
|
|
2975
|
+
expect(trace.logs).toEqual([
|
|
2976
|
+
`❌ Could not read Jira issue ${JIRA_ISSUE_KEY}, so no ${kind.noun} was ${kind.created}: Server Error.\nJira said: ${JSON.stringify(notFound, null, 2)}`,
|
|
2977
|
+
]);
|
|
2978
|
+
});
|
|
2979
|
+
|
|
2980
|
+
test(`a success that is not the issue (a proxy's page) ${kind.create}s nothing`, async () => {
|
|
2981
|
+
const trace: SimulationTrace = await runCreate({
|
|
2982
|
+
event: jiraIssueCreatedEvent(),
|
|
2983
|
+
getIssue: { status: 200, body: "<html><body>Sign in</body></html>" },
|
|
2984
|
+
});
|
|
2985
|
+
|
|
2986
|
+
expect(trace.ports["get-issue-1"]).toBe("success");
|
|
2987
|
+
expect(trace.ports["if-unlinked-1"]).toBe("no");
|
|
2988
|
+
expect(metadataIdsOf(trace)).toEqual([kind.components.severityFindMany]);
|
|
2989
|
+
expect(trace.requests).toEqual([GET_LABELS_REQUEST]);
|
|
2990
|
+
expect(trace.logs).toEqual([
|
|
2991
|
+
`ℹ️ Jira did not return the issue, so no ${kind.noun} was ${kind.created}.`,
|
|
2992
|
+
]);
|
|
2993
|
+
});
|
|
2994
|
+
|
|
2995
|
+
test(`an issue_updated delivery to this webhook ${kind.create}s nothing`, async () => {
|
|
2996
|
+
const trace: SimulationTrace = await runCreate({
|
|
2997
|
+
event: jiraIssueUpdatedEvent({ items: STARTED_CHANGELOG }),
|
|
2998
|
+
});
|
|
2999
|
+
|
|
3000
|
+
expect(metadataIdsOf(trace)).toEqual([kind.components.severityFindMany]);
|
|
3001
|
+
expect(trace.requests).toEqual([]);
|
|
3002
|
+
expect(trace.logs).toEqual([
|
|
3003
|
+
"ℹ️ Ignored a jira:issue_updated event: this workflow only handles jira:issue_created.",
|
|
3004
|
+
]);
|
|
3005
|
+
});
|
|
3006
|
+
|
|
3007
|
+
test("a failed create is logged, and the issue is not labelled with an id that does not exist", async () => {
|
|
3008
|
+
const trace: SimulationTrace = await runCreate({
|
|
3009
|
+
event: jiraIssueCreatedEvent(),
|
|
3010
|
+
database: {
|
|
3011
|
+
...createDatabase(kind),
|
|
3012
|
+
[kind.components.createOne]: DATABASE_FAILURE,
|
|
3013
|
+
},
|
|
3014
|
+
});
|
|
3015
|
+
|
|
3016
|
+
// Only the read that came before the create; no label edit follows it.
|
|
3017
|
+
expect(trace.requests).toEqual([GET_LABELS_REQUEST]);
|
|
3018
|
+
expect(trace.logs).toEqual([
|
|
3019
|
+
`❌ Could not ${kind.create} the ${kind.noun} for Jira issue ${JIRA_ISSUE_KEY}. The database error is in the run log above.`,
|
|
3020
|
+
]);
|
|
3021
|
+
});
|
|
3022
|
+
|
|
3023
|
+
test(`Jira refusing the labels ends on the warning, since the ${kind.noun} does exist`, async () => {
|
|
3024
|
+
const rejection: JSONObject = {
|
|
3025
|
+
errorMessages: [],
|
|
3026
|
+
errors: {
|
|
3027
|
+
labels:
|
|
3028
|
+
"Field 'labels' cannot be set. It is not on the appropriate screen, or unknown.",
|
|
3029
|
+
},
|
|
3030
|
+
};
|
|
3031
|
+
|
|
3032
|
+
const trace: SimulationTrace = await runCreate({
|
|
3033
|
+
event: jiraIssueCreatedEvent(),
|
|
3034
|
+
edit: badRequest(rejection),
|
|
3035
|
+
});
|
|
3036
|
+
|
|
3037
|
+
expect(trace.requests).toEqual([
|
|
3038
|
+
GET_LABELS_REQUEST,
|
|
3039
|
+
linkLabelsRequest(kind),
|
|
3040
|
+
]);
|
|
3041
|
+
expect(trace.logs).toEqual([
|
|
3042
|
+
`⚠️ The ${kind.noun} was ${kind.created}, but Jira did not accept the link labels, so the other Jira templates cannot find it: Server Error.\nJira said: ${JSON.stringify(rejection, null, 2)}`,
|
|
3043
|
+
]);
|
|
3044
|
+
});
|
|
3045
|
+
},
|
|
3046
|
+
);
|
|
3047
|
+
|
|
3048
|
+
describe.each(KINDS)(
|
|
3049
|
+
"simulated: acknowledge or resolve the $noun when its Jira issue moves",
|
|
3050
|
+
(kind: SimulatedKind) => {
|
|
3051
|
+
const TEMPLATE: string = kind.templates.statusToState;
|
|
3052
|
+
|
|
3053
|
+
type RunStatusFunction = (props: {
|
|
3054
|
+
event: JSONObject;
|
|
3055
|
+
record: JSONObject | null;
|
|
3056
|
+
}) => Promise<SimulationTrace>;
|
|
3057
|
+
|
|
3058
|
+
const runStatus: RunStatusFunction = async (props: {
|
|
3059
|
+
event: JSONObject;
|
|
3060
|
+
record: JSONObject | null;
|
|
3061
|
+
}): Promise<SimulationTrace> => {
|
|
3062
|
+
return await runTemplate({
|
|
3063
|
+
templateId: TEMPLATE,
|
|
3064
|
+
trigger: webhookDelivery(props.event),
|
|
3065
|
+
database: statusDatabase(kind, props.record),
|
|
3066
|
+
});
|
|
3067
|
+
};
|
|
3068
|
+
|
|
3069
|
+
const RESOLVED_EVENT: JSONObject = jiraIssueUpdatedEvent({
|
|
3070
|
+
items: RESOLVED_CHANGELOG,
|
|
3071
|
+
labels: linkedLabelsOf(kind),
|
|
3072
|
+
status: DONE,
|
|
3073
|
+
});
|
|
3074
|
+
|
|
3075
|
+
test(`Done resolves an acknowledged ${kind.noun} with a new timeline row`, async () => {
|
|
3076
|
+
const trace: SimulationTrace = await runStatus({
|
|
3077
|
+
event: RESOLVED_EVENT,
|
|
3078
|
+
record: recordOf(kind, kind.acknowledgedState),
|
|
3079
|
+
});
|
|
3080
|
+
|
|
3081
|
+
expect(trace.executed).toEqual([
|
|
3082
|
+
"webhook-1",
|
|
3083
|
+
"read-event-1",
|
|
3084
|
+
"if-status-changed-1",
|
|
3085
|
+
kind.steps.find,
|
|
3086
|
+
"find-states-1",
|
|
3087
|
+
"decide-state-1",
|
|
3088
|
+
"if-change-1",
|
|
3089
|
+
"change-state-1",
|
|
3090
|
+
"log-changed",
|
|
3091
|
+
]);
|
|
3092
|
+
|
|
3093
|
+
expect(callOf(trace, kind.components.findOne).args).toEqual({
|
|
3094
|
+
query: { _id: kind.id },
|
|
3095
|
+
select: {
|
|
3096
|
+
_id: true,
|
|
3097
|
+
[kind.numberField]: true,
|
|
3098
|
+
[kind.stateRelation]: { _id: true, name: true, order: true },
|
|
3099
|
+
},
|
|
3100
|
+
});
|
|
3101
|
+
|
|
3102
|
+
// A timeline row, never an edit to the record: the timeline is what notifies and refuses to go backwards.
|
|
3103
|
+
expect(metadataIdsOf(trace)).toEqual([
|
|
3104
|
+
kind.components.findOne,
|
|
3105
|
+
kind.components.stateFindMany,
|
|
3106
|
+
kind.components.timelineCreateOne,
|
|
3107
|
+
]);
|
|
3108
|
+
expect(jsonOf(trace, kind.components.timelineCreateOne)).toEqual({
|
|
3109
|
+
[kind.idColumn]: kind.id,
|
|
3110
|
+
[kind.timelineStateColumn]: kind.resolvedState["_id"] as string,
|
|
3111
|
+
rootCause: `${ONEUPTIME_SYNCED_FROM_JIRA_MARKER}: issue ${JIRA_ISSUE_KEY} moved to Done by Priya Patel.`,
|
|
3112
|
+
});
|
|
3113
|
+
expect(trace.requests).toEqual([]);
|
|
3114
|
+
expect(trace.logs).toEqual([
|
|
3115
|
+
`✅ Moved ${kind.number} to Resolved because Jira issue ${JIRA_ISSUE_KEY} is now Done.`,
|
|
3116
|
+
]);
|
|
3117
|
+
});
|
|
3118
|
+
|
|
3119
|
+
test(`In Progress acknowledges a new ${kind.noun}`, async () => {
|
|
3120
|
+
const trace: SimulationTrace = await runStatus({
|
|
3121
|
+
event: jiraIssueUpdatedEvent({
|
|
3122
|
+
items: STARTED_CHANGELOG,
|
|
3123
|
+
labels: linkedLabelsOf(kind),
|
|
3124
|
+
status: IN_PROGRESS,
|
|
3125
|
+
}),
|
|
3126
|
+
record: recordOf(kind, kind.createdState),
|
|
3127
|
+
});
|
|
3128
|
+
|
|
3129
|
+
expect(
|
|
3130
|
+
jsonOf(trace, kind.components.timelineCreateOne)[
|
|
3131
|
+
kind.timelineStateColumn
|
|
3132
|
+
],
|
|
3133
|
+
).toBe(kind.acknowledgedState["_id"]);
|
|
3134
|
+
expect(trace.logs).toEqual([
|
|
3135
|
+
`✅ Moved ${kind.number} to Acknowledged because Jira issue ${JIRA_ISSUE_KEY} is now In Progress.`,
|
|
3136
|
+
]);
|
|
3137
|
+
});
|
|
3138
|
+
|
|
3139
|
+
test(`an ${kind.noun} that is already resolved is not moved again`, async () => {
|
|
3140
|
+
const trace: SimulationTrace = await runStatus({
|
|
3141
|
+
event: RESOLVED_EVENT,
|
|
3142
|
+
record: recordOf(kind, kind.resolvedState),
|
|
3143
|
+
});
|
|
3144
|
+
|
|
3145
|
+
expect(metadataIdsOf(trace)).toEqual([
|
|
3146
|
+
kind.components.findOne,
|
|
3147
|
+
kind.components.stateFindMany,
|
|
3148
|
+
]);
|
|
3149
|
+
expect(trace.logs).toEqual([
|
|
3150
|
+
`ℹ️ ${kind.number} is already Resolved, so Jira issue ${JIRA_ISSUE_KEY} moving to Done changes nothing.`,
|
|
3151
|
+
]);
|
|
3152
|
+
});
|
|
3153
|
+
|
|
3154
|
+
test(`moving the issue back to To Do never moves the ${kind.noun} backwards`, async () => {
|
|
3155
|
+
const trace: SimulationTrace = await runStatus({
|
|
3156
|
+
event: jiraIssueUpdatedEvent({
|
|
3157
|
+
items: [
|
|
3158
|
+
changelogItem({
|
|
3159
|
+
field: "status",
|
|
3160
|
+
from: "3",
|
|
3161
|
+
fromString: "In Progress",
|
|
3162
|
+
to: "10001",
|
|
3163
|
+
toString: "To Do",
|
|
3164
|
+
}),
|
|
3165
|
+
],
|
|
3166
|
+
labels: linkedLabelsOf(kind),
|
|
3167
|
+
status: TO_DO,
|
|
3168
|
+
}),
|
|
3169
|
+
record: recordOf(kind, kind.acknowledgedState),
|
|
3170
|
+
});
|
|
3171
|
+
|
|
3172
|
+
expect(metadataIdsOf(trace)).not.toContain(
|
|
3173
|
+
kind.components.timelineCreateOne,
|
|
3174
|
+
);
|
|
3175
|
+
expect(trace.logs).toEqual([
|
|
3176
|
+
`ℹ️ Jira issue ${JIRA_ISSUE_KEY} moved to To Do, which does not map to a OneUptime state.`,
|
|
3177
|
+
]);
|
|
3178
|
+
});
|
|
3179
|
+
|
|
3180
|
+
test(`an issue with no ${kind.noun} label touches nothing in the database`, async () => {
|
|
3181
|
+
const trace: SimulationTrace = await runStatus({
|
|
3182
|
+
event: jiraIssueUpdatedEvent({
|
|
3183
|
+
items: RESOLVED_CHANGELOG,
|
|
3184
|
+
status: DONE,
|
|
3185
|
+
}),
|
|
3186
|
+
record: recordOf(kind, kind.acknowledgedState),
|
|
3187
|
+
});
|
|
3188
|
+
|
|
3189
|
+
expect(trace.databaseCalls).toEqual([]);
|
|
3190
|
+
expect(trace.logs).toEqual([notLinkedLog(kind)]);
|
|
3191
|
+
});
|
|
3192
|
+
|
|
3193
|
+
test(`a label naming an ${kind.noun} this project does not have changes nothing`, async () => {
|
|
3194
|
+
const trace: SimulationTrace = await runStatus({
|
|
3195
|
+
event: RESOLVED_EVENT,
|
|
3196
|
+
record: null,
|
|
3197
|
+
});
|
|
3198
|
+
|
|
3199
|
+
expect(metadataIdsOf(trace)).toEqual([
|
|
3200
|
+
kind.components.findOne,
|
|
3201
|
+
kind.components.stateFindMany,
|
|
3202
|
+
]);
|
|
3203
|
+
expect(trace.logs).toEqual([
|
|
3204
|
+
`ℹ️ No ${kind.noun} with id ${kind.id} exists in this project.`,
|
|
3205
|
+
]);
|
|
3206
|
+
});
|
|
3207
|
+
},
|
|
3208
|
+
);
|
|
3209
|
+
|
|
3210
|
+
const EXPECTED_COMMENT_NOTE: string = `${ONEUPTIME_SYNCED_FROM_JIRA_MARKER}: Marco Rossi commented on [${JIRA_ISSUE_KEY}](${JIRA_SITE}/browse/${JIRA_ISSUE_KEY}).\n\nRolled back the *eu-west-1* deploy. Watching the error rate.`;
|
|
3211
|
+
|
|
3212
|
+
describe.each(KINDS)(
|
|
3213
|
+
"simulated: add Jira comments to the $noun as private notes",
|
|
3214
|
+
(kind: SimulatedKind) => {
|
|
3215
|
+
test(`reads the link off the issue, checks the ${kind.noun} is here, and writes the note`, async () => {
|
|
3216
|
+
const trace: SimulationTrace = await runComment(kind, {
|
|
3217
|
+
event: jiraCommentEvent(),
|
|
3218
|
+
});
|
|
3219
|
+
|
|
3220
|
+
expect(trace.executed).toEqual([
|
|
3221
|
+
"webhook-1",
|
|
3222
|
+
"read-comment-1",
|
|
3223
|
+
"if-comment-1",
|
|
3224
|
+
"get-issue-1",
|
|
3225
|
+
"find-link-1",
|
|
3226
|
+
"if-linked-1",
|
|
3227
|
+
kind.steps.find,
|
|
3228
|
+
"if-found-1",
|
|
3229
|
+
"create-note-1",
|
|
3230
|
+
"log-noted",
|
|
3231
|
+
]);
|
|
3232
|
+
|
|
3233
|
+
// Comment webhooks carry no labels, so the issue is fetched for them.
|
|
3234
|
+
expect(trace.requests).toEqual([GET_LABELS_REQUEST]);
|
|
3235
|
+
|
|
3236
|
+
expect(trace.databaseCalls).toEqual([
|
|
3237
|
+
{
|
|
3238
|
+
componentId: kind.steps.find,
|
|
3239
|
+
metadataId: kind.components.findOne,
|
|
3240
|
+
args: {
|
|
3241
|
+
query: { _id: kind.id },
|
|
3242
|
+
select: { _id: true, [kind.numberField]: true },
|
|
3243
|
+
},
|
|
3244
|
+
},
|
|
3245
|
+
{
|
|
3246
|
+
componentId: "create-note-1",
|
|
3247
|
+
metadataId: kind.components.noteCreateOne,
|
|
3248
|
+
args: {
|
|
3249
|
+
json: { [kind.idColumn]: kind.id, note: EXPECTED_COMMENT_NOTE },
|
|
3250
|
+
},
|
|
3251
|
+
},
|
|
3252
|
+
]);
|
|
3253
|
+
expect(trace.logs).toEqual([
|
|
3254
|
+
`✅ Added the Jira comment on ${JIRA_ISSUE_KEY} to ${kind.number} as a private note.`,
|
|
3255
|
+
]);
|
|
3256
|
+
});
|
|
3257
|
+
|
|
3258
|
+
test("a comment OneUptime posted is not copied back, and Jira is not even asked about it", async () => {
|
|
3259
|
+
const trace: SimulationTrace = await runComment(kind, {
|
|
3260
|
+
event: jiraCommentEvent({
|
|
3261
|
+
body: `${JIRA_SYNCED_FROM_ONEUPTIME_MARKER}: private note on ${kind.number} by Jane Doe.\n\nFailed over.`,
|
|
3262
|
+
author: AUTOMATION_USER,
|
|
3263
|
+
}),
|
|
3264
|
+
});
|
|
3265
|
+
|
|
3266
|
+
expect(trace.requests).toEqual([]);
|
|
3267
|
+
expect(trace.databaseCalls).toEqual([]);
|
|
3268
|
+
expect(trace.logs).toEqual([COMMENT_FROM_ONEUPTIME_LOG]);
|
|
3269
|
+
});
|
|
3270
|
+
|
|
3271
|
+
test("an issue without the labels is only looked at", async () => {
|
|
3272
|
+
const trace: SimulationTrace = await runComment(kind, {
|
|
3273
|
+
event: jiraCommentEvent(),
|
|
3274
|
+
labels: [],
|
|
3275
|
+
});
|
|
3276
|
+
|
|
3277
|
+
expect(trace.requests).toEqual([GET_LABELS_REQUEST]);
|
|
3278
|
+
expect(trace.databaseCalls).toEqual([]);
|
|
3279
|
+
expect(trace.logs).toEqual([notLinkedLog(kind)]);
|
|
3280
|
+
});
|
|
3281
|
+
|
|
3282
|
+
test(`a label naming an ${kind.noun} in another project writes no note`, async () => {
|
|
3283
|
+
const trace: SimulationTrace = await runComment(kind, {
|
|
3284
|
+
event: jiraCommentEvent(),
|
|
3285
|
+
labels: [JIRA_LINK_LABEL, kind.label(kind.otherId)],
|
|
3286
|
+
record: null,
|
|
3287
|
+
});
|
|
3288
|
+
|
|
3289
|
+
expect(callOf(trace, kind.components.findOne).args["query"]).toEqual({
|
|
3290
|
+
_id: kind.otherId,
|
|
3291
|
+
});
|
|
3292
|
+
expect(trace.ports["if-found-1"]).toBe("no");
|
|
3293
|
+
expect(metadataIdsOf(trace)).toEqual([kind.components.findOne]);
|
|
3294
|
+
expect(trace.logs).toEqual([
|
|
3295
|
+
`ℹ️ Jira issue ${JIRA_ISSUE_KEY} names ${kind.noun} ${kind.otherId}, which is not in this project.`,
|
|
3296
|
+
]);
|
|
3297
|
+
});
|
|
3298
|
+
},
|
|
3299
|
+
);
|
|
3300
|
+
|
|
3301
|
+
/** A priority change on an issue carrying the given labels. */
|
|
3302
|
+
type PriorityChangeFunction = (labels: Array<string>) => JSONObject;
|
|
3303
|
+
|
|
3304
|
+
const priorityChange: PriorityChangeFunction = (
|
|
3305
|
+
labels: Array<string>,
|
|
3306
|
+
): JSONObject => {
|
|
3307
|
+
return jiraIssueUpdatedEvent({
|
|
3308
|
+
labels: labels,
|
|
3309
|
+
items: [
|
|
3310
|
+
changelogItem({
|
|
3311
|
+
field: "priority",
|
|
3312
|
+
from: "3",
|
|
3313
|
+
fromString: "Medium",
|
|
3314
|
+
to: "1",
|
|
3315
|
+
toString: "Highest",
|
|
3316
|
+
}),
|
|
3317
|
+
],
|
|
3318
|
+
});
|
|
3319
|
+
};
|
|
3320
|
+
|
|
3321
|
+
describe.each(KINDS)(
|
|
3322
|
+
"simulated: add Jira issue changes to the $noun as private notes",
|
|
3323
|
+
(kind: SimulatedKind) => {
|
|
3324
|
+
test(`a priority change is noted on the linked ${kind.noun}`, async () => {
|
|
3325
|
+
const trace: SimulationTrace = await runIssueChanges(kind, {
|
|
3326
|
+
event: priorityChange(linkedLabelsOf(kind)),
|
|
3327
|
+
});
|
|
3328
|
+
|
|
3329
|
+
expect(trace.executed).toEqual([
|
|
3330
|
+
"webhook-1",
|
|
3331
|
+
"read-changes-1",
|
|
3332
|
+
"if-changed-1",
|
|
3333
|
+
kind.steps.find,
|
|
3334
|
+
"if-found-1",
|
|
3335
|
+
"create-note-1",
|
|
3336
|
+
"log-noted",
|
|
3337
|
+
]);
|
|
3338
|
+
expect(callOf(trace, kind.components.findOne).args).toEqual({
|
|
3339
|
+
query: { _id: kind.id },
|
|
3340
|
+
select: { _id: true, [kind.numberField]: true },
|
|
3341
|
+
});
|
|
3342
|
+
expect(jsonOf(trace, kind.components.noteCreateOne)).toEqual({
|
|
3343
|
+
[kind.idColumn]: kind.id,
|
|
3344
|
+
note: `${ONEUPTIME_SYNCED_FROM_JIRA_MARKER}: Priya Patel updated [${JIRA_ISSUE_KEY}](${JIRA_SITE}/browse/${JIRA_ISSUE_KEY}).\n\n- priority: Medium → Highest`,
|
|
3345
|
+
});
|
|
3346
|
+
expect(trace.requests).toEqual([]);
|
|
3347
|
+
expect(trace.logs).toEqual([
|
|
3348
|
+
`✅ Noted the changes to Jira issue ${JIRA_ISSUE_KEY} on ${kind.number}.`,
|
|
3349
|
+
]);
|
|
3350
|
+
});
|
|
3351
|
+
|
|
3352
|
+
test("adding only the link labels is not worth a note", async () => {
|
|
3353
|
+
const trace: SimulationTrace = await runIssueChanges(kind, {
|
|
3354
|
+
event: jiraIssueUpdatedEvent({
|
|
3355
|
+
labels: linkedLabelsOf(kind),
|
|
3356
|
+
items: [
|
|
3357
|
+
changelogItem({
|
|
3358
|
+
field: "labels",
|
|
3359
|
+
fromString: "",
|
|
3360
|
+
toString: linkedLabelsOf(kind).join(" "),
|
|
3361
|
+
}),
|
|
3362
|
+
],
|
|
3363
|
+
}),
|
|
3364
|
+
});
|
|
3365
|
+
|
|
3366
|
+
expect(trace.databaseCalls).toEqual([]);
|
|
3367
|
+
expect(trace.logs).toEqual([NOTHING_WORTH_A_NOTE_LOG]);
|
|
3368
|
+
});
|
|
3369
|
+
|
|
3370
|
+
test("a status change is left to the status template", async () => {
|
|
3371
|
+
const trace: SimulationTrace = await runIssueChanges(kind, {
|
|
3372
|
+
event: jiraIssueUpdatedEvent({
|
|
3373
|
+
labels: linkedLabelsOf(kind),
|
|
3374
|
+
items: RESOLVED_CHANGELOG,
|
|
3375
|
+
status: DONE,
|
|
3376
|
+
}),
|
|
3377
|
+
});
|
|
3378
|
+
|
|
3379
|
+
expect(trace.databaseCalls).toEqual([]);
|
|
3380
|
+
expect(trace.logs).toEqual([NOTHING_WORTH_A_NOTE_LOG]);
|
|
3381
|
+
});
|
|
3382
|
+
|
|
3383
|
+
// Dragging an issue on a board changes Rank, which Jira reports under its custom field id.
|
|
3384
|
+
test("a Rank change, sent as customfield_10019, is not worth a note", async () => {
|
|
3385
|
+
const trace: SimulationTrace = await runIssueChanges(kind, {
|
|
3386
|
+
event: jiraIssueUpdatedEvent({
|
|
3387
|
+
labels: linkedLabelsOf(kind),
|
|
3388
|
+
items: [
|
|
3389
|
+
changelogItem({
|
|
3390
|
+
field: "Rank",
|
|
3391
|
+
fieldId: "customfield_10019",
|
|
3392
|
+
fromString: "",
|
|
3393
|
+
toString: "Ranked higher",
|
|
3394
|
+
}),
|
|
3395
|
+
],
|
|
3396
|
+
}),
|
|
3397
|
+
});
|
|
3398
|
+
|
|
3399
|
+
expect(trace.databaseCalls).toEqual([]);
|
|
3400
|
+
expect(trace.logs).toEqual([NOTHING_WORTH_A_NOTE_LOG]);
|
|
3401
|
+
});
|
|
3402
|
+
|
|
3403
|
+
test(`a label naming an ${kind.noun} in another project writes no note`, async () => {
|
|
3404
|
+
const trace: SimulationTrace = await runIssueChanges(kind, {
|
|
3405
|
+
event: priorityChange([JIRA_LINK_LABEL, kind.label(kind.otherId)]),
|
|
3406
|
+
record: null,
|
|
3407
|
+
});
|
|
3408
|
+
|
|
3409
|
+
expect(callOf(trace, kind.components.findOne).args["query"]).toEqual({
|
|
3410
|
+
_id: kind.otherId,
|
|
3411
|
+
});
|
|
3412
|
+
expect(metadataIdsOf(trace)).toEqual([kind.components.findOne]);
|
|
3413
|
+
expect(trace.logs).toEqual([
|
|
3414
|
+
`ℹ️ Jira issue ${JIRA_ISSUE_KEY} names ${kind.noun} ${kind.otherId}, which is not in this project.`,
|
|
3415
|
+
]);
|
|
3416
|
+
});
|
|
3417
|
+
},
|
|
3418
|
+
);
|
|
3419
|
+
|
|
3420
|
+
/* ------------------------------- What differs for alerts ------------------------------- */
|
|
3421
|
+
|
|
3422
|
+
/*
|
|
3423
|
+
* The alert templates are the incident ones with the nouns swapped, except
|
|
3424
|
+
* where an alert is a different thing. Each difference is checked here with
|
|
3425
|
+
* the values written out, so it cannot pass by both sides of the kind table
|
|
3426
|
+
* being wrong the same way.
|
|
3427
|
+
*/
|
|
3428
|
+
describe("simulated: what differs for alerts", () => {
|
|
3429
|
+
test("alerts have no public notes, so there is no template to post one", () => {
|
|
3430
|
+
expect(
|
|
3431
|
+
getWorkflowTemplate("jira-comment-from-alert-public-note"),
|
|
3432
|
+
).toBeNull();
|
|
3433
|
+
expect(
|
|
3434
|
+
buildGraphForTemplate("jira-comment-from-alert-public-note", generateId),
|
|
3435
|
+
).toBeNull();
|
|
3436
|
+
expect(
|
|
3437
|
+
ALERT.noteTemplates.map((noteTemplate: NoteTemplate) => {
|
|
3438
|
+
return noteTemplate.templateId;
|
|
3439
|
+
}),
|
|
3440
|
+
).toEqual(["jira-comment-from-alert-private-note"]);
|
|
3441
|
+
});
|
|
3442
|
+
|
|
3443
|
+
test("an alert's issue is labelled oneuptime-alert-<id> and links to the alert's page under /alerts/", async () => {
|
|
3444
|
+
const trace: SimulationTrace = await runCreateIssue(ALERT);
|
|
3445
|
+
const fields: JSONObject = issueFieldsOf(trace);
|
|
3446
|
+
|
|
3447
|
+
expect(fields["labels"]).toEqual([
|
|
3448
|
+
"oneuptime",
|
|
3449
|
+
`oneuptime-alert-${ALERT_ID}`,
|
|
3450
|
+
]);
|
|
3451
|
+
expect(JSON.stringify(fields["description"])).toContain(
|
|
3452
|
+
`"href":"https://oneuptime.com/dashboard/${PROJECT_ID}/alerts/${ALERT_ID}"`,
|
|
3453
|
+
);
|
|
3454
|
+
expect(JSON.stringify(fields)).not.toContain("incident");
|
|
3455
|
+
});
|
|
3456
|
+
|
|
3457
|
+
test("the alert templates search Jira for oneuptime-alert-<id>, never for an incident label", async () => {
|
|
3458
|
+
const jira: FakeJiraFunction = jiraRoutes({
|
|
3459
|
+
[SEARCH]: ok(jiraSearchResponse()),
|
|
3460
|
+
[TRANSITION]: NO_CONTENT,
|
|
3461
|
+
[COMMENT]: COMMENT_CREATED,
|
|
3462
|
+
});
|
|
3463
|
+
|
|
3464
|
+
const traces: Array<SimulationTrace> = [
|
|
3465
|
+
await runTemplate({
|
|
3466
|
+
templateId: "jira-transition-issue-on-alert-state",
|
|
3467
|
+
trigger: onRecord(alertModel({ state: ALERT_RESOLVED_STATE })),
|
|
3468
|
+
jira: jira,
|
|
3469
|
+
}),
|
|
3470
|
+
await runTemplate({
|
|
3471
|
+
templateId: "jira-comment-from-alert-private-note",
|
|
3472
|
+
trigger: onRecord(alertNoteModel()),
|
|
3473
|
+
jira: jira,
|
|
3474
|
+
}),
|
|
3475
|
+
await runTemplate({
|
|
3476
|
+
templateId: "jira-comment-on-alert-update",
|
|
3477
|
+
trigger: onRecord(alertModel()),
|
|
3478
|
+
jira: jira,
|
|
3479
|
+
}),
|
|
3480
|
+
];
|
|
3481
|
+
|
|
3482
|
+
for (const trace of traces) {
|
|
3483
|
+
expect(
|
|
3484
|
+
((trace.requests[0]!.body as JSONObject)["jql"] as string).startsWith(
|
|
3485
|
+
`labels = "oneuptime-alert-${ALERT_ID}"`,
|
|
3486
|
+
),
|
|
3487
|
+
).toBe(true);
|
|
3488
|
+
expect(trace.requests).toHaveLength(2);
|
|
3489
|
+
}
|
|
3490
|
+
});
|
|
3491
|
+
|
|
3492
|
+
test("an alert created from Jira is written with no status-page fields, which an incident is kept off with", async () => {
|
|
3493
|
+
const alert: SimulationTrace = await runCreateFromIssue(ALERT);
|
|
3494
|
+
const incident: SimulationTrace = await runCreateFromIssue(INCIDENT);
|
|
3495
|
+
|
|
3496
|
+
expect(Object.keys(jsonOf(alert, "alert-create-one")).sort()).toEqual([
|
|
3497
|
+
"alertSeverityId",
|
|
3498
|
+
"customFields",
|
|
3499
|
+
"description",
|
|
3500
|
+
"title",
|
|
3501
|
+
]);
|
|
3502
|
+
expect(jsonOf(alert, "alert-create-one")["description"]).toContain(
|
|
3503
|
+
"Created from Jira issue",
|
|
3504
|
+
);
|
|
3505
|
+
expect(jsonOf(incident, "incident-create-one")).toMatchObject({
|
|
3506
|
+
isVisibleOnStatusPage: false,
|
|
3507
|
+
shouldStatusPageSubscribersBeNotifiedOnIncidentCreated: false,
|
|
3508
|
+
});
|
|
3509
|
+
expect(alert.logs).toEqual([
|
|
3510
|
+
`✅ Created an alert with severity High for Jira issue ${JIRA_ISSUE_KEY}, and labelled the issue to link them.`,
|
|
3511
|
+
]);
|
|
3512
|
+
expect(alert.requests[1]).toEqual({
|
|
3513
|
+
method: "PUT",
|
|
3514
|
+
url: `${JIRA_SITE}/rest/api/3/issue/${JIRA_ISSUE_KEY}`,
|
|
3515
|
+
headers: AUTHORIZATION,
|
|
3516
|
+
body: {
|
|
3517
|
+
update: {
|
|
3518
|
+
labels: [
|
|
3519
|
+
{ add: "oneuptime" },
|
|
3520
|
+
{ add: `oneuptime-alert-${NEW_ALERT_ID}` },
|
|
3521
|
+
],
|
|
3522
|
+
},
|
|
3523
|
+
},
|
|
3524
|
+
});
|
|
3525
|
+
});
|
|
3526
|
+
|
|
3527
|
+
/*
|
|
3528
|
+
* A project starts with two alert severities, High and Low, against three
|
|
3529
|
+
* incident ones. The priority bands are spread over whatever is there, so
|
|
3530
|
+
* the middle band rounds down to Low rather than landing on a severity
|
|
3531
|
+
* that does not exist.
|
|
3532
|
+
*/
|
|
3533
|
+
test.each([
|
|
3534
|
+
["Highest", HIGH_ALERT_SEVERITY_ID, "High"],
|
|
3535
|
+
["High", HIGH_ALERT_SEVERITY_ID, "High"],
|
|
3536
|
+
["Medium", LOW_ALERT_SEVERITY_ID, "Low"],
|
|
3537
|
+
["Low", LOW_ALERT_SEVERITY_ID, "Low"],
|
|
3538
|
+
["Lowest", LOW_ALERT_SEVERITY_ID, "Low"],
|
|
3539
|
+
])(
|
|
3540
|
+
"with the two default alert severities, a %s priority creates a %s-severity alert",
|
|
3541
|
+
async (priority: string, severityId: string, severityName: string) => {
|
|
3542
|
+
const trace: SimulationTrace = await runCreateFromIssue(ALERT, {
|
|
3543
|
+
event: jiraIssueCreatedEvent({ priority: priority }),
|
|
3544
|
+
});
|
|
3545
|
+
|
|
3546
|
+
expect(jsonOf(trace, "alert-create-one")["alertSeverityId"]).toBe(
|
|
3547
|
+
severityId,
|
|
3548
|
+
);
|
|
3549
|
+
expect(trace.logs[0]).toContain(`with severity ${severityName} `);
|
|
3550
|
+
},
|
|
3551
|
+
);
|
|
3552
|
+
|
|
3553
|
+
/*
|
|
3554
|
+
* An alert's root cause is written when the alert is created and cannot be
|
|
3555
|
+
* edited afterwards, so the edit-comment template does not listen for it —
|
|
3556
|
+
* but the comment it posts for any other edit still shows it.
|
|
3557
|
+
*/
|
|
3558
|
+
test("an alert's root cause is not listened on, and the comment still shows it", async () => {
|
|
3559
|
+
const TEMPLATE: string = "jira-comment-on-alert-update";
|
|
3560
|
+
|
|
3561
|
+
expect(firesOnUpdate(TEMPLATE, { rootCause: "Changed" })).toBe(false);
|
|
3562
|
+
expect(
|
|
3563
|
+
firesOnUpdate("jira-comment-on-incident-update", {
|
|
3564
|
+
rootCause: "Changed",
|
|
3565
|
+
}),
|
|
3566
|
+
).toBe(true);
|
|
3567
|
+
|
|
3568
|
+
// The dashboard's edit form sends the relation, which is heard.
|
|
3569
|
+
expect(
|
|
3570
|
+
firesOnUpdate(TEMPLATE, {
|
|
3571
|
+
alertSeverity: { _id: LOW_ALERT_SEVERITY_ID },
|
|
3572
|
+
}),
|
|
3573
|
+
).toBe(true);
|
|
3574
|
+
|
|
3575
|
+
const trace: SimulationTrace = await runTemplate({
|
|
3576
|
+
templateId: TEMPLATE,
|
|
3577
|
+
trigger: onRecord(
|
|
3578
|
+
alertModel({
|
|
3579
|
+
severity: "Low",
|
|
3580
|
+
rootCause: "The WAL archiver stopped.",
|
|
3581
|
+
}),
|
|
3582
|
+
),
|
|
3583
|
+
jira: jiraRoutes({
|
|
3584
|
+
[SEARCH]: ok(jiraSearchResponse()),
|
|
3585
|
+
[COMMENT]: COMMENT_CREATED,
|
|
3586
|
+
}),
|
|
3587
|
+
});
|
|
3588
|
+
|
|
3589
|
+
expect(wikiTextOf(postedCommentOf(trace))).toContain(
|
|
3590
|
+
"Severity: Low\nState: Identified\nRoot cause: The WAL archiver stopped.",
|
|
3591
|
+
);
|
|
3592
|
+
});
|
|
3593
|
+
});
|
|
3594
|
+
|
|
3595
|
+
/* ------------------------------- Echo loops ------------------------------- */
|
|
3596
|
+
|
|
3597
|
+
/*
|
|
3598
|
+
* Every write in one direction is an event in the other. Each test here takes
|
|
3599
|
+
* what one template ACTUALLY sent — the request body or database payload from
|
|
3600
|
+
* its trace, not a hand-written stand-in — replays it as the event the other
|
|
3601
|
+
* side would raise, and checks the loop stops there. A template whose output
|
|
3602
|
+
* drifted away from what the other side recognises would fail here even while
|
|
3603
|
+
* every single-template test still passed.
|
|
3604
|
+
*/
|
|
3605
|
+
describe.each(KINDS)(
|
|
3606
|
+
"simulated: the $noun templates do not echo each other",
|
|
3607
|
+
(kind: SimulatedKind) => {
|
|
3608
|
+
test(`(a) the issue OneUptime files comes back as issue_created, and ${kind.create}s no second ${kind.noun}`, async () => {
|
|
3609
|
+
const created: SimulationTrace = await runCreateIssue(kind);
|
|
3610
|
+
const fields: JSONObject = issueFieldsOf(created);
|
|
3611
|
+
|
|
3612
|
+
const echo: SimulationTrace = await runTemplate({
|
|
3613
|
+
templateId: kind.templates.createFromIssue,
|
|
3614
|
+
trigger: webhookDelivery(
|
|
3615
|
+
jiraIssueCreatedEvent({
|
|
3616
|
+
summary: fields["summary"] as string,
|
|
3617
|
+
labels: fields["labels"] as Array<string>,
|
|
3618
|
+
description: wikiTextOf(fields["description"]),
|
|
3619
|
+
reporter: AUTOMATION_USER,
|
|
3620
|
+
user: AUTOMATION_USER,
|
|
3621
|
+
}),
|
|
3622
|
+
),
|
|
3623
|
+
jira: createFromIssueJira({
|
|
3624
|
+
labels: fields["labels"] as Array<string>,
|
|
3625
|
+
}),
|
|
3626
|
+
database: createDatabase(kind),
|
|
3627
|
+
});
|
|
3628
|
+
|
|
3629
|
+
// The payload's own labels stop it, before Jira is asked anything.
|
|
3630
|
+
expect(echo.ports[kind.steps.ifCreate]).toBe("no");
|
|
3631
|
+
expect(metadataIdsOf(echo)).not.toContain(kind.components.createOne);
|
|
3632
|
+
expect(echo.requests).toEqual([]);
|
|
3633
|
+
expect(echo.logs).toEqual([alreadyLinkedLog(kind)]);
|
|
3634
|
+
});
|
|
3635
|
+
|
|
3636
|
+
/*
|
|
3637
|
+
* The same echo when the payload does not show the labels — a delivery
|
|
3638
|
+
* built before they were set, or one somebody forged. The labels OneUptime
|
|
3639
|
+
* filed the issue with are still what Jira returns, and they stop it.
|
|
3640
|
+
*/
|
|
3641
|
+
test("(a') the same issue_created without its labels in the payload is stopped by the labels Jira returns", async () => {
|
|
3642
|
+
const created: SimulationTrace = await runCreateIssue(kind);
|
|
3643
|
+
const fields: JSONObject = issueFieldsOf(created);
|
|
3644
|
+
|
|
3645
|
+
const echo: SimulationTrace = await runTemplate({
|
|
3646
|
+
templateId: kind.templates.createFromIssue,
|
|
3647
|
+
trigger: webhookDelivery(
|
|
3648
|
+
jiraIssueCreatedEvent({
|
|
3649
|
+
summary: fields["summary"] as string,
|
|
3650
|
+
labels: [],
|
|
3651
|
+
description: wikiTextOf(fields["description"]),
|
|
3652
|
+
reporter: AUTOMATION_USER,
|
|
3653
|
+
user: AUTOMATION_USER,
|
|
3654
|
+
}),
|
|
3655
|
+
),
|
|
3656
|
+
jira: createFromIssueJira({
|
|
3657
|
+
labels: fields["labels"] as Array<string>,
|
|
3658
|
+
}),
|
|
3659
|
+
database: createDatabase(kind),
|
|
3660
|
+
});
|
|
3661
|
+
|
|
3662
|
+
expect(echo.ports[kind.steps.ifCreate]).toBe("yes");
|
|
3663
|
+
expect(echo.ports["if-unlinked-1"]).toBe("no");
|
|
3664
|
+
expect(metadataIdsOf(echo)).toEqual([kind.components.severityFindMany]);
|
|
3665
|
+
expect(echo.requests).toEqual([GET_LABELS_REQUEST]);
|
|
3666
|
+
expect(echo.logs).toEqual([alreadyLinkedLog(kind)]);
|
|
3667
|
+
});
|
|
3668
|
+
|
|
3669
|
+
test(`(b) the ${kind.noun} ${kind.created} from Jira comes back through ${kind.noun}-on-create, and files no second issue`, async () => {
|
|
3670
|
+
const createdFromJira: SimulationTrace = await runCreateFromIssue(kind);
|
|
3671
|
+
const json: JSONObject = jsonOf(
|
|
3672
|
+
createdFromJira,
|
|
3673
|
+
kind.components.createOne,
|
|
3674
|
+
);
|
|
3675
|
+
|
|
3676
|
+
const echo: SimulationTrace = await runTemplate({
|
|
3677
|
+
templateId: kind.templates.createIssue,
|
|
3678
|
+
trigger: onRecord(
|
|
3679
|
+
kind.model({
|
|
3680
|
+
_id: kind.newId,
|
|
3681
|
+
title: json["title"] as string,
|
|
3682
|
+
description: json["description"] as string,
|
|
3683
|
+
customFields: json["customFields"] as JSONObject,
|
|
3684
|
+
}),
|
|
3685
|
+
),
|
|
3686
|
+
jira: jiraRoutes({ [CREATE_ISSUE]: ISSUE_CREATED }),
|
|
3687
|
+
});
|
|
3688
|
+
|
|
3689
|
+
expect(echo.requests).toEqual([]);
|
|
3690
|
+
expect(echo.logs).toEqual([
|
|
3691
|
+
`ℹ️ ${kind.number} was ${kind.created} from Jira issue ${JIRA_ISSUE_KEY}, so no new issue was created.`,
|
|
3692
|
+
]);
|
|
3693
|
+
});
|
|
3694
|
+
|
|
3695
|
+
test.each(kind.noteTemplates)(
|
|
3696
|
+
"(c) $templateId: the comment it posts comes back as comment_created, and writes no note",
|
|
3697
|
+
async (noteTemplate: NoteTemplate) => {
|
|
3698
|
+
const posted: SimulationTrace = await runNoteToComment(
|
|
3699
|
+
noteTemplate.templateId,
|
|
3700
|
+
kind.note(),
|
|
3701
|
+
);
|
|
3702
|
+
const comment: JSONObject = postedCommentOf(posted);
|
|
3703
|
+
|
|
3704
|
+
// As the webhook really carries it (wiki markup), and as ADF, in case a site sends that.
|
|
3705
|
+
for (const body of [wikiTextOf(comment), comment]) {
|
|
3706
|
+
const echo: SimulationTrace = await runComment(kind, {
|
|
3707
|
+
event: jiraCommentEvent({ body: body, author: AUTOMATION_USER }),
|
|
3708
|
+
});
|
|
3709
|
+
|
|
3710
|
+
expect(echo.requests).toEqual([]);
|
|
3711
|
+
expect(echo.databaseCalls).toEqual([]);
|
|
3712
|
+
expect(echo.logs).toEqual([COMMENT_FROM_ONEUPTIME_LOG]);
|
|
3713
|
+
}
|
|
3714
|
+
},
|
|
3715
|
+
);
|
|
3716
|
+
|
|
3717
|
+
test("(d) the note a Jira comment becomes fires the private note trigger, and posts no comment back", async () => {
|
|
3718
|
+
const noted: SimulationTrace = await runComment(kind, {
|
|
3719
|
+
event: jiraCommentEvent(),
|
|
3720
|
+
});
|
|
3721
|
+
const note: JSONObject = jsonOf(noted, kind.components.noteCreateOne);
|
|
3722
|
+
|
|
3723
|
+
const echo: SimulationTrace = await runNoteToComment(
|
|
3724
|
+
kind.templates.privateNote,
|
|
3725
|
+
kind.note({ note: note["note"] as string, authorName: null }),
|
|
3726
|
+
);
|
|
3727
|
+
|
|
3728
|
+
expect(echo.requests).toEqual([searchRequest(kind, ["key", "summary"])]);
|
|
3729
|
+
expect(echo.logs).toEqual([NOTE_FROM_JIRA_LOG]);
|
|
3730
|
+
});
|
|
3731
|
+
|
|
3732
|
+
test("(e) the note an issue change becomes posts no comment back", async () => {
|
|
3733
|
+
const noted: SimulationTrace = await runIssueChanges(kind, {
|
|
3734
|
+
event: jiraIssueUpdatedEvent({
|
|
3735
|
+
labels: linkedLabelsOf(kind),
|
|
3736
|
+
items: [
|
|
3737
|
+
changelogItem({
|
|
3738
|
+
field: "assignee",
|
|
3739
|
+
fromString: null,
|
|
3740
|
+
toString: "Marco Rossi",
|
|
3741
|
+
}),
|
|
3742
|
+
],
|
|
3743
|
+
}),
|
|
3744
|
+
});
|
|
3745
|
+
const note: JSONObject = jsonOf(noted, kind.components.noteCreateOne);
|
|
3746
|
+
|
|
3747
|
+
const echo: SimulationTrace = await runNoteToComment(
|
|
3748
|
+
kind.templates.privateNote,
|
|
3749
|
+
kind.note({ note: note["note"] as string, authorName: null }),
|
|
3750
|
+
);
|
|
3751
|
+
|
|
3752
|
+
expect(echo.requests).toEqual([searchRequest(kind, ["key", "summary"])]);
|
|
3753
|
+
expect(echo.logs).toEqual([NOTE_FROM_JIRA_LOG]);
|
|
3754
|
+
});
|
|
3755
|
+
|
|
3756
|
+
test(`(f) the comment an ${kind.noun} edit posts comes back as comment_created, and writes no note`, async () => {
|
|
3757
|
+
const posted: SimulationTrace = await runTemplate({
|
|
3758
|
+
templateId: kind.templates.updateComment,
|
|
3759
|
+
trigger: onRecord(
|
|
3760
|
+
kind.model({ rootCause: "A bad deploy of the payments service." }),
|
|
3761
|
+
),
|
|
3762
|
+
jira: jiraRoutes({
|
|
3763
|
+
[SEARCH]: ok(jiraSearchResponse()),
|
|
3764
|
+
[COMMENT]: COMMENT_CREATED,
|
|
3765
|
+
}),
|
|
3766
|
+
});
|
|
3767
|
+
|
|
3768
|
+
const echo: SimulationTrace = await runComment(kind, {
|
|
3769
|
+
event: jiraCommentEvent({
|
|
3770
|
+
body: wikiTextOf(postedCommentOf(posted)),
|
|
3771
|
+
author: AUTOMATION_USER,
|
|
3772
|
+
}),
|
|
3773
|
+
});
|
|
3774
|
+
|
|
3775
|
+
expect(echo.requests).toEqual([]);
|
|
3776
|
+
expect(echo.databaseCalls).toEqual([]);
|
|
3777
|
+
expect(echo.logs).toEqual([COMMENT_FROM_ONEUPTIME_LOG]);
|
|
3778
|
+
});
|
|
3779
|
+
|
|
3780
|
+
test("(g) resolving in OneUptime moves the issue to Done, and the issue_updated that follows changes nothing", async () => {
|
|
3781
|
+
const transitioned: SimulationTrace = await runTemplate({
|
|
3782
|
+
templateId: kind.templates.transition,
|
|
3783
|
+
trigger: onRecord(kind.model({ state: kind.resolvedState })),
|
|
3784
|
+
jira: jiraRoutes({
|
|
3785
|
+
[SEARCH]: ok(jiraSearchResponse()),
|
|
3786
|
+
[TRANSITION]: NO_CONTENT,
|
|
3787
|
+
}),
|
|
3788
|
+
});
|
|
3789
|
+
|
|
3790
|
+
// Where the transition it chose leads is the issue's new status.
|
|
3791
|
+
const transitionId: string = (
|
|
3792
|
+
(transitioned.requests[1]!.body as JSONObject)[
|
|
3793
|
+
"transition"
|
|
3794
|
+
] as JSONObject
|
|
3795
|
+
)["id"] as string;
|
|
3796
|
+
const taken: JSONObject | undefined = (
|
|
3797
|
+
firstIssueOf(jiraSearchResponse())["transitions"] as Array<JSONObject>
|
|
3798
|
+
).find((transition: JSONObject) => {
|
|
3799
|
+
return transition["id"] === transitionId;
|
|
3800
|
+
});
|
|
3801
|
+
|
|
3802
|
+
expect(taken).toBeDefined();
|
|
3803
|
+
expect(taken!["to"]).toEqual(DONE);
|
|
3804
|
+
|
|
3805
|
+
const event: JSONObject = jiraIssueUpdatedEvent({
|
|
3806
|
+
items: RESOLVED_CHANGELOG,
|
|
3807
|
+
labels: linkedLabelsOf(kind),
|
|
3808
|
+
status: taken!["to"] as JSONObject,
|
|
3809
|
+
user: AUTOMATION_USER,
|
|
3810
|
+
});
|
|
3811
|
+
|
|
3812
|
+
const status: SimulationTrace = await runTemplate({
|
|
3813
|
+
templateId: kind.templates.statusToState,
|
|
3814
|
+
trigger: webhookDelivery(event),
|
|
3815
|
+
database: statusDatabase(kind, recordOf(kind, kind.resolvedState)),
|
|
3816
|
+
});
|
|
3817
|
+
|
|
3818
|
+
expect(metadataIdsOf(status)).not.toContain(
|
|
3819
|
+
kind.components.timelineCreateOne,
|
|
3820
|
+
);
|
|
3821
|
+
expect(status.logs).toEqual([
|
|
3822
|
+
`ℹ️ ${kind.number} is already Resolved, so Jira issue ${JIRA_ISSUE_KEY} moving to Done changes nothing.`,
|
|
3823
|
+
]);
|
|
3824
|
+
|
|
3825
|
+
// The same delivery reaches the issue-changes template, which leaves status to the one above.
|
|
3826
|
+
const changes: SimulationTrace = await runIssueChanges(kind, {
|
|
3827
|
+
event: event,
|
|
3828
|
+
});
|
|
3829
|
+
|
|
3830
|
+
expect(changes.databaseCalls).toEqual([]);
|
|
3831
|
+
});
|
|
3832
|
+
|
|
3833
|
+
test(`(h) resolving from Jira fires ${kind.noun}-on-update, which finds the issue already Done and moves nothing`, async () => {
|
|
3834
|
+
const resolved: SimulationTrace = await runTemplate({
|
|
3835
|
+
templateId: kind.templates.statusToState,
|
|
3836
|
+
trigger: webhookDelivery(
|
|
3837
|
+
jiraIssueUpdatedEvent({
|
|
3838
|
+
items: RESOLVED_CHANGELOG,
|
|
3839
|
+
labels: linkedLabelsOf(kind),
|
|
3840
|
+
status: DONE,
|
|
3841
|
+
}),
|
|
3842
|
+
),
|
|
3843
|
+
database: statusDatabase(kind, recordOf(kind, kind.acknowledgedState)),
|
|
3844
|
+
});
|
|
3845
|
+
|
|
3846
|
+
const row: JSONObject = jsonOf(
|
|
3847
|
+
resolved,
|
|
3848
|
+
kind.components.timelineCreateOne,
|
|
3849
|
+
);
|
|
3850
|
+
const newState: JSONObject | undefined = kind.states.find(
|
|
3851
|
+
(state: JSONObject) => {
|
|
3852
|
+
return state["_id"] === row[kind.timelineStateColumn];
|
|
3853
|
+
},
|
|
3854
|
+
);
|
|
3855
|
+
|
|
3856
|
+
expect(newState).toBeDefined();
|
|
3857
|
+
|
|
3858
|
+
const echo: SimulationTrace = await runTemplate({
|
|
3859
|
+
templateId: kind.templates.transition,
|
|
3860
|
+
trigger: onRecord(
|
|
3861
|
+
kind.model({
|
|
3862
|
+
_id: row[kind.idColumn] as string,
|
|
3863
|
+
state: newState as JSONObject,
|
|
3864
|
+
}),
|
|
3865
|
+
),
|
|
3866
|
+
jira: jiraRoutes({
|
|
3867
|
+
[SEARCH]: ok(
|
|
3868
|
+
jiraSearchResponse({
|
|
3869
|
+
status: DONE,
|
|
3870
|
+
transitions: [
|
|
3871
|
+
jiraTransition({ id: "11", name: "Reopen", to: TO_DO }),
|
|
3872
|
+
],
|
|
3873
|
+
}),
|
|
3874
|
+
),
|
|
3875
|
+
[TRANSITION]: NO_CONTENT,
|
|
3876
|
+
}),
|
|
3877
|
+
});
|
|
3878
|
+
|
|
3879
|
+
expect(echo.requests).toEqual([
|
|
3880
|
+
searchRequest(kind, ["key", "status"], true),
|
|
3881
|
+
]);
|
|
3882
|
+
expect(echo.logs).toEqual([
|
|
3883
|
+
`ℹ️ Jira issue ${JIRA_ISSUE_KEY} is already Done.`,
|
|
3884
|
+
]);
|
|
3885
|
+
});
|
|
3886
|
+
|
|
3887
|
+
test(`(i) the link labels the create-from-Jira template adds come back as issue_updated, and write nothing`, async () => {
|
|
3888
|
+
const createdFromJira: SimulationTrace = await runCreateFromIssue(kind);
|
|
3889
|
+
const added: Array<string> = addedLabelsOf(createdFromJira);
|
|
3890
|
+
|
|
3891
|
+
const event: JSONObject = jiraIssueUpdatedEvent({
|
|
3892
|
+
labels: added,
|
|
3893
|
+
items: [
|
|
3894
|
+
changelogItem({
|
|
3895
|
+
field: "labels",
|
|
3896
|
+
fromString: "",
|
|
3897
|
+
toString: added.join(" "),
|
|
3898
|
+
}),
|
|
3899
|
+
],
|
|
3900
|
+
user: AUTOMATION_USER,
|
|
3901
|
+
});
|
|
3902
|
+
|
|
3903
|
+
const changes: SimulationTrace = await runIssueChanges(kind, {
|
|
3904
|
+
event: event,
|
|
3905
|
+
});
|
|
3906
|
+
|
|
3907
|
+
expect(changes.databaseCalls).toEqual([]);
|
|
3908
|
+
expect(changes.logs).toEqual([NOTHING_WORTH_A_NOTE_LOG]);
|
|
3909
|
+
|
|
3910
|
+
const status: SimulationTrace = await runTemplate({
|
|
3911
|
+
templateId: kind.templates.statusToState,
|
|
3912
|
+
trigger: webhookDelivery(event),
|
|
3913
|
+
database: statusDatabase(kind, recordOf(kind, kind.acknowledgedState)),
|
|
3914
|
+
});
|
|
3915
|
+
|
|
3916
|
+
expect(status.databaseCalls).toEqual([]);
|
|
3917
|
+
expect(status.logs).toEqual([
|
|
3918
|
+
`ℹ️ Jira issue ${JIRA_ISSUE_KEY} changed, but its status did not.`,
|
|
3919
|
+
]);
|
|
3920
|
+
});
|
|
3921
|
+
|
|
3922
|
+
/*
|
|
3923
|
+
* Jira retries a delivery it thinks failed, so the same issue_created can
|
|
3924
|
+
* arrive after the first delivery already created a record and labelled
|
|
3925
|
+
* the issue. The payload is unchanged — it still shows no labels — and
|
|
3926
|
+
* only Jira's answer can tell the two deliveries apart.
|
|
3927
|
+
*/
|
|
3928
|
+
test(`(j) a retried issue_created ${kind.create}s no second ${kind.noun}, because the first delivery's labels are now in Jira`, async () => {
|
|
3929
|
+
const first: SimulationTrace = await runCreateFromIssue(kind);
|
|
3930
|
+
|
|
3931
|
+
expect(metadataIdsOf(first)).toContain(kind.components.createOne);
|
|
3932
|
+
|
|
3933
|
+
const retry: SimulationTrace = await runCreateFromIssue(kind, {
|
|
3934
|
+
labels: addedLabelsOf(first),
|
|
3935
|
+
});
|
|
3936
|
+
|
|
3937
|
+
expect(metadataIdsOf(retry)).toEqual([kind.components.severityFindMany]);
|
|
3938
|
+
expect(retry.requests).toEqual([GET_LABELS_REQUEST]);
|
|
3939
|
+
expect(retry.logs).toEqual([alreadyLinkedLog(kind)]);
|
|
3940
|
+
});
|
|
3941
|
+
},
|
|
3942
|
+
);
|
|
3943
|
+
|
|
3944
|
+
/* ------------------------------- Across kinds ------------------------------- */
|
|
3945
|
+
|
|
3946
|
+
interface KindPair {
|
|
3947
|
+
/** The kind of record the issue is linked to. */
|
|
3948
|
+
linked: SimulatedKind;
|
|
3949
|
+
/** The kind whose templates must leave the issue alone. */
|
|
3950
|
+
other: SimulatedKind;
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
const KIND_PAIRS: Array<KindPair> = [
|
|
3954
|
+
{ linked: INCIDENT, other: ALERT },
|
|
3955
|
+
{ linked: ALERT, other: INCIDENT },
|
|
3956
|
+
];
|
|
3957
|
+
|
|
3958
|
+
/*
|
|
3959
|
+
* A project can enable the incident and the alert templates side by side,
|
|
3960
|
+
* and then every Jira webhook reaches both sets. An issue linked to one kind
|
|
3961
|
+
* of record must never become, change, or collect notes on a record of the
|
|
3962
|
+
* other kind. Each test sends the same event to both kinds' templates, and the
|
|
3963
|
+
* linked kind acting on it is what shows the event was a real one.
|
|
3964
|
+
*/
|
|
3965
|
+
describe.each(KIND_PAIRS)(
|
|
3966
|
+
"simulated: an issue linked to an $linked.noun is left alone by the $other.noun templates",
|
|
3967
|
+
(pair: KindPair) => {
|
|
3968
|
+
const linked: SimulatedKind = pair.linked;
|
|
3969
|
+
const other: SimulatedKind = pair.other;
|
|
3970
|
+
|
|
3971
|
+
test(`the issue the ${linked.noun} template files comes back as issue_created, and the ${other.noun} template ${other.create}s nothing`, async () => {
|
|
3972
|
+
const created: SimulationTrace = await runCreateIssue(linked);
|
|
3973
|
+
const fields: JSONObject = issueFieldsOf(created);
|
|
3974
|
+
|
|
3975
|
+
expect(fields["labels"]).toEqual(linkedLabelsOf(linked));
|
|
3976
|
+
|
|
3977
|
+
const echo: SimulationTrace = await runTemplate({
|
|
3978
|
+
templateId: other.templates.createFromIssue,
|
|
3979
|
+
trigger: webhookDelivery(
|
|
3980
|
+
jiraIssueCreatedEvent({
|
|
3981
|
+
summary: fields["summary"] as string,
|
|
3982
|
+
labels: fields["labels"] as Array<string>,
|
|
3983
|
+
description: wikiTextOf(fields["description"]),
|
|
3984
|
+
reporter: AUTOMATION_USER,
|
|
3985
|
+
user: AUTOMATION_USER,
|
|
3986
|
+
}),
|
|
3987
|
+
),
|
|
3988
|
+
jira: createFromIssueJira({
|
|
3989
|
+
labels: fields["labels"] as Array<string>,
|
|
3990
|
+
}),
|
|
3991
|
+
database: createDatabase(other),
|
|
3992
|
+
});
|
|
3993
|
+
|
|
3994
|
+
expect(echo.ports[other.steps.ifCreate]).toBe("no");
|
|
3995
|
+
expect(metadataIdsOf(echo)).toEqual([other.components.severityFindMany]);
|
|
3996
|
+
expect(echo.requests).toEqual([]);
|
|
3997
|
+
expect(echo.logs).toEqual([alreadyLinkedLog(other)]);
|
|
3998
|
+
});
|
|
3999
|
+
|
|
4000
|
+
/*
|
|
4001
|
+
* The bare oneuptime label is enough on its own to stop a create, so the
|
|
4002
|
+
* tests above would pass even if only it counted. Without it — someone
|
|
4003
|
+
* tidied the labels away — the record's own label still has to count as
|
|
4004
|
+
* a link, whichever kind of record it names.
|
|
4005
|
+
*/
|
|
4006
|
+
test.each([
|
|
4007
|
+
["in the payload", true],
|
|
4008
|
+
["only in Jira", false],
|
|
4009
|
+
])(
|
|
4010
|
+
`an issue carrying only the ${linked.noun}'s own label (%s) ${other.create}s no ${other.noun}`,
|
|
4011
|
+
async (_where: string, inPayload: boolean) => {
|
|
4012
|
+
const labels: Array<string> = [linked.label()];
|
|
4013
|
+
|
|
4014
|
+
const trace: SimulationTrace = await runCreateFromIssue(other, {
|
|
4015
|
+
event: jiraIssueCreatedEvent({ labels: inPayload ? labels : [] }),
|
|
4016
|
+
labels: labels,
|
|
4017
|
+
});
|
|
4018
|
+
|
|
4019
|
+
expect(trace.ports[other.steps.ifCreate]).toBe(
|
|
4020
|
+
inPayload ? "no" : "yes",
|
|
4021
|
+
);
|
|
4022
|
+
expect(metadataIdsOf(trace)).toEqual([
|
|
4023
|
+
other.components.severityFindMany,
|
|
4024
|
+
]);
|
|
4025
|
+
expect(trace.requests).toEqual(inPayload ? [] : [GET_LABELS_REQUEST]);
|
|
4026
|
+
expect(trace.logs).toEqual([alreadyLinkedLog(other)]);
|
|
4027
|
+
},
|
|
4028
|
+
);
|
|
4029
|
+
|
|
4030
|
+
/*
|
|
4031
|
+
* Both create-from-Jira templates registered for the Issue created event:
|
|
4032
|
+
* each receives the same delivery. Once the first has created its record
|
|
4033
|
+
* and labelled the issue, the second's check with Jira has to refuse it.
|
|
4034
|
+
*/
|
|
4035
|
+
test.each([
|
|
4036
|
+
["both link labels", false],
|
|
4037
|
+
[`only the ${linked.noun}'s own label`, true],
|
|
4038
|
+
])(
|
|
4039
|
+
`when the ${linked.noun} template has already linked the issue, the ${other.noun} template's check with Jira refuses an issue carrying %s`,
|
|
4040
|
+
async (_carrying: string, dropLinkLabel: boolean) => {
|
|
4041
|
+
const first: SimulationTrace = await runCreateFromIssue(linked);
|
|
4042
|
+
|
|
4043
|
+
expect(metadataIdsOf(first)).toEqual([
|
|
4044
|
+
linked.components.severityFindMany,
|
|
4045
|
+
linked.components.createOne,
|
|
4046
|
+
]);
|
|
4047
|
+
|
|
4048
|
+
const added: Array<string> = addedLabelsOf(first);
|
|
4049
|
+
|
|
4050
|
+
expect(added).toEqual([
|
|
4051
|
+
JIRA_LINK_LABEL,
|
|
4052
|
+
`${linked.labelPrefix}${linked.newId}`,
|
|
4053
|
+
]);
|
|
4054
|
+
|
|
4055
|
+
// The same delivery, which still shows no labels, reaches the other kind's template.
|
|
4056
|
+
const second: SimulationTrace = await runCreateFromIssue(other, {
|
|
4057
|
+
labels: dropLinkLabel
|
|
4058
|
+
? added.filter((label: string) => {
|
|
4059
|
+
return label !== JIRA_LINK_LABEL;
|
|
4060
|
+
})
|
|
4061
|
+
: added,
|
|
4062
|
+
});
|
|
4063
|
+
|
|
4064
|
+
expect(second.executed).toEqual([
|
|
4065
|
+
"webhook-1",
|
|
4066
|
+
"find-severities-1",
|
|
4067
|
+
other.steps.prepareRecord,
|
|
4068
|
+
other.steps.ifCreate,
|
|
4069
|
+
"get-issue-1",
|
|
4070
|
+
"confirm-unlinked-1",
|
|
4071
|
+
"if-unlinked-1",
|
|
4072
|
+
"log-already-linked",
|
|
4073
|
+
]);
|
|
4074
|
+
expect(metadataIdsOf(second)).toEqual([
|
|
4075
|
+
other.components.severityFindMany,
|
|
4076
|
+
]);
|
|
4077
|
+
expect(second.requests).toEqual([GET_LABELS_REQUEST]);
|
|
4078
|
+
expect(second.logs).toEqual([alreadyLinkedLog(other)]);
|
|
4079
|
+
},
|
|
4080
|
+
);
|
|
4081
|
+
|
|
4082
|
+
test(`a status change on the ${linked.noun}'s issue moves the ${linked.noun}, and no ${other.noun}`, async () => {
|
|
4083
|
+
const event: JSONObject = jiraIssueUpdatedEvent({
|
|
4084
|
+
items: RESOLVED_CHANGELOG,
|
|
4085
|
+
labels: linkedLabelsOf(linked),
|
|
4086
|
+
status: DONE,
|
|
4087
|
+
});
|
|
4088
|
+
|
|
4089
|
+
const otherTrace: SimulationTrace = await runTemplate({
|
|
4090
|
+
templateId: other.templates.statusToState,
|
|
4091
|
+
trigger: webhookDelivery(event),
|
|
4092
|
+
database: statusDatabase(
|
|
4093
|
+
other,
|
|
4094
|
+
recordOf(other, other.acknowledgedState),
|
|
4095
|
+
),
|
|
4096
|
+
});
|
|
4097
|
+
|
|
4098
|
+
expect(otherTrace.executed).toEqual([
|
|
4099
|
+
"webhook-1",
|
|
4100
|
+
"read-event-1",
|
|
4101
|
+
"if-status-changed-1",
|
|
4102
|
+
"log-ignored",
|
|
4103
|
+
]);
|
|
4104
|
+
expect(otherTrace.databaseCalls).toEqual([]);
|
|
4105
|
+
expect(otherTrace.logs).toEqual([notLinkedLog(other)]);
|
|
4106
|
+
|
|
4107
|
+
const linkedTrace: SimulationTrace = await runTemplate({
|
|
4108
|
+
templateId: linked.templates.statusToState,
|
|
4109
|
+
trigger: webhookDelivery(event),
|
|
4110
|
+
database: statusDatabase(
|
|
4111
|
+
linked,
|
|
4112
|
+
recordOf(linked, linked.acknowledgedState),
|
|
4113
|
+
),
|
|
4114
|
+
});
|
|
4115
|
+
|
|
4116
|
+
expect(
|
|
4117
|
+
jsonOf(linkedTrace, linked.components.timelineCreateOne),
|
|
4118
|
+
).toMatchObject({
|
|
4119
|
+
[linked.idColumn]: linked.id,
|
|
4120
|
+
[linked.timelineStateColumn]: linked.resolvedState["_id"] as string,
|
|
4121
|
+
});
|
|
4122
|
+
});
|
|
4123
|
+
|
|
4124
|
+
test(`a comment on the ${linked.noun}'s issue is noted on the ${linked.noun}, and on no ${other.noun}`, async () => {
|
|
4125
|
+
const otherTrace: SimulationTrace = await runComment(other, {
|
|
4126
|
+
event: jiraCommentEvent(),
|
|
4127
|
+
labels: linkedLabelsOf(linked),
|
|
4128
|
+
});
|
|
4129
|
+
|
|
4130
|
+
expect(otherTrace.requests).toEqual([GET_LABELS_REQUEST]);
|
|
4131
|
+
expect(otherTrace.databaseCalls).toEqual([]);
|
|
4132
|
+
expect(otherTrace.logs).toEqual([notLinkedLog(other)]);
|
|
4133
|
+
|
|
4134
|
+
const linkedTrace: SimulationTrace = await runComment(linked, {
|
|
4135
|
+
event: jiraCommentEvent(),
|
|
4136
|
+
labels: linkedLabelsOf(linked),
|
|
4137
|
+
});
|
|
4138
|
+
|
|
4139
|
+
expect(jsonOf(linkedTrace, linked.components.noteCreateOne)).toEqual({
|
|
4140
|
+
[linked.idColumn]: linked.id,
|
|
4141
|
+
note: EXPECTED_COMMENT_NOTE,
|
|
4142
|
+
});
|
|
4143
|
+
});
|
|
4144
|
+
|
|
4145
|
+
test(`an edit to the ${linked.noun}'s issue is noted on the ${linked.noun}, and on no ${other.noun}`, async () => {
|
|
4146
|
+
const event: JSONObject = priorityChange(linkedLabelsOf(linked));
|
|
4147
|
+
|
|
4148
|
+
const otherTrace: SimulationTrace = await runIssueChanges(other, {
|
|
4149
|
+
event: event,
|
|
4150
|
+
});
|
|
4151
|
+
|
|
4152
|
+
expect(otherTrace.databaseCalls).toEqual([]);
|
|
4153
|
+
expect(otherTrace.logs).toEqual([notLinkedLog(other)]);
|
|
4154
|
+
|
|
4155
|
+
const linkedTrace: SimulationTrace = await runIssueChanges(linked, {
|
|
4156
|
+
event: event,
|
|
4157
|
+
});
|
|
4158
|
+
|
|
4159
|
+
expect(
|
|
4160
|
+
jsonOf(linkedTrace, linked.components.noteCreateOne)[linked.idColumn],
|
|
4161
|
+
).toBe(linked.id);
|
|
4162
|
+
});
|
|
4163
|
+
|
|
4164
|
+
test(`a comment the ${linked.noun}'s note template posts comes back, and is copied to no ${other.noun} either`, async () => {
|
|
4165
|
+
const posted: SimulationTrace = await runNoteToComment(
|
|
4166
|
+
linked.templates.privateNote,
|
|
4167
|
+
linked.note(),
|
|
4168
|
+
);
|
|
4169
|
+
|
|
4170
|
+
// Stopped by its marker before the other template even asks Jira which record it is on.
|
|
4171
|
+
const echo: SimulationTrace = await runComment(other, {
|
|
4172
|
+
event: jiraCommentEvent({
|
|
4173
|
+
body: wikiTextOf(postedCommentOf(posted)),
|
|
4174
|
+
author: AUTOMATION_USER,
|
|
4175
|
+
}),
|
|
4176
|
+
labels: linkedLabelsOf(linked),
|
|
4177
|
+
});
|
|
4178
|
+
|
|
4179
|
+
expect(echo.requests).toEqual([]);
|
|
4180
|
+
expect(echo.databaseCalls).toEqual([]);
|
|
4181
|
+
expect(echo.logs).toEqual([COMMENT_FROM_ONEUPTIME_LOG]);
|
|
4182
|
+
});
|
|
4183
|
+
|
|
4184
|
+
/*
|
|
4185
|
+
* The other direction. Jira here answers a label search the way Jira
|
|
4186
|
+
* does, so an issue is found only by the label it carries: the other
|
|
4187
|
+
* kind's transitions, notes and edits search by their own label, find
|
|
4188
|
+
* nothing, and post nothing to the linked kind's issue.
|
|
4189
|
+
*/
|
|
4190
|
+
test(`the ${other.noun} templates' searches do not find the ${linked.noun}'s issue, so nothing is posted to it`, async () => {
|
|
4191
|
+
const jira: FakeJiraFunction = labelAwareJira(linkedLabelsOf(linked));
|
|
4192
|
+
|
|
4193
|
+
const transition: SimulationTrace = await runTemplate({
|
|
4194
|
+
templateId: other.templates.transition,
|
|
4195
|
+
trigger: onRecord(other.model({ state: other.resolvedState })),
|
|
4196
|
+
jira: jira,
|
|
4197
|
+
});
|
|
4198
|
+
const note: SimulationTrace = await runTemplate({
|
|
4199
|
+
templateId: other.templates.privateNote,
|
|
4200
|
+
trigger: onRecord(other.note()),
|
|
4201
|
+
jira: jira,
|
|
4202
|
+
});
|
|
4203
|
+
const edit: SimulationTrace = await runTemplate({
|
|
4204
|
+
templateId: other.templates.updateComment,
|
|
4205
|
+
trigger: onRecord(other.model()),
|
|
4206
|
+
jira: jira,
|
|
4207
|
+
});
|
|
4208
|
+
|
|
4209
|
+
expect(transition.requests).toEqual([
|
|
4210
|
+
searchRequest(other, ["key", "status"], true),
|
|
4211
|
+
]);
|
|
4212
|
+
expect(note.requests).toEqual([searchRequest(other, ["key", "summary"])]);
|
|
4213
|
+
expect(edit.requests).toEqual([searchRequest(other, ["key", "summary"])]);
|
|
4214
|
+
|
|
4215
|
+
for (const trace of [transition, note, edit]) {
|
|
4216
|
+
expect(trace.logs).toEqual([noLinkedIssueLog(other)]);
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
// The same Jira answers the linked kind's search, so it was the label that decided.
|
|
4220
|
+
const linkedTransition: SimulationTrace = await runTemplate({
|
|
4221
|
+
templateId: linked.templates.transition,
|
|
4222
|
+
trigger: onRecord(linked.model({ state: linked.resolvedState })),
|
|
4223
|
+
jira: jira,
|
|
4224
|
+
});
|
|
4225
|
+
|
|
4226
|
+
expect(linkedTransition.requests).toEqual([
|
|
4227
|
+
searchRequest(linked, ["key", "status"], true),
|
|
4228
|
+
transitionRequest("31"),
|
|
4229
|
+
]);
|
|
4230
|
+
});
|
|
4231
|
+
},
|
|
4232
|
+
);
|
|
4233
|
+
|
|
4234
|
+
/* ------------------------------- Security ------------------------------- */
|
|
4235
|
+
|
|
4236
|
+
const TOKEN_REFERENCE: string = "{{local.variables.jiraBasicAuthToken}}";
|
|
4237
|
+
const HEADERS_REFERENCE: string =
|
|
4238
|
+
"{{local.components.webhook-1.returnValues.request-headers}}";
|
|
4239
|
+
|
|
4240
|
+
type ExpectNothingSubstitutedFunction = (trace: SimulationTrace) => void;
|
|
4241
|
+
|
|
4242
|
+
/*
|
|
4243
|
+
* What leaves the workflow — URLs, request bodies, database payloads — holds
|
|
4244
|
+
* no reference still waiting to be resolved, no secret, and none of the
|
|
4245
|
+
* webhook's own headers.
|
|
4246
|
+
*/
|
|
4247
|
+
const expectNothingSubstituted: ExpectNothingSubstitutedFunction = (
|
|
4248
|
+
trace: SimulationTrace,
|
|
4249
|
+
): void => {
|
|
4250
|
+
const outgoing: Array<string> = [
|
|
4251
|
+
...trace.requests.map((request: JiraRequest) => {
|
|
4252
|
+
return request.url + JSON.stringify(request.body ?? null);
|
|
4253
|
+
}),
|
|
4254
|
+
...trace.databaseCalls.map((call: DatabaseCall) => {
|
|
4255
|
+
return JSON.stringify(call.args);
|
|
4256
|
+
}),
|
|
4257
|
+
];
|
|
4258
|
+
|
|
4259
|
+
for (const text of outgoing) {
|
|
4260
|
+
expect(text).not.toContain("{{");
|
|
4261
|
+
expect(text).not.toContain(JIRA_TOKEN);
|
|
4262
|
+
expect(text).not.toContain(WEBHOOK_IDENTIFIER);
|
|
4263
|
+
}
|
|
4264
|
+
};
|
|
4265
|
+
|
|
4266
|
+
type ExpectTokenOnlyInAuthorizationFunction = (trace: SimulationTrace) => void;
|
|
4267
|
+
|
|
4268
|
+
/*
|
|
4269
|
+
* The secret has exactly one place to be: the Authorization header of a
|
|
4270
|
+
* request to Jira. Anywhere else in the trace — a URL, a body, a database
|
|
4271
|
+
* payload, a log line, a step's return value — is somewhere the run log or a
|
|
4272
|
+
* third party would see it.
|
|
4273
|
+
*/
|
|
4274
|
+
const expectTokenOnlyInAuthorization: ExpectTokenOnlyInAuthorizationFunction = (
|
|
4275
|
+
trace: SimulationTrace,
|
|
4276
|
+
): void => {
|
|
4277
|
+
for (const request of trace.requests) {
|
|
4278
|
+
expect(request.headers).toEqual(AUTHORIZATION);
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
const everythingElse: string = JSON.stringify({
|
|
4282
|
+
executed: trace.executed,
|
|
4283
|
+
requests: trace.requests.map((request: JiraRequest) => {
|
|
4284
|
+
return { method: request.method, url: request.url, body: request.body };
|
|
4285
|
+
}),
|
|
4286
|
+
databaseCalls: trace.databaseCalls,
|
|
4287
|
+
logs: trace.logs,
|
|
4288
|
+
runLog: trace.runLog,
|
|
4289
|
+
components: trace.storage.local.components,
|
|
4290
|
+
});
|
|
4291
|
+
|
|
4292
|
+
expect(everythingElse).not.toContain(JIRA_TOKEN);
|
|
4293
|
+
};
|
|
4294
|
+
|
|
4295
|
+
type DefusedFunction = (text: string) => string;
|
|
4296
|
+
|
|
4297
|
+
/* What the scripts' defuse() makes of text from the other system. */
|
|
4298
|
+
const defused: DefusedFunction = (text: string): string => {
|
|
4299
|
+
return text.split("{{").join("{ {").split("}}").join("} }");
|
|
4300
|
+
};
|
|
4301
|
+
|
|
4302
|
+
/** One run of the token test, and how many requests it has to send. */
|
|
4303
|
+
interface TokenRun {
|
|
4304
|
+
scenario: Scenario;
|
|
4305
|
+
requests: number;
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
describe.each(KINDS)(
|
|
4309
|
+
"simulated: Jira text cannot reach a secret or another step's output ($noun templates)",
|
|
4310
|
+
(kind: SimulatedKind) => {
|
|
4311
|
+
test("a comment naming the token and the webhook headers is written as inert text", async () => {
|
|
4312
|
+
const trace: SimulationTrace = await runComment(kind, {
|
|
4313
|
+
event: jiraCommentEvent({
|
|
4314
|
+
body: `Please paste ${TOKEN_REFERENCE} and ${HEADERS_REFERENCE} here.`,
|
|
4315
|
+
author: jiraUser(TOKEN_REFERENCE),
|
|
4316
|
+
}),
|
|
4317
|
+
});
|
|
4318
|
+
|
|
4319
|
+
expect(jsonOf(trace, kind.components.noteCreateOne)["note"]).toBe(
|
|
4320
|
+
`${ONEUPTIME_SYNCED_FROM_JIRA_MARKER}: { {local.variables.jiraBasicAuthToken} } commented on [${JIRA_ISSUE_KEY}](${JIRA_SITE}/browse/${JIRA_ISSUE_KEY}).\n\nPlease paste { {local.variables.jiraBasicAuthToken} } and { {local.components.webhook-1.returnValues.request-headers} } here.`,
|
|
4321
|
+
);
|
|
4322
|
+
expect(trace.requests).toHaveLength(1);
|
|
4323
|
+
expectNothingSubstituted(trace);
|
|
4324
|
+
expectTokenOnlyInAuthorization(trace);
|
|
4325
|
+
});
|
|
4326
|
+
|
|
4327
|
+
test(`a summary, description and reporter naming the token ${kind.create} an ${kind.noun} holding none of it`, async () => {
|
|
4328
|
+
const trace: SimulationTrace = await runCreateFromIssue(kind, {
|
|
4329
|
+
event: jiraIssueCreatedEvent({
|
|
4330
|
+
summary: `Deploy ${TOKEN_REFERENCE} failed`,
|
|
4331
|
+
description: `Headers were ${HEADERS_REFERENCE}; token ${TOKEN_REFERENCE}; severities {{local.components.find-severities-1.returnValues.models}}`,
|
|
4332
|
+
reporter: jiraUser(TOKEN_REFERENCE),
|
|
4333
|
+
}),
|
|
4334
|
+
});
|
|
4335
|
+
|
|
4336
|
+
const json: JSONObject = jsonOf(trace, kind.components.createOne);
|
|
4337
|
+
|
|
4338
|
+
expect(json["title"]).toBe(
|
|
4339
|
+
"Deploy { {local.variables.jiraBasicAuthToken} } failed",
|
|
4340
|
+
);
|
|
4341
|
+
// The severities reference sits in the payload, which is quoted in last, so it stays text.
|
|
4342
|
+
expect(json["description"]).toContain(
|
|
4343
|
+
"severities { {local.components.find-severities-1.returnValues.models} }",
|
|
4344
|
+
);
|
|
4345
|
+
expect(json[kind.severityIdColumn]).toBe(kind.mostSevere.id);
|
|
4346
|
+
expect(trace.requests).toEqual([
|
|
4347
|
+
GET_LABELS_REQUEST,
|
|
4348
|
+
linkLabelsRequest(kind),
|
|
4349
|
+
]);
|
|
4350
|
+
expectNothingSubstituted(trace);
|
|
4351
|
+
expectTokenOnlyInAuthorization(trace);
|
|
4352
|
+
});
|
|
4353
|
+
|
|
4354
|
+
/*
|
|
4355
|
+
* The issue key from the payload is put into the GET's URL unencoded, so
|
|
4356
|
+
* a forged key could otherwise point the credentials at any Jira endpoint.
|
|
4357
|
+
*/
|
|
4358
|
+
test.each([
|
|
4359
|
+
["OPS-17/../../../myself"],
|
|
4360
|
+
["OPS-17?expand=renderedFields#"],
|
|
4361
|
+
["../../../rest/api/3/user/search?query=a"],
|
|
4362
|
+
])(
|
|
4363
|
+
"a forged issue key %s sends no request with the credentials",
|
|
4364
|
+
async (issueKey: string) => {
|
|
4365
|
+
const trace: SimulationTrace = await runCreateFromIssue(kind, {
|
|
4366
|
+
event: jiraIssueCreatedEvent({ key: issueKey }),
|
|
4367
|
+
});
|
|
4368
|
+
|
|
4369
|
+
expect(trace.requests).toEqual([]);
|
|
4370
|
+
expect(metadataIdsOf(trace)).toEqual([
|
|
4371
|
+
kind.components.severityFindMany,
|
|
4372
|
+
]);
|
|
4373
|
+
expect(trace.logs).toEqual([
|
|
4374
|
+
"ℹ️ The event did not name a Jira issue key.",
|
|
4375
|
+
]);
|
|
4376
|
+
},
|
|
4377
|
+
);
|
|
4378
|
+
|
|
4379
|
+
/*
|
|
4380
|
+
* A skip reason is logged by substituting it into a Log step, so it is
|
|
4381
|
+
* held to the same rule as any other text from Jira: here a status name,
|
|
4382
|
+
* which Jira admins choose.
|
|
4383
|
+
*/
|
|
4384
|
+
test("a Jira status named like a reference is quoted inertly in the reason a transition was skipped", async () => {
|
|
4385
|
+
const trace: SimulationTrace = await runTemplate({
|
|
4386
|
+
templateId: kind.templates.transition,
|
|
4387
|
+
trigger: onRecord(kind.model({ state: kind.resolvedState })),
|
|
4388
|
+
jira: jiraRoutes({
|
|
4389
|
+
[SEARCH]: ok(
|
|
4390
|
+
jiraSearchResponse({
|
|
4391
|
+
status: jiraStatus({
|
|
4392
|
+
name: TOKEN_REFERENCE,
|
|
4393
|
+
categoryKey: "indeterminate",
|
|
4394
|
+
id: "10300",
|
|
4395
|
+
}),
|
|
4396
|
+
transitions: [],
|
|
4397
|
+
}),
|
|
4398
|
+
),
|
|
4399
|
+
[TRANSITION]: NO_CONTENT,
|
|
4400
|
+
}),
|
|
4401
|
+
});
|
|
4402
|
+
|
|
4403
|
+
expect(trace.requests).toEqual([
|
|
4404
|
+
searchRequest(kind, ["key", "status"], true),
|
|
4405
|
+
]);
|
|
4406
|
+
expect(trace.logs).toEqual([
|
|
4407
|
+
`ℹ️ Jira issue ${JIRA_ISSUE_KEY} has no transition from { {local.variables.jiraBasicAuthToken} } to a Done status that can be chosen without naming it in STATE_TO_JIRA_STATUS.`,
|
|
4408
|
+
]);
|
|
4409
|
+
expectNothingSubstituted(trace);
|
|
4410
|
+
expectTokenOnlyInAuthorization(trace);
|
|
4411
|
+
});
|
|
4412
|
+
|
|
4413
|
+
// The transition's id is Jira's text too, and it is quoted into the request body.
|
|
4414
|
+
test("a transition id named like a reference is sent as inert text", async () => {
|
|
4415
|
+
const trace: SimulationTrace = await runTemplate({
|
|
4416
|
+
templateId: kind.templates.transition,
|
|
4417
|
+
trigger: onRecord(kind.model({ state: kind.resolvedState })),
|
|
4418
|
+
jira: jiraRoutes({
|
|
4419
|
+
[SEARCH]: ok(
|
|
4420
|
+
jiraSearchResponse({
|
|
4421
|
+
transitions: [
|
|
4422
|
+
jiraTransition({
|
|
4423
|
+
id: TOKEN_REFERENCE,
|
|
4424
|
+
name: "Close",
|
|
4425
|
+
to: DONE,
|
|
4426
|
+
}),
|
|
4427
|
+
],
|
|
4428
|
+
}),
|
|
4429
|
+
),
|
|
4430
|
+
[TRANSITION]: NO_CONTENT,
|
|
4431
|
+
}),
|
|
4432
|
+
});
|
|
4433
|
+
|
|
4434
|
+
expect(trace.requests).toHaveLength(2);
|
|
4435
|
+
expect(trace.requests[1]!.body).toEqual({
|
|
4436
|
+
transition: { id: "{ {local.variables.jiraBasicAuthToken} }" },
|
|
4437
|
+
});
|
|
4438
|
+
expectNothingSubstituted(trace);
|
|
4439
|
+
expectTokenOnlyInAuthorization(trace);
|
|
4440
|
+
});
|
|
4441
|
+
|
|
4442
|
+
test("a changelog naming references is noted as inert text", async () => {
|
|
4443
|
+
const trace: SimulationTrace = await runIssueChanges(kind, {
|
|
4444
|
+
event: jiraIssueUpdatedEvent({
|
|
4445
|
+
labels: linkedLabelsOf(kind),
|
|
4446
|
+
user: jiraUser(HEADERS_REFERENCE),
|
|
4447
|
+
items: [
|
|
4448
|
+
changelogItem({
|
|
4449
|
+
field: "summary",
|
|
4450
|
+
fromString: "Checkout API returns 502",
|
|
4451
|
+
toString: `Leak ${TOKEN_REFERENCE} and {{local.components.${kind.steps.find}.returnValues.model}}`,
|
|
4452
|
+
}),
|
|
4453
|
+
],
|
|
4454
|
+
}),
|
|
4455
|
+
});
|
|
4456
|
+
|
|
4457
|
+
const note: string = jsonOf(trace, kind.components.noteCreateOne)[
|
|
4458
|
+
"note"
|
|
4459
|
+
] as string;
|
|
4460
|
+
|
|
4461
|
+
expect(note).toContain(
|
|
4462
|
+
`- summary: Checkout API returns 502 → Leak { {local.variables.jiraBasicAuthToken} } and { {local.components.${kind.steps.find}.returnValues.model} }`,
|
|
4463
|
+
);
|
|
4464
|
+
expectNothingSubstituted(trace);
|
|
4465
|
+
});
|
|
4466
|
+
|
|
4467
|
+
/*
|
|
4468
|
+
* Jira users choose their own display names, and the status template
|
|
4469
|
+
* quotes the event its first script returns — which carries that name —
|
|
4470
|
+
* into the next script's arguments BEFORE the record and the states. A
|
|
4471
|
+
* name that is a reference to one of those later values would be
|
|
4472
|
+
* substituted in its place, leaving the real reference unresolved. The
|
|
4473
|
+
* helper block's rule — text from the other system is defused before it
|
|
4474
|
+
* is returned — is what is supposed to prevent that.
|
|
4475
|
+
*/
|
|
4476
|
+
test.each([
|
|
4477
|
+
["{{local.components.find-states-1.returnValues.models}}"],
|
|
4478
|
+
[`{{local.components.${kind.steps.find}.returnValues.model}}`],
|
|
4479
|
+
])(
|
|
4480
|
+
`a Jira user named %s still resolves the ${kind.noun}`,
|
|
4481
|
+
async (displayName: string) => {
|
|
4482
|
+
const trace: SimulationTrace = await runTemplate({
|
|
4483
|
+
templateId: kind.templates.statusToState,
|
|
4484
|
+
trigger: webhookDelivery(
|
|
4485
|
+
jiraIssueUpdatedEvent({
|
|
4486
|
+
items: RESOLVED_CHANGELOG,
|
|
4487
|
+
labels: linkedLabelsOf(kind),
|
|
4488
|
+
status: DONE,
|
|
4489
|
+
user: jiraUser(displayName),
|
|
4490
|
+
}),
|
|
4491
|
+
),
|
|
4492
|
+
database: statusDatabase(
|
|
4493
|
+
kind,
|
|
4494
|
+
recordOf(kind, kind.acknowledgedState),
|
|
4495
|
+
),
|
|
4496
|
+
});
|
|
4497
|
+
|
|
4498
|
+
expect(trace.logs).toEqual([
|
|
4499
|
+
`✅ Moved ${kind.number} to Resolved because Jira issue ${JIRA_ISSUE_KEY} is now Done.`,
|
|
4500
|
+
]);
|
|
4501
|
+
expect(jsonOf(trace, kind.components.timelineCreateOne)).toEqual({
|
|
4502
|
+
[kind.idColumn]: kind.id,
|
|
4503
|
+
[kind.timelineStateColumn]: kind.resolvedState["_id"] as string,
|
|
4504
|
+
rootCause: `${ONEUPTIME_SYNCED_FROM_JIRA_MARKER}: issue ${JIRA_ISSUE_KEY} moved to Done by ${defused(displayName)}.`,
|
|
4505
|
+
});
|
|
4506
|
+
expectNothingSubstituted(trace);
|
|
4507
|
+
},
|
|
4508
|
+
);
|
|
4509
|
+
|
|
4510
|
+
/*
|
|
4511
|
+
* The same rule for the other direction: the transition template returns
|
|
4512
|
+
* the name of the status it moves the issue to, and the success log
|
|
4513
|
+
* quotes that name before the script's own reason.
|
|
4514
|
+
*/
|
|
4515
|
+
test("a Jira status named like a reference is logged as text, not substituted", async () => {
|
|
4516
|
+
const trickStatus: JSONObject = jiraStatus({
|
|
4517
|
+
name: "{{local.components.plan-transition-1.returnValues.returnValue.reason}}",
|
|
4518
|
+
categoryKey: "done",
|
|
4519
|
+
id: "10099",
|
|
4520
|
+
});
|
|
4521
|
+
|
|
4522
|
+
const trace: SimulationTrace = await runTemplate({
|
|
4523
|
+
templateId: kind.templates.transition,
|
|
4524
|
+
trigger: onRecord(kind.model({ state: kind.resolvedState })),
|
|
4525
|
+
jira: jiraRoutes({
|
|
4526
|
+
[SEARCH]: ok(
|
|
4527
|
+
jiraSearchResponse({
|
|
4528
|
+
transitions: [
|
|
4529
|
+
jiraTransition({ id: "41", name: "Close", to: trickStatus }),
|
|
4530
|
+
],
|
|
4531
|
+
}),
|
|
4532
|
+
),
|
|
4533
|
+
[TRANSITION]: NO_CONTENT,
|
|
4534
|
+
}),
|
|
4535
|
+
});
|
|
4536
|
+
|
|
4537
|
+
expect(trace.requests[1]!.body).toEqual({ transition: { id: "41" } });
|
|
4538
|
+
expect(trace.logs).toEqual([
|
|
4539
|
+
`✅ Moved Jira issue ${JIRA_ISSUE_KEY} to { {local.components.plan-transition-1.returnValues.returnValue.reason} }. ${kind.number} is Resolved in OneUptime.`,
|
|
4540
|
+
]);
|
|
4541
|
+
});
|
|
4542
|
+
|
|
4543
|
+
test.each(kind.noteTemplates)(
|
|
4544
|
+
"$templateId: OneUptime text naming the token is posted to Jira as inert text",
|
|
4545
|
+
async (noteTemplate: NoteTemplate) => {
|
|
4546
|
+
const trace: SimulationTrace = await runNoteToComment(
|
|
4547
|
+
noteTemplate.templateId,
|
|
4548
|
+
kind.note({ note: `Rotate ${TOKEN_REFERENCE} after this.` }),
|
|
4549
|
+
);
|
|
4550
|
+
|
|
4551
|
+
expect(trace.requests).toHaveLength(2);
|
|
4552
|
+
expect(JSON.stringify(trace.requests[1]!.body)).toContain(
|
|
4553
|
+
"Rotate { {local.variables.jiraBasicAuthToken} } after this.",
|
|
4554
|
+
);
|
|
4555
|
+
expectNothingSubstituted(trace);
|
|
4556
|
+
expectTokenOnlyInAuthorization(trace);
|
|
4557
|
+
},
|
|
4558
|
+
);
|
|
4559
|
+
|
|
4560
|
+
test("an edited title naming the token is posted to Jira as inert text", async () => {
|
|
4561
|
+
const trace: SimulationTrace = await runTemplate({
|
|
4562
|
+
templateId: kind.templates.updateComment,
|
|
4563
|
+
trigger: onRecord(
|
|
4564
|
+
kind.model({ title: `Rotate ${TOKEN_REFERENCE} after this` }),
|
|
4565
|
+
),
|
|
4566
|
+
jira: jiraRoutes({
|
|
4567
|
+
[SEARCH]: ok(jiraSearchResponse()),
|
|
4568
|
+
[COMMENT]: COMMENT_CREATED,
|
|
4569
|
+
}),
|
|
4570
|
+
});
|
|
4571
|
+
|
|
4572
|
+
expect(trace.requests).toHaveLength(2);
|
|
4573
|
+
expect(wikiTextOf(postedCommentOf(trace))).toContain(
|
|
4574
|
+
"Title: Rotate { {local.variables.jiraBasicAuthToken} } after this",
|
|
4575
|
+
);
|
|
4576
|
+
expectNothingSubstituted(trace);
|
|
4577
|
+
expectTokenOnlyInAuthorization(trace);
|
|
4578
|
+
});
|
|
4579
|
+
|
|
4580
|
+
/*
|
|
4581
|
+
* Every path that talks to Jira, run once each: the token must ride in
|
|
4582
|
+
* the Authorization header of every request, as Basic auth, and turn up
|
|
4583
|
+
* nowhere else — including in failure logs that print Jira's response.
|
|
4584
|
+
*/
|
|
4585
|
+
test("the token is in every request's Authorization header and nowhere else in any trace", async () => {
|
|
4586
|
+
const runs: Array<TokenRun> = [
|
|
4587
|
+
{
|
|
4588
|
+
requests: 1,
|
|
4589
|
+
scenario: {
|
|
4590
|
+
templateId: kind.templates.createIssue,
|
|
4591
|
+
trigger: onRecord(kind.model()),
|
|
4592
|
+
jira: jiraRoutes({ [CREATE_ISSUE]: ISSUE_CREATED }),
|
|
4593
|
+
},
|
|
4594
|
+
},
|
|
4595
|
+
{
|
|
4596
|
+
requests: 1,
|
|
4597
|
+
scenario: {
|
|
4598
|
+
templateId: kind.templates.createIssue,
|
|
4599
|
+
trigger: onRecord(kind.model()),
|
|
4600
|
+
jira: jiraRoutes({
|
|
4601
|
+
[CREATE_ISSUE]: badRequest({
|
|
4602
|
+
errorMessages: ["Unauthorized"],
|
|
4603
|
+
errors: {},
|
|
4604
|
+
}),
|
|
4605
|
+
}),
|
|
4606
|
+
},
|
|
4607
|
+
},
|
|
4608
|
+
{
|
|
4609
|
+
requests: 2,
|
|
4610
|
+
scenario: {
|
|
4611
|
+
templateId: kind.templates.transition,
|
|
4612
|
+
trigger: onRecord(kind.model({ state: kind.acknowledgedState })),
|
|
4613
|
+
jira: jiraRoutes({
|
|
4614
|
+
[SEARCH]: ok(jiraSearchResponse()),
|
|
4615
|
+
[TRANSITION]: NO_CONTENT,
|
|
4616
|
+
}),
|
|
4617
|
+
},
|
|
4618
|
+
},
|
|
4619
|
+
{
|
|
4620
|
+
requests: 1,
|
|
4621
|
+
scenario: {
|
|
4622
|
+
templateId: kind.templates.transition,
|
|
4623
|
+
trigger: onRecord(kind.model({ state: kind.acknowledgedState })),
|
|
4624
|
+
jira: jiraRoutes({
|
|
4625
|
+
[SEARCH]: { status: 401, body: "Unauthorized" },
|
|
4626
|
+
}),
|
|
4627
|
+
},
|
|
4628
|
+
},
|
|
4629
|
+
...kind.noteTemplates.flatMap(
|
|
4630
|
+
(noteTemplate: NoteTemplate): Array<TokenRun> => {
|
|
4631
|
+
return [
|
|
4632
|
+
{
|
|
4633
|
+
requests: 2,
|
|
4634
|
+
scenario: {
|
|
4635
|
+
templateId: noteTemplate.templateId,
|
|
4636
|
+
trigger: onRecord(kind.note()),
|
|
4637
|
+
jira: jiraRoutes({
|
|
4638
|
+
[SEARCH]: ok(jiraSearchResponse()),
|
|
4639
|
+
[COMMENT]: COMMENT_CREATED,
|
|
4640
|
+
}),
|
|
4641
|
+
},
|
|
4642
|
+
},
|
|
4643
|
+
{
|
|
4644
|
+
requests: 2,
|
|
4645
|
+
scenario: {
|
|
4646
|
+
templateId: noteTemplate.templateId,
|
|
4647
|
+
trigger: onRecord(kind.note()),
|
|
4648
|
+
jira: jiraRoutes({
|
|
4649
|
+
[SEARCH]: ok(jiraSearchResponse()),
|
|
4650
|
+
[COMMENT]: {
|
|
4651
|
+
status: 403,
|
|
4652
|
+
body: { errorMessages: ["Forbidden"] },
|
|
4653
|
+
},
|
|
4654
|
+
}),
|
|
4655
|
+
},
|
|
4656
|
+
},
|
|
4657
|
+
];
|
|
4658
|
+
},
|
|
4659
|
+
),
|
|
4660
|
+
{
|
|
4661
|
+
requests: 2,
|
|
4662
|
+
scenario: {
|
|
4663
|
+
templateId: kind.templates.updateComment,
|
|
4664
|
+
trigger: onRecord(kind.model()),
|
|
4665
|
+
jira: jiraRoutes({
|
|
4666
|
+
[SEARCH]: ok(jiraSearchResponse()),
|
|
4667
|
+
[COMMENT]: COMMENT_CREATED,
|
|
4668
|
+
}),
|
|
4669
|
+
},
|
|
4670
|
+
},
|
|
4671
|
+
{
|
|
4672
|
+
requests: 2,
|
|
4673
|
+
scenario: {
|
|
4674
|
+
templateId: kind.templates.createFromIssue,
|
|
4675
|
+
trigger: webhookDelivery(jiraIssueCreatedEvent()),
|
|
4676
|
+
jira: createFromIssueJira(),
|
|
4677
|
+
database: createDatabase(kind),
|
|
4678
|
+
},
|
|
4679
|
+
},
|
|
4680
|
+
{
|
|
4681
|
+
requests: 1,
|
|
4682
|
+
scenario: {
|
|
4683
|
+
templateId: kind.templates.createFromIssue,
|
|
4684
|
+
trigger: webhookDelivery(jiraIssueCreatedEvent()),
|
|
4685
|
+
jira: createFromIssueJira({ labels: linkedLabelsOf(kind) }),
|
|
4686
|
+
database: createDatabase(kind),
|
|
4687
|
+
},
|
|
4688
|
+
},
|
|
4689
|
+
{
|
|
4690
|
+
requests: 1,
|
|
4691
|
+
scenario: {
|
|
4692
|
+
templateId: kind.templates.createFromIssue,
|
|
4693
|
+
trigger: webhookDelivery(jiraIssueCreatedEvent()),
|
|
4694
|
+
jira: createFromIssueJira({
|
|
4695
|
+
getIssue: {
|
|
4696
|
+
status: 404,
|
|
4697
|
+
body: { errorMessages: ["Issue does not exist"], errors: {} },
|
|
4698
|
+
},
|
|
4699
|
+
}),
|
|
4700
|
+
database: createDatabase(kind),
|
|
4701
|
+
},
|
|
4702
|
+
},
|
|
4703
|
+
{
|
|
4704
|
+
requests: 2,
|
|
4705
|
+
scenario: {
|
|
4706
|
+
templateId: kind.templates.createFromIssue,
|
|
4707
|
+
trigger: webhookDelivery(jiraIssueCreatedEvent()),
|
|
4708
|
+
jira: createFromIssueJira({
|
|
4709
|
+
edit: badRequest({
|
|
4710
|
+
errorMessages: [],
|
|
4711
|
+
errors: { labels: "No." },
|
|
4712
|
+
}),
|
|
4713
|
+
}),
|
|
4714
|
+
database: createDatabase(kind),
|
|
4715
|
+
},
|
|
4716
|
+
},
|
|
4717
|
+
{
|
|
4718
|
+
requests: 1,
|
|
4719
|
+
scenario: commentScenario(kind, { event: jiraCommentEvent() }),
|
|
4720
|
+
},
|
|
4721
|
+
{
|
|
4722
|
+
requests: 1,
|
|
4723
|
+
scenario: {
|
|
4724
|
+
templateId: kind.templates.commentToNote,
|
|
4725
|
+
trigger: webhookDelivery(jiraCommentEvent()),
|
|
4726
|
+
jira: jiraRoutes({
|
|
4727
|
+
[GET_LABELS]: {
|
|
4728
|
+
status: 404,
|
|
4729
|
+
body: { errorMessages: ["Issue does not exist"] },
|
|
4730
|
+
},
|
|
4731
|
+
}),
|
|
4732
|
+
},
|
|
4733
|
+
},
|
|
4734
|
+
];
|
|
4735
|
+
|
|
4736
|
+
const traces: Array<SimulationTrace> = [];
|
|
4737
|
+
|
|
4738
|
+
for (const run of runs) {
|
|
4739
|
+
const trace: SimulationTrace = await runTemplate(run.scenario);
|
|
4740
|
+
|
|
4741
|
+
// A run that sent fewer requests than it should would test less than it claims.
|
|
4742
|
+
expect(trace.requests).toHaveLength(run.requests);
|
|
4743
|
+
traces.push(trace);
|
|
4744
|
+
}
|
|
4745
|
+
|
|
4746
|
+
// Every template of this kind that holds the token is represented...
|
|
4747
|
+
const covered: Array<string> = traces
|
|
4748
|
+
.map((trace: SimulationTrace) => {
|
|
4749
|
+
return trace.templateId;
|
|
4750
|
+
})
|
|
4751
|
+
.filter((templateId: string, index: number, all: Array<string>) => {
|
|
4752
|
+
return all.indexOf(templateId) === index;
|
|
4753
|
+
})
|
|
4754
|
+
.sort();
|
|
4755
|
+
const holdingTheToken: Array<string> = templateIdsOf(kind)
|
|
4756
|
+
.filter((templateId: string) => {
|
|
4757
|
+
return getWorkflowTemplate(templateId)!.variables.some(
|
|
4758
|
+
(variable: WorkflowTemplateVariable) => {
|
|
4759
|
+
return variable.name === "jiraBasicAuthToken";
|
|
4760
|
+
},
|
|
4761
|
+
);
|
|
4762
|
+
})
|
|
4763
|
+
.sort();
|
|
4764
|
+
|
|
4765
|
+
expect(covered).toEqual(holdingTheToken);
|
|
4766
|
+
|
|
4767
|
+
// ...and so is every Jira endpoint any of them calls, the create-from-Jira template's GET included.
|
|
4768
|
+
const routes: Array<string> = traces
|
|
4769
|
+
.flatMap((trace: SimulationTrace) => {
|
|
4770
|
+
return trace.requests.map((request: JiraRequest) => {
|
|
4771
|
+
return `${request.method} ${request.url.replace(JIRA_SITE, "")}`;
|
|
4772
|
+
});
|
|
4773
|
+
})
|
|
4774
|
+
.filter((route: string, index: number, all: Array<string>) => {
|
|
4775
|
+
return all.indexOf(route) === index;
|
|
4776
|
+
})
|
|
4777
|
+
.sort();
|
|
4778
|
+
|
|
4779
|
+
expect(routes).toEqual(
|
|
4780
|
+
[
|
|
4781
|
+
CREATE_ISSUE,
|
|
4782
|
+
SEARCH,
|
|
4783
|
+
TRANSITION,
|
|
4784
|
+
COMMENT,
|
|
4785
|
+
EDIT_ISSUE,
|
|
4786
|
+
GET_LABELS,
|
|
4787
|
+
].sort(),
|
|
4788
|
+
);
|
|
4789
|
+
|
|
4790
|
+
for (const trace of traces) {
|
|
4791
|
+
expectTokenOnlyInAuthorization(trace);
|
|
4792
|
+
expectNothingSubstituted(trace);
|
|
4793
|
+
|
|
4794
|
+
// A read carries no body for the credentials to be copied into.
|
|
4795
|
+
for (const request of trace.requests) {
|
|
4796
|
+
if (request.method === "GET") {
|
|
4797
|
+
expect(request.body).toBeUndefined();
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
});
|
|
4802
|
+
},
|
|
4803
|
+
);
|