@oneuptime/common 14.0.1 → 14.0.3
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/AlertFeed.ts +2 -0
- package/Models/DatabaseModels/AutoRemediationSuggestion.ts +63 -0
- package/Models/DatabaseModels/IncidentAlert.ts +540 -0
- package/Models/DatabaseModels/IncidentEpisodeMember.ts +8 -1
- package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +9 -2
- package/Models/DatabaseModels/IncidentFeed.ts +2 -0
- package/Models/DatabaseModels/IncidentPublicNote.ts +9 -2
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +429 -0
- package/Models/DatabaseModels/Monitor.ts +48 -0
- package/Models/DatabaseModels/Project.ts +60 -0
- package/Models/DatabaseModels/RumApplication.ts +52 -1
- 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/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -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/IncidentAlertService.ts +1811 -0
- 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 +330 -30
- package/Server/Services/IncidentStateTimelineService.ts +39 -0
- package/Server/Services/IncidentTemplateService.ts +83 -3
- package/Server/Services/Index.ts +2 -0
- 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/SessionReplayGateCache.ts +24 -2
- 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/BulkIncidentLinkActions.test.tsx +869 -0
- package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
- package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
- package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
- package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +794 -0
- package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -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/DiscoveryScanLivePage.test.tsx +39 -0
- package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
- package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
- package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -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 +238 -0
- package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
- package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
- package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
- package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
- package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
- package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
- package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
- package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
- package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
- package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
- package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +665 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -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/IoTMetricTooltips.test.tsx +1123 -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/KubernetesClusterMetricTooltips.test.ts +837 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
- package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
- package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
- package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
- package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
- package/Tests/App/Dashboard/LayerDateTimeFieldTimezoneName.test.tsx +66 -0
- package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
- package/Tests/App/Dashboard/LogMonitorPreview.test.tsx +174 -0
- package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
- package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
- package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -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/NetworkDeviceTooltips.test.tsx +852 -0
- package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
- package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
- package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -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/ProxmoxMetricTooltips.test.ts +592 -0
- package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
- package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -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/ResourceOverviewTileTooltips.test.tsx +1066 -0
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -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/ServerlessMetricTooltips.test.tsx +474 -0
- package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
- package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
- package/Tests/App/Dashboard/TeamMembershipRemoval.test.ts +900 -0
- package/Tests/App/Dashboard/TeamMembershipRemovalPages.test.tsx +460 -0
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
- package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -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/VMwareMetricTooltips.test.ts +801 -0
- package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
- package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
- package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -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/IncidentAlertModel.test.ts +554 -0
- package/Tests/Models/DatabaseModels/KubernetesClusterAiAccessColumns.test.ts +520 -0
- package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +66 -2
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
- 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/AddIncidentAlertMigration.test.ts +341 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +589 -0
- package/Tests/Server/Infrastructure/Postgres/AddOAuth2WorkflowVariablesMigration.test.ts +254 -0
- package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -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/IncidentAlertPostgres.test.ts +903 -0
- package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
- package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
- package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
- package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -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/SessionReplayGateCachePolicy.test.ts +103 -0
- 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/Rum/WebVitals.test.ts +173 -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/FieldLabelInfoCardTooltip.test.tsx +413 -0
- 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/InfoTooltip.test.tsx +242 -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/BaseModelTableBulkDelete.test.tsx +207 -3
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
- 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/TableHeaderTooltip.test.tsx +662 -0
- 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/Rum/PrivacySummaryCard.test.tsx +166 -9
- package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
- package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
- package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
- package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
- package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
- package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
- package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
- package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
- package/Tests/UI/Telemetry/TelemetryDetailPanel.test.tsx +28 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/PermissionGate.test.ts +76 -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/Incident/IncidentFromAlerts.test.ts +696 -0
- 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/SessionTraceState.test.ts +239 -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/Incident/IncidentAlertLink.ts +38 -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/Permission.ts +48 -0
- package/Types/Rum/SessionReplay.ts +80 -8
- package/Types/Rum/SessionReplayApi.ts +44 -1
- package/Types/Rum/UserFlow.ts +187 -0
- package/Types/Rum/WebVitals.ts +52 -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/Detail/FieldLabel.tsx +8 -0
- 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/InfoCard/InfoCard.tsx +47 -1
- 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 +161 -31
- package/UI/Components/ModelTable/Column.ts +5 -0
- 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/Table/TableHeader.tsx +47 -2
- package/UI/Components/Table/Types/Column.ts +7 -0
- 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/Components/Tooltip/InfoTooltip.tsx +78 -0
- package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +19 -1
- package/UI/Utils/PermissionGate.ts +11 -2
- 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/Incident/IncidentFromAlerts.ts +516 -0
- 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/SessionTraceState.ts +212 -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/AlertFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.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/IncidentAlert.js +553 -0
- package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
- 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/IncidentFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.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/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.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/Project.js +62 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
- package/build/dist/Models/DatabaseModels/RumApplication.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/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -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/IncidentAlertService.js +1370 -0
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
- 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 +263 -33
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +33 -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/Index.js +2 -0
- package/build/dist/Server/Services/Index.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/SessionReplayGateCache.js +8 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.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/Incident/IncidentAlertLink.js +34 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
- 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/Permission.js +42 -0
- package/build/dist/Types/Permission.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/Rum/WebVitals.js +25 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- 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/Detail/FieldLabel.js +2 -0
- package/build/dist/UI/Components/Detail/FieldLabel.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/InfoCard/InfoCard.js +22 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.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 +89 -22
- 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/Table/TableHeader.js +18 -2
- package/build/dist/UI/Components/Table/TableHeader.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/Components/Tooltip/InfoTooltip.js +28 -0
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
- 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/PermissionGate.js +4 -2
- package/build/dist/UI/Utils/PermissionGate.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/Incident/IncidentFromAlerts.js +306 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
- 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/SessionTraceState.js +154 -0
- package/build/dist/Utils/Rum/SessionTraceState.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,3690 @@
|
|
|
1
|
+
import WorkspaceNotificationRuleService from "../../../Server/Services/WorkspaceNotificationRuleService";
|
|
2
|
+
import WorkspaceProjectAuthTokenService from "../../../Server/Services/WorkspaceProjectAuthTokenService";
|
|
3
|
+
import WorkspaceNotificationLogService from "../../../Server/Services/WorkspaceNotificationLogService";
|
|
4
|
+
import ProjectService from "../../../Server/Services/ProjectService";
|
|
5
|
+
import UserService from "../../../Server/Services/UserService";
|
|
6
|
+
import WorkspaceUtil from "../../../Server/Utils/Workspace/Workspace";
|
|
7
|
+
import MicrosoftTeamsUtil from "../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
|
|
8
|
+
import logger from "../../../Server/Utils/Logger";
|
|
9
|
+
import {
|
|
10
|
+
WorkspaceChannel,
|
|
11
|
+
WorkspaceSendMessageResponse,
|
|
12
|
+
WorkspaceThread,
|
|
13
|
+
} from "../../../Server/Utils/Workspace/WorkspaceBase";
|
|
14
|
+
import WorkspaceProjectAuthToken, {
|
|
15
|
+
MicrosoftTeamsChat,
|
|
16
|
+
MicrosoftTeamsMiscData,
|
|
17
|
+
SlackMiscData,
|
|
18
|
+
} from "../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
|
|
19
|
+
import WorkspaceNotificationLog from "../../../Models/DatabaseModels/WorkspaceNotificationLog";
|
|
20
|
+
import Project from "../../../Models/DatabaseModels/Project";
|
|
21
|
+
import WorkspaceType from "../../../Types/Workspace/WorkspaceType";
|
|
22
|
+
import WorkspaceNotificationStatus from "../../../Types/Workspace/WorkspaceNotificationStatus";
|
|
23
|
+
import WorkspaceNotificationActionType from "../../../Types/Workspace/WorkspaceNotificationActionType";
|
|
24
|
+
import WorkspaceMessagePayload, {
|
|
25
|
+
WorkspacePayloadMarkdown,
|
|
26
|
+
} from "../../../Types/Workspace/WorkspaceMessagePayload";
|
|
27
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
28
|
+
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
|
29
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
30
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
31
|
+
import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* Tests for WorkspaceNotificationRuleService.sendTestNotificationToDestination,
|
|
35
|
+
* the service behind the "Send Test" button beside every Slack channel,
|
|
36
|
+
* Microsoft Teams channel and Microsoft Teams chat in Project Settings.
|
|
37
|
+
*
|
|
38
|
+
* What these pin down:
|
|
39
|
+
*
|
|
40
|
+
* - The send goes through the SAME bot delivery path real notifications use
|
|
41
|
+
* (WorkspaceUtil.postMessageToAllWorkspaceChannelsAsBot), with exactly one
|
|
42
|
+
* payload addressed to exactly one destination.
|
|
43
|
+
* - Every malformed request is rejected with a user-facing BadDataException
|
|
44
|
+
* BEFORE anything is loaded, sent or logged. Microsoft Teams team / channel
|
|
45
|
+
* ids are interpolated unencoded into Microsoft Graph URL paths downstream,
|
|
46
|
+
* so path separators, query / fragment starters, percent escapes, whitespace,
|
|
47
|
+
* control characters and dot-only segments ("." / "..") are a security
|
|
48
|
+
* boundary, not just hygiene.
|
|
49
|
+
* - The SaaS bot is shared across tenants, so a project may only post to
|
|
50
|
+
* destinations proven to be its own: the Teams chats it captured itself,
|
|
51
|
+
* and Teams channels listed for the requested team with the project's own
|
|
52
|
+
* app token (MicrosoftTeamsUtil.getAllWorkspaceChannels). A channel that
|
|
53
|
+
* listing cannot vouch for - or a listing that fails - sends nothing and
|
|
54
|
+
* logs nothing.
|
|
55
|
+
* - Whatever a provider throws - an Error, an HTTPErrorResponse (not an
|
|
56
|
+
* Error), a string, null - becomes a readable message, never
|
|
57
|
+
* "[object Object]".
|
|
58
|
+
* - A "success" that sent nothing is reported as a failure, never as success.
|
|
59
|
+
* - Every outcome is recorded in Notification Logs, and the best-effort parts
|
|
60
|
+
* (project name, user name, success-log write) never block or fail a send.
|
|
61
|
+
*
|
|
62
|
+
* Nothing here touches a database or the network: every collaborator is a
|
|
63
|
+
* jest.spyOn on the service singletons, restored after every test.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
const PROJECT_ID: ObjectID = ObjectID.generate();
|
|
67
|
+
const TEST_BY_USER_ID: ObjectID = ObjectID.generate();
|
|
68
|
+
|
|
69
|
+
const SLACK_CHANNEL_ID: string = "C0123ABCD";
|
|
70
|
+
const SLACK_CHANNEL_NAME: string = "alerts";
|
|
71
|
+
|
|
72
|
+
const TEAMS_TEAM_ID: string = "aaaaaaaa-1111-2222-3333-444444444444";
|
|
73
|
+
const TEAMS_CHANNEL_ID: string = "19:general-channel@thread.tacv2";
|
|
74
|
+
const TEAMS_CHANNEL_NAME: string = "General";
|
|
75
|
+
const TEAMS_APP_TOKEN: string = "teams-app-token";
|
|
76
|
+
|
|
77
|
+
const TEAMS_CHAT_ID: string = "19:chat-7f3a91@thread.v2";
|
|
78
|
+
const TEAMS_CHAT_NAME: string = "Ops War Room";
|
|
79
|
+
|
|
80
|
+
const PROJECT_NAME: string = "Acme Production";
|
|
81
|
+
const USER_MARKDOWN: string =
|
|
82
|
+
"[Alice Admin](https://oneuptime.example.com/dashboard/p1/settings/users/u1)";
|
|
83
|
+
|
|
84
|
+
const SUCCESS_STATUS_MESSAGE: string =
|
|
85
|
+
"Test notification sent from Project Settings";
|
|
86
|
+
|
|
87
|
+
const MSG_UNSUPPORTED_WORKSPACE_TYPE_PATTERN: RegExp =
|
|
88
|
+
/Slack.*Microsoft Teams|Microsoft Teams.*Slack/;
|
|
89
|
+
const MSG_NO_DESTINATION: string =
|
|
90
|
+
"Please choose a channel or a chat to send the test notification to.";
|
|
91
|
+
const MSG_BOTH_DESTINATIONS: string =
|
|
92
|
+
"Please choose either a channel or a chat, not both.";
|
|
93
|
+
const MSG_CHAT_WITH_SLACK: string =
|
|
94
|
+
"Chats are only supported for Microsoft Teams. Please choose a Slack channel.";
|
|
95
|
+
const MSG_INVALID_SLACK_CHANNEL: string = "The Slack channel id is not valid.";
|
|
96
|
+
const MSG_TEAM_REQUIRED: string =
|
|
97
|
+
"Please select the team this channel belongs to.";
|
|
98
|
+
const MSG_INVALID_TEAMS_TEAM: string =
|
|
99
|
+
"The Microsoft Teams team id is not valid.";
|
|
100
|
+
const MSG_INVALID_TEAMS_CHANNEL: string =
|
|
101
|
+
"The Microsoft Teams channel id is not valid.";
|
|
102
|
+
const MSG_INVALID_TEAMS_CHAT: string =
|
|
103
|
+
"The Microsoft Teams chat id is not valid.";
|
|
104
|
+
const MSG_CHAT_NOT_CONNECTED: string =
|
|
105
|
+
"This chat is no longer connected to OneUptime. Add the OneUptime app to the chat in Microsoft Teams, click Refresh Chats, and try again.";
|
|
106
|
+
const MSG_CHANNEL_NOT_IN_TEAM: string =
|
|
107
|
+
"This channel was not found in the selected team. It may have been renamed or deleted, or it may be a shared channel, which Microsoft Teams does not let bots post in. Click Refresh Channels and try again.";
|
|
108
|
+
const MSG_CHANNEL_LISTING_FAILED_PREFIX: string =
|
|
109
|
+
"Could not load the channels of the selected team from Microsoft Teams.";
|
|
110
|
+
const SEND_FAILED_PREFIX: string = "Could not send the test notification.";
|
|
111
|
+
// What WorkspaceBase.getSendErrorMessage reports for a throw with nothing to say.
|
|
112
|
+
const UNKNOWN_ERROR_REASON: string = "Unknown error";
|
|
113
|
+
|
|
114
|
+
function notConnectedMessage(displayName: string): string {
|
|
115
|
+
return `This project is not connected to ${displayName}. Please go to Project Settings and connect ${displayName} first.`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function reconnectMessage(data: {
|
|
119
|
+
destinationLabel: string;
|
|
120
|
+
displayName: string;
|
|
121
|
+
}): string {
|
|
122
|
+
return `OneUptime could not send the test notification to ${data.destinationLabel}. Please reconnect ${data.displayName} from Project Settings and try again.`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------- fixtures
|
|
126
|
+
|
|
127
|
+
interface ThreadSpec {
|
|
128
|
+
id: string;
|
|
129
|
+
name: string;
|
|
130
|
+
threadId: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface ErrorSpec {
|
|
134
|
+
id: string;
|
|
135
|
+
name: string;
|
|
136
|
+
error: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function makeSendResponse(data: {
|
|
140
|
+
workspaceType: WorkspaceType;
|
|
141
|
+
threads?: Array<ThreadSpec> | undefined;
|
|
142
|
+
errors?: Array<ErrorSpec> | undefined;
|
|
143
|
+
}): WorkspaceSendMessageResponse {
|
|
144
|
+
const response: WorkspaceSendMessageResponse = {
|
|
145
|
+
workspaceType: data.workspaceType,
|
|
146
|
+
threads: (data.threads || []).map((thread: ThreadSpec) => {
|
|
147
|
+
return {
|
|
148
|
+
channel: {
|
|
149
|
+
id: thread.id,
|
|
150
|
+
name: thread.name,
|
|
151
|
+
workspaceType: data.workspaceType,
|
|
152
|
+
} as WorkspaceChannel,
|
|
153
|
+
threadId: thread.threadId,
|
|
154
|
+
};
|
|
155
|
+
}),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (data.errors) {
|
|
159
|
+
response.errors = data.errors.map((error: ErrorSpec) => {
|
|
160
|
+
return {
|
|
161
|
+
channel: {
|
|
162
|
+
id: error.id,
|
|
163
|
+
name: error.name,
|
|
164
|
+
workspaceType: data.workspaceType,
|
|
165
|
+
} as WorkspaceChannel,
|
|
166
|
+
error: error.error,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return response;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function makeChat(data: {
|
|
175
|
+
id: string;
|
|
176
|
+
name: string;
|
|
177
|
+
chatType?: "personal" | "groupChat" | undefined;
|
|
178
|
+
}): MicrosoftTeamsChat {
|
|
179
|
+
return {
|
|
180
|
+
id: data.id,
|
|
181
|
+
name: data.name,
|
|
182
|
+
chatType: data.chatType || "groupChat",
|
|
183
|
+
serviceUrl: "https://smba.trafficmanager.net/amer/",
|
|
184
|
+
addedAt: "2026-09-01T00:00:00.000Z",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function defaultAvailableChats(): Record<string, MicrosoftTeamsChat> {
|
|
189
|
+
return {
|
|
190
|
+
[TEAMS_CHAT_ID]: makeChat({ id: TEAMS_CHAT_ID, name: TEAMS_CHAT_NAME }),
|
|
191
|
+
"19:personal-bob@unq.gbl.spaces": makeChat({
|
|
192
|
+
id: "19:personal-bob@unq.gbl.spaces",
|
|
193
|
+
name: "Bob Builder",
|
|
194
|
+
chatType: "personal",
|
|
195
|
+
}),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// A channel as MicrosoftTeamsUtil.getAllWorkspaceChannels lists it.
|
|
200
|
+
function makeTeamChannel(data: {
|
|
201
|
+
id: string;
|
|
202
|
+
name: string;
|
|
203
|
+
teamId?: string | undefined;
|
|
204
|
+
}): WorkspaceChannel {
|
|
205
|
+
return {
|
|
206
|
+
id: data.id,
|
|
207
|
+
name: data.name,
|
|
208
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
209
|
+
teamId: data.teamId || TEAMS_TEAM_ID,
|
|
210
|
+
membershipType: "standard",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// One team's channel listing, keyed by channel id like the real one.
|
|
215
|
+
function teamListing(
|
|
216
|
+
...channels: Array<WorkspaceChannel>
|
|
217
|
+
): Dictionary<WorkspaceChannel> {
|
|
218
|
+
const listing: Dictionary<WorkspaceChannel> = {};
|
|
219
|
+
|
|
220
|
+
for (const channel of channels) {
|
|
221
|
+
listing[channel.id] = channel;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return listing;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
* The channel listing of every team in this project's tenant, keyed by team
|
|
229
|
+
* id: by default one team holding the default channel and one other.
|
|
230
|
+
*/
|
|
231
|
+
function defaultTeamChannelsByTeamId(): Dictionary<
|
|
232
|
+
Dictionary<WorkspaceChannel>
|
|
233
|
+
> {
|
|
234
|
+
return {
|
|
235
|
+
[TEAMS_TEAM_ID]: teamListing(
|
|
236
|
+
makeTeamChannel({ id: TEAMS_CHANNEL_ID, name: TEAMS_CHANNEL_NAME }),
|
|
237
|
+
makeTeamChannel({
|
|
238
|
+
id: "19:engineering-channel@thread.tacv2",
|
|
239
|
+
name: "Engineering",
|
|
240
|
+
}),
|
|
241
|
+
),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Microsoft Graph's answer for a team the token cannot see.
|
|
246
|
+
function graphTeamNotFound(): HTTPErrorResponse {
|
|
247
|
+
return new HTTPErrorResponse(
|
|
248
|
+
404,
|
|
249
|
+
{
|
|
250
|
+
error: {
|
|
251
|
+
code: "NotFound",
|
|
252
|
+
message: "No team found with Group Id in the tenant.",
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{},
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function makeProjectAuth(data: {
|
|
260
|
+
workspaceType: WorkspaceType;
|
|
261
|
+
authToken?: string | undefined;
|
|
262
|
+
availableChats?: Record<string, MicrosoftTeamsChat> | null | undefined;
|
|
263
|
+
}): WorkspaceProjectAuthToken {
|
|
264
|
+
const auth: WorkspaceProjectAuthToken = new WorkspaceProjectAuthToken();
|
|
265
|
+
auth.projectId = PROJECT_ID;
|
|
266
|
+
auth.workspaceType = data.workspaceType;
|
|
267
|
+
|
|
268
|
+
if (data.authToken !== undefined) {
|
|
269
|
+
auth.authToken = data.authToken;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (data.workspaceType === WorkspaceType.Slack) {
|
|
273
|
+
auth.miscData = {
|
|
274
|
+
teamId: "T0SLACKTEAM",
|
|
275
|
+
teamName: "Acme Slack",
|
|
276
|
+
botUserId: "U0BOTUSER",
|
|
277
|
+
} as SlackMiscData;
|
|
278
|
+
} else if (data.availableChats !== null) {
|
|
279
|
+
auth.miscData = {
|
|
280
|
+
tenantId: "tenant-1",
|
|
281
|
+
teamId: TEAMS_TEAM_ID,
|
|
282
|
+
teamName: "Acme Team",
|
|
283
|
+
botId: "bot-1",
|
|
284
|
+
availableChats:
|
|
285
|
+
data.availableChats === undefined
|
|
286
|
+
? defaultAvailableChats()
|
|
287
|
+
: data.availableChats,
|
|
288
|
+
} as MicrosoftTeamsMiscData;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return auth;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ------------------------------------------------------------------ mocks
|
|
295
|
+
|
|
296
|
+
type Connection = "connected" | "not-connected" | "no-auth-token";
|
|
297
|
+
|
|
298
|
+
interface MockOptions {
|
|
299
|
+
workspaceType: WorkspaceType;
|
|
300
|
+
connection?: Connection | undefined;
|
|
301
|
+
// null: the Teams auth record has no miscData at all.
|
|
302
|
+
availableChats?: Record<string, MicrosoftTeamsChat> | null | undefined;
|
|
303
|
+
/*
|
|
304
|
+
* What MicrosoftTeamsUtil.getAllWorkspaceChannels lists, per team id. A
|
|
305
|
+
* team missing from it is refused the way Graph refuses a team outside the
|
|
306
|
+
* token's tenant. Defaults to defaultTeamChannelsByTeamId().
|
|
307
|
+
*/
|
|
308
|
+
teamChannelsByTeamId?: Dictionary<Dictionary<WorkspaceChannel>> | undefined;
|
|
309
|
+
listChannelsRejects?: { error: unknown } | undefined;
|
|
310
|
+
responses?: Array<WorkspaceSendMessageResponse> | undefined;
|
|
311
|
+
postRejects?: { error: unknown } | undefined;
|
|
312
|
+
// null: ProjectService.findOneById resolves null.
|
|
313
|
+
projectName?: string | null | undefined;
|
|
314
|
+
projectLookupFails?: boolean | undefined;
|
|
315
|
+
userMarkdown?: string | undefined;
|
|
316
|
+
userLookupFails?: boolean | undefined;
|
|
317
|
+
logWriteFails?: boolean | undefined;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
interface Mocks {
|
|
321
|
+
getProjectAuthSpy: jest.SpyInstance;
|
|
322
|
+
connectedChatsSpy: jest.SpyInstance;
|
|
323
|
+
listTeamChannelsSpy: jest.SpyInstance;
|
|
324
|
+
findProjectSpy: jest.SpyInstance;
|
|
325
|
+
userMarkdownSpy: jest.SpyInstance;
|
|
326
|
+
postSpy: jest.SpyInstance;
|
|
327
|
+
createLogSpy: jest.SpyInstance;
|
|
328
|
+
loggerErrorSpy: jest.SpyInstance;
|
|
329
|
+
// testRule machinery that must never be reached from a destination test.
|
|
330
|
+
testRuleSpy: jest.SpyInstance;
|
|
331
|
+
ruleFindOneByIdSpy: jest.SpyInstance;
|
|
332
|
+
createChannelsSpy: jest.SpyInstance;
|
|
333
|
+
inviteUsersSpy: jest.SpyInstance;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function defaultThreadFor(workspaceType: WorkspaceType): ThreadSpec {
|
|
337
|
+
if (workspaceType === WorkspaceType.Slack) {
|
|
338
|
+
return {
|
|
339
|
+
id: SLACK_CHANNEL_ID,
|
|
340
|
+
name: SLACK_CHANNEL_NAME,
|
|
341
|
+
threadId: "1726000000.000100",
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
id: TEAMS_CHANNEL_ID,
|
|
347
|
+
name: TEAMS_CHANNEL_NAME,
|
|
348
|
+
threadId: "1726000000001",
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function mockDeps(options: MockOptions): Mocks {
|
|
353
|
+
const connection: Connection = options.connection || "connected";
|
|
354
|
+
|
|
355
|
+
let projectAuth: WorkspaceProjectAuthToken | null = null;
|
|
356
|
+
|
|
357
|
+
if (connection === "connected") {
|
|
358
|
+
projectAuth = makeProjectAuth({
|
|
359
|
+
workspaceType: options.workspaceType,
|
|
360
|
+
authToken:
|
|
361
|
+
options.workspaceType === WorkspaceType.Slack
|
|
362
|
+
? "xoxb-test-token"
|
|
363
|
+
: TEAMS_APP_TOKEN,
|
|
364
|
+
availableChats: options.availableChats,
|
|
365
|
+
});
|
|
366
|
+
} else if (connection === "no-auth-token") {
|
|
367
|
+
projectAuth = makeProjectAuth({
|
|
368
|
+
workspaceType: options.workspaceType,
|
|
369
|
+
availableChats: options.availableChats,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const getProjectAuthSpy: jest.SpyInstance = jest
|
|
374
|
+
.spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
|
|
375
|
+
.mockResolvedValue(projectAuth);
|
|
376
|
+
|
|
377
|
+
// Calls through, so the real miscData.availableChats lookup is exercised.
|
|
378
|
+
const connectedChatsSpy: jest.SpyInstance = jest.spyOn(
|
|
379
|
+
WorkspaceNotificationRuleService,
|
|
380
|
+
"getConnectedMicrosoftTeamsChats",
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Always stubbed: the real listing would call Microsoft Graph.
|
|
384
|
+
const listTeamChannelsSpy: jest.SpyInstance = jest.spyOn(
|
|
385
|
+
MicrosoftTeamsUtil,
|
|
386
|
+
"getAllWorkspaceChannels",
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
if (options.listChannelsRejects) {
|
|
390
|
+
listTeamChannelsSpy.mockRejectedValue(options.listChannelsRejects.error);
|
|
391
|
+
} else {
|
|
392
|
+
const teamChannelsByTeamId: Dictionary<Dictionary<WorkspaceChannel>> =
|
|
393
|
+
options.teamChannelsByTeamId || defaultTeamChannelsByTeamId();
|
|
394
|
+
|
|
395
|
+
listTeamChannelsSpy.mockImplementation(
|
|
396
|
+
async (data: {
|
|
397
|
+
authToken: string;
|
|
398
|
+
projectId: ObjectID;
|
|
399
|
+
teamId: string;
|
|
400
|
+
}): Promise<Dictionary<WorkspaceChannel>> => {
|
|
401
|
+
const listing: Dictionary<WorkspaceChannel> | undefined =
|
|
402
|
+
Object.prototype.hasOwnProperty.call(
|
|
403
|
+
teamChannelsByTeamId,
|
|
404
|
+
data.teamId,
|
|
405
|
+
)
|
|
406
|
+
? teamChannelsByTeamId[data.teamId]
|
|
407
|
+
: undefined;
|
|
408
|
+
|
|
409
|
+
if (!listing) {
|
|
410
|
+
throw graphTeamNotFound();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return listing;
|
|
414
|
+
},
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const findProjectSpy: jest.SpyInstance = jest.spyOn(
|
|
419
|
+
ProjectService,
|
|
420
|
+
"findOneById",
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
if (options.projectLookupFails) {
|
|
424
|
+
findProjectSpy.mockRejectedValue(new Error("database is unavailable"));
|
|
425
|
+
} else if (options.projectName === null) {
|
|
426
|
+
findProjectSpy.mockResolvedValue(null);
|
|
427
|
+
} else {
|
|
428
|
+
const project: Project = new Project();
|
|
429
|
+
project.name =
|
|
430
|
+
options.projectName === undefined ? PROJECT_NAME : options.projectName;
|
|
431
|
+
findProjectSpy.mockResolvedValue(project);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const userMarkdownSpy: jest.SpyInstance = jest.spyOn(
|
|
435
|
+
UserService,
|
|
436
|
+
"getUserMarkdownString",
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
if (options.userLookupFails) {
|
|
440
|
+
userMarkdownSpy.mockRejectedValue(new Error("user lookup exploded"));
|
|
441
|
+
} else {
|
|
442
|
+
userMarkdownSpy.mockResolvedValue(
|
|
443
|
+
options.userMarkdown === undefined ? USER_MARKDOWN : options.userMarkdown,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const postSpy: jest.SpyInstance = jest.spyOn(
|
|
448
|
+
WorkspaceUtil,
|
|
449
|
+
"postMessageToAllWorkspaceChannelsAsBot",
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
if (options.postRejects) {
|
|
453
|
+
postSpy.mockRejectedValue(options.postRejects.error);
|
|
454
|
+
} else {
|
|
455
|
+
postSpy.mockResolvedValue(
|
|
456
|
+
options.responses || [
|
|
457
|
+
makeSendResponse({
|
|
458
|
+
workspaceType: options.workspaceType,
|
|
459
|
+
threads: [defaultThreadFor(options.workspaceType)],
|
|
460
|
+
}),
|
|
461
|
+
],
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const createLogSpy: jest.SpyInstance = jest.spyOn(
|
|
466
|
+
WorkspaceNotificationLogService,
|
|
467
|
+
"create",
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
if (options.logWriteFails) {
|
|
471
|
+
createLogSpy.mockRejectedValue(new Error("notification log is read-only"));
|
|
472
|
+
} else {
|
|
473
|
+
createLogSpy.mockResolvedValue(new WorkspaceNotificationLog());
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const loggerErrorSpy: jest.SpyInstance = jest
|
|
477
|
+
.spyOn(logger, "error")
|
|
478
|
+
.mockImplementation((): void => {
|
|
479
|
+
return undefined;
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const testRuleSpy: jest.SpyInstance = jest
|
|
483
|
+
.spyOn(WorkspaceNotificationRuleService, "testRule")
|
|
484
|
+
.mockResolvedValue(undefined);
|
|
485
|
+
|
|
486
|
+
const ruleFindOneByIdSpy: jest.SpyInstance = jest
|
|
487
|
+
.spyOn(WorkspaceNotificationRuleService, "findOneById")
|
|
488
|
+
.mockResolvedValue(null);
|
|
489
|
+
|
|
490
|
+
const createChannelsSpy: jest.SpyInstance = jest
|
|
491
|
+
.spyOn(WorkspaceNotificationRuleService, "createChannelsBasedOnRules")
|
|
492
|
+
.mockResolvedValue([]);
|
|
493
|
+
|
|
494
|
+
const inviteUsersSpy: jest.SpyInstance = jest
|
|
495
|
+
.spyOn(
|
|
496
|
+
WorkspaceNotificationRuleService,
|
|
497
|
+
"inviteUsersBasedOnRulesAndWorkspaceChannels",
|
|
498
|
+
)
|
|
499
|
+
.mockResolvedValue(undefined as never);
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
getProjectAuthSpy,
|
|
503
|
+
connectedChatsSpy,
|
|
504
|
+
listTeamChannelsSpy,
|
|
505
|
+
findProjectSpy,
|
|
506
|
+
userMarkdownSpy,
|
|
507
|
+
postSpy,
|
|
508
|
+
createLogSpy,
|
|
509
|
+
loggerErrorSpy,
|
|
510
|
+
testRuleSpy,
|
|
511
|
+
ruleFindOneByIdSpy,
|
|
512
|
+
createChannelsSpy,
|
|
513
|
+
inviteUsersSpy,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ---------------------------------------------------------------- helpers
|
|
518
|
+
|
|
519
|
+
interface DestinationArgs {
|
|
520
|
+
workspaceType: WorkspaceType;
|
|
521
|
+
channelId?: string | undefined;
|
|
522
|
+
teamId?: string | undefined;
|
|
523
|
+
chatId?: string | undefined;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function sendTest(args: DestinationArgs): Promise<WorkspaceThread> {
|
|
527
|
+
return WorkspaceNotificationRuleService.sendTestNotificationToDestination({
|
|
528
|
+
projectId: PROJECT_ID,
|
|
529
|
+
testByUserId: TEST_BY_USER_ID,
|
|
530
|
+
...args,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function captureError(promise: Promise<unknown>): Promise<Error> {
|
|
535
|
+
let resolvedWith: unknown = undefined;
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
resolvedWith = await promise;
|
|
539
|
+
} catch (err) {
|
|
540
|
+
return err as Error;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
throw new Error(
|
|
544
|
+
`Expected the test send to fail, but it resolved with ${JSON.stringify(resolvedWith)}`,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function expectBadData(
|
|
549
|
+
promise: Promise<unknown>,
|
|
550
|
+
message: string | RegExp,
|
|
551
|
+
): Promise<Error> {
|
|
552
|
+
const err: Error = await captureError(promise);
|
|
553
|
+
|
|
554
|
+
expect(err).toBeInstanceOf(BadDataException);
|
|
555
|
+
|
|
556
|
+
if (typeof message === "string") {
|
|
557
|
+
expect(err.message).toBe(message);
|
|
558
|
+
} else {
|
|
559
|
+
expect(err.message).toMatch(message);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return err;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function expectNothingSentOrLogged(mocks: Mocks): void {
|
|
566
|
+
expect(mocks.postSpy).not.toHaveBeenCalled();
|
|
567
|
+
expect(mocks.createLogSpy).not.toHaveBeenCalled();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/*
|
|
571
|
+
* Validation happens before step 3 (loading the project auth), so a rejected
|
|
572
|
+
* request must not have touched credentials, the chat list or the lookups
|
|
573
|
+
* used to build the message either.
|
|
574
|
+
*/
|
|
575
|
+
function expectRejectedBeforeAnyWork(mocks: Mocks): void {
|
|
576
|
+
expectNothingSentOrLogged(mocks);
|
|
577
|
+
expect(mocks.getProjectAuthSpy).not.toHaveBeenCalled();
|
|
578
|
+
expect(mocks.connectedChatsSpy).not.toHaveBeenCalled();
|
|
579
|
+
expect(mocks.listTeamChannelsSpy).not.toHaveBeenCalled();
|
|
580
|
+
expect(mocks.findProjectSpy).not.toHaveBeenCalled();
|
|
581
|
+
expect(mocks.userMarkdownSpy).not.toHaveBeenCalled();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/*
|
|
585
|
+
* The team's channels were listed exactly once, with THIS project's own app
|
|
586
|
+
* token and project id, for the requested (trimmed) team - never for any
|
|
587
|
+
* other team.
|
|
588
|
+
*/
|
|
589
|
+
function expectTeamListedOnce(mocks: Mocks, teamId: string): void {
|
|
590
|
+
expect(mocks.listTeamChannelsSpy).toHaveBeenCalledTimes(1);
|
|
591
|
+
expect(mocks.listTeamChannelsSpy).toHaveBeenCalledWith({
|
|
592
|
+
authToken: TEAMS_APP_TOKEN,
|
|
593
|
+
projectId: PROJECT_ID,
|
|
594
|
+
teamId: teamId,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/*
|
|
599
|
+
* A Teams channel the tenant check refused: the listing ran, but no message
|
|
600
|
+
* was built (so no project / user lookups), nothing was sent and nothing was
|
|
601
|
+
* logged.
|
|
602
|
+
*/
|
|
603
|
+
function expectRefusedByTenantCheck(mocks: Mocks): void {
|
|
604
|
+
expectNothingSentOrLogged(mocks);
|
|
605
|
+
expect(mocks.findProjectSpy).not.toHaveBeenCalled();
|
|
606
|
+
expect(mocks.userMarkdownSpy).not.toHaveBeenCalled();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// The single payload of the single send, after asserting there was exactly one.
|
|
610
|
+
function onlyPayload(mocks: Mocks): WorkspaceMessagePayload {
|
|
611
|
+
expect(mocks.postSpy).toHaveBeenCalledTimes(1);
|
|
612
|
+
|
|
613
|
+
const args: {
|
|
614
|
+
projectId: ObjectID;
|
|
615
|
+
messagePayloadsByWorkspace: Array<WorkspaceMessagePayload>;
|
|
616
|
+
} = mocks.postSpy.mock.calls[0]![0] as {
|
|
617
|
+
projectId: ObjectID;
|
|
618
|
+
messagePayloadsByWorkspace: Array<WorkspaceMessagePayload>;
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
expect(args.projectId.toString()).toBe(PROJECT_ID.toString());
|
|
622
|
+
expect(args.messagePayloadsByWorkspace).toHaveLength(1);
|
|
623
|
+
|
|
624
|
+
return args.messagePayloadsByWorkspace[0]!;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// The markdown text of the payload, after asserting it is one markdown block.
|
|
628
|
+
function markdownOf(payload: WorkspaceMessagePayload): string {
|
|
629
|
+
expect(payload.messageBlocks).toHaveLength(1);
|
|
630
|
+
|
|
631
|
+
const block: WorkspacePayloadMarkdown = payload
|
|
632
|
+
.messageBlocks[0] as WorkspacePayloadMarkdown;
|
|
633
|
+
|
|
634
|
+
expect(block._type).toBe("WorkspacePayloadMarkdown");
|
|
635
|
+
expect(typeof block.text).toBe("string");
|
|
636
|
+
|
|
637
|
+
return block.text;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function loggedEntries(mocks: Mocks): Array<WorkspaceNotificationLog> {
|
|
641
|
+
return mocks.createLogSpy.mock.calls.map((call: Array<unknown>) => {
|
|
642
|
+
return (call[0] as { data: WorkspaceNotificationLog }).data;
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function loggedProps(mocks: Mocks, index: number): unknown {
|
|
647
|
+
return (mocks.createLogSpy.mock.calls[index]![0] as { props: unknown }).props;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function successLogs(mocks: Mocks): Array<WorkspaceNotificationLog> {
|
|
651
|
+
return loggedEntries(mocks).filter((log: WorkspaceNotificationLog) => {
|
|
652
|
+
return log.status === WorkspaceNotificationStatus.Success;
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function errorLogs(mocks: Mocks): Array<WorkspaceNotificationLog> {
|
|
657
|
+
return loggedEntries(mocks).filter((log: WorkspaceNotificationLog) => {
|
|
658
|
+
return log.status === WorkspaceNotificationStatus.Error;
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function countOccurrences(haystack: string, needle: string): number {
|
|
663
|
+
return haystack.split(needle).length - 1;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/*
|
|
667
|
+
* The spec's message, written out literally for the common cases so these
|
|
668
|
+
* tests do not merely mirror the implementation's string building.
|
|
669
|
+
*/
|
|
670
|
+
const FULL_CHANNEL_MARKDOWN: string =
|
|
671
|
+
"**Test notification from OneUptime**\n\n" +
|
|
672
|
+
`This is a test notification sent from the OneUptime project **${PROJECT_NAME}** by ${USER_MARKDOWN}. ` +
|
|
673
|
+
"If you can see this message, OneUptime can post notifications to this channel. No action is needed.";
|
|
674
|
+
|
|
675
|
+
const FULL_CHAT_MARKDOWN: string =
|
|
676
|
+
"**Test notification from OneUptime**\n\n" +
|
|
677
|
+
`This is a test notification sent from the OneUptime project **${PROJECT_NAME}** by ${USER_MARKDOWN}. ` +
|
|
678
|
+
"If you can see this message, OneUptime can post notifications to this chat. No action is needed.";
|
|
679
|
+
|
|
680
|
+
const NO_PROJECT_CHANNEL_MARKDOWN: string =
|
|
681
|
+
"**Test notification from OneUptime**\n\n" +
|
|
682
|
+
`This is a test notification sent by ${USER_MARKDOWN}. ` +
|
|
683
|
+
"If you can see this message, OneUptime can post notifications to this channel. No action is needed.";
|
|
684
|
+
|
|
685
|
+
const NO_USER_CHANNEL_MARKDOWN: string =
|
|
686
|
+
"**Test notification from OneUptime**\n\n" +
|
|
687
|
+
`This is a test notification sent from the OneUptime project **${PROJECT_NAME}**. ` +
|
|
688
|
+
"If you can see this message, OneUptime can post notifications to this channel. No action is needed.";
|
|
689
|
+
|
|
690
|
+
const BARE_CHANNEL_MARKDOWN: string =
|
|
691
|
+
"**Test notification from OneUptime**\n\n" +
|
|
692
|
+
"This is a test notification sent. " +
|
|
693
|
+
"If you can see this message, OneUptime can post notifications to this channel. No action is needed.";
|
|
694
|
+
|
|
695
|
+
/*
|
|
696
|
+
* The three kinds of destination the button exists for, for behaviour that
|
|
697
|
+
* must hold for every one of them.
|
|
698
|
+
*/
|
|
699
|
+
interface DestinationCase {
|
|
700
|
+
label: string;
|
|
701
|
+
workspaceType: WorkspaceType;
|
|
702
|
+
displayName: string;
|
|
703
|
+
args: DestinationArgs;
|
|
704
|
+
destinationId: string;
|
|
705
|
+
kind: "channel" | "chat";
|
|
706
|
+
thread: ThreadSpec;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const DESTINATIONS: Array<DestinationCase> = [
|
|
710
|
+
{
|
|
711
|
+
label: "a Slack channel",
|
|
712
|
+
workspaceType: WorkspaceType.Slack,
|
|
713
|
+
displayName: "Slack",
|
|
714
|
+
args: { workspaceType: WorkspaceType.Slack, channelId: SLACK_CHANNEL_ID },
|
|
715
|
+
destinationId: SLACK_CHANNEL_ID,
|
|
716
|
+
kind: "channel",
|
|
717
|
+
thread: {
|
|
718
|
+
id: SLACK_CHANNEL_ID,
|
|
719
|
+
name: SLACK_CHANNEL_NAME,
|
|
720
|
+
threadId: "1726000000.000100",
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
label: "a Microsoft Teams channel",
|
|
725
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
726
|
+
displayName: "Microsoft Teams",
|
|
727
|
+
args: {
|
|
728
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
729
|
+
teamId: TEAMS_TEAM_ID,
|
|
730
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
731
|
+
},
|
|
732
|
+
destinationId: TEAMS_CHANNEL_ID,
|
|
733
|
+
kind: "channel",
|
|
734
|
+
thread: {
|
|
735
|
+
id: TEAMS_CHANNEL_ID,
|
|
736
|
+
name: TEAMS_CHANNEL_NAME,
|
|
737
|
+
threadId: "1726000000001",
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
label: "a Microsoft Teams chat",
|
|
742
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
743
|
+
displayName: "Microsoft Teams",
|
|
744
|
+
args: {
|
|
745
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
746
|
+
chatId: TEAMS_CHAT_ID,
|
|
747
|
+
},
|
|
748
|
+
destinationId: TEAMS_CHAT_ID,
|
|
749
|
+
kind: "chat",
|
|
750
|
+
thread: {
|
|
751
|
+
id: TEAMS_CHAT_ID,
|
|
752
|
+
name: TEAMS_CHAT_NAME,
|
|
753
|
+
threadId: "1726000000002",
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
];
|
|
757
|
+
|
|
758
|
+
beforeEach(() => {
|
|
759
|
+
jest.restoreAllMocks();
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
afterEach(() => {
|
|
763
|
+
jest.restoreAllMocks();
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// ================================================================= Slack
|
|
767
|
+
|
|
768
|
+
describe("sendTestNotificationToDestination: Slack channel", () => {
|
|
769
|
+
test("sends exactly one payload, addressed only to the channel id", async () => {
|
|
770
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
771
|
+
|
|
772
|
+
await sendTest({
|
|
773
|
+
workspaceType: WorkspaceType.Slack,
|
|
774
|
+
channelId: SLACK_CHANNEL_ID,
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
const payload: WorkspaceMessagePayload = onlyPayload(mocks);
|
|
778
|
+
|
|
779
|
+
expect(payload._type).toBe("WorkspaceMessagePayload");
|
|
780
|
+
expect(payload.workspaceType).toBe(WorkspaceType.Slack);
|
|
781
|
+
expect(payload.channelIds).toEqual([SLACK_CHANNEL_ID]);
|
|
782
|
+
expect(payload.channelNames).toEqual([]);
|
|
783
|
+
// teamId is a Microsoft Teams concept, chatIds a Teams chat concept.
|
|
784
|
+
expect(payload).not.toHaveProperty("teamId");
|
|
785
|
+
expect(payload).not.toHaveProperty("chatIds");
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
test("the message is the spec's markdown: heading, project, user and 'this channel'", async () => {
|
|
789
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
790
|
+
|
|
791
|
+
await sendTest({
|
|
792
|
+
workspaceType: WorkspaceType.Slack,
|
|
793
|
+
channelId: SLACK_CHANNEL_ID,
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
const text: string = markdownOf(onlyPayload(mocks));
|
|
797
|
+
|
|
798
|
+
expect(text).toContain("Test notification from OneUptime");
|
|
799
|
+
expect(text).toContain(PROJECT_NAME);
|
|
800
|
+
expect(text).toContain(USER_MARKDOWN);
|
|
801
|
+
expect(text).toContain("this channel");
|
|
802
|
+
expect(text).not.toContain("this chat");
|
|
803
|
+
expect(text).toBe(FULL_CHANNEL_MARKDOWN);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test("looks up the project name as root and the user markdown for the tester", async () => {
|
|
807
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
808
|
+
|
|
809
|
+
await sendTest({
|
|
810
|
+
workspaceType: WorkspaceType.Slack,
|
|
811
|
+
channelId: SLACK_CHANNEL_ID,
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
expect(mocks.findProjectSpy).toHaveBeenCalledTimes(1);
|
|
815
|
+
const projectQuery: {
|
|
816
|
+
id: ObjectID;
|
|
817
|
+
select: unknown;
|
|
818
|
+
props: unknown;
|
|
819
|
+
} = mocks.findProjectSpy.mock.calls[0]![0] as {
|
|
820
|
+
id: ObjectID;
|
|
821
|
+
select: unknown;
|
|
822
|
+
props: unknown;
|
|
823
|
+
};
|
|
824
|
+
expect(projectQuery.id.toString()).toBe(PROJECT_ID.toString());
|
|
825
|
+
expect(projectQuery.select).toEqual({ name: true });
|
|
826
|
+
expect(projectQuery.props).toEqual({ isRoot: true });
|
|
827
|
+
|
|
828
|
+
expect(mocks.userMarkdownSpy).toHaveBeenCalledTimes(1);
|
|
829
|
+
const userQuery: { userId: ObjectID; projectId: ObjectID } = mocks
|
|
830
|
+
.userMarkdownSpy.mock.calls[0]![0] as {
|
|
831
|
+
userId: ObjectID;
|
|
832
|
+
projectId: ObjectID;
|
|
833
|
+
};
|
|
834
|
+
expect(userQuery.userId.toString()).toBe(TEST_BY_USER_ID.toString());
|
|
835
|
+
expect(userQuery.projectId.toString()).toBe(PROJECT_ID.toString());
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
test("loads the Slack project auth for this project", async () => {
|
|
839
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
840
|
+
|
|
841
|
+
await sendTest({
|
|
842
|
+
workspaceType: WorkspaceType.Slack,
|
|
843
|
+
channelId: SLACK_CHANNEL_ID,
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
expect(mocks.getProjectAuthSpy).toHaveBeenCalledWith({
|
|
847
|
+
projectId: PROJECT_ID,
|
|
848
|
+
workspaceType: WorkspaceType.Slack,
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
test("returns the thread the provider reported", async () => {
|
|
853
|
+
const response: WorkspaceSendMessageResponse = makeSendResponse({
|
|
854
|
+
workspaceType: WorkspaceType.Slack,
|
|
855
|
+
threads: [
|
|
856
|
+
{
|
|
857
|
+
id: SLACK_CHANNEL_ID,
|
|
858
|
+
name: SLACK_CHANNEL_NAME,
|
|
859
|
+
threadId: "1726000000.000777",
|
|
860
|
+
},
|
|
861
|
+
],
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
mockDeps({ workspaceType: WorkspaceType.Slack, responses: [response] });
|
|
865
|
+
|
|
866
|
+
const thread: WorkspaceThread = await sendTest({
|
|
867
|
+
workspaceType: WorkspaceType.Slack,
|
|
868
|
+
channelId: SLACK_CHANNEL_ID,
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
expect(thread).toBe(response.threads[0]);
|
|
872
|
+
expect(thread.threadId).toBe("1726000000.000777");
|
|
873
|
+
expect(thread.channel.id).toBe(SLACK_CHANNEL_ID);
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
test("writes exactly one Success notification log with every spec field", async () => {
|
|
877
|
+
const mocks: Mocks = mockDeps({
|
|
878
|
+
workspaceType: WorkspaceType.Slack,
|
|
879
|
+
responses: [
|
|
880
|
+
makeSendResponse({
|
|
881
|
+
workspaceType: WorkspaceType.Slack,
|
|
882
|
+
threads: [
|
|
883
|
+
{
|
|
884
|
+
id: SLACK_CHANNEL_ID,
|
|
885
|
+
name: SLACK_CHANNEL_NAME,
|
|
886
|
+
threadId: "1726000000.000100",
|
|
887
|
+
},
|
|
888
|
+
],
|
|
889
|
+
}),
|
|
890
|
+
],
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
await sendTest({
|
|
894
|
+
workspaceType: WorkspaceType.Slack,
|
|
895
|
+
channelId: SLACK_CHANNEL_ID,
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
expect(mocks.createLogSpy).toHaveBeenCalledTimes(1);
|
|
899
|
+
|
|
900
|
+
const log: WorkspaceNotificationLog = loggedEntries(mocks)[0]!;
|
|
901
|
+
|
|
902
|
+
expect(log).toBeInstanceOf(WorkspaceNotificationLog);
|
|
903
|
+
expect(log.projectId?.toString()).toBe(PROJECT_ID.toString());
|
|
904
|
+
expect(log.workspaceType).toBe(WorkspaceType.Slack);
|
|
905
|
+
expect(log.channelId).toBe(SLACK_CHANNEL_ID);
|
|
906
|
+
expect(log.channelName).toBe(SLACK_CHANNEL_NAME);
|
|
907
|
+
expect(log.threadId).toBe("1726000000.000100");
|
|
908
|
+
expect(log.userId?.toString()).toBe(TEST_BY_USER_ID.toString());
|
|
909
|
+
expect(log.status).toBe(WorkspaceNotificationStatus.Success);
|
|
910
|
+
expect(log.statusMessage).toBe(SUCCESS_STATUS_MESSAGE);
|
|
911
|
+
expect(log.actionType).toBe(WorkspaceNotificationActionType.SendMessage);
|
|
912
|
+
expect(log.message).toBe(FULL_CHANNEL_MARKDOWN);
|
|
913
|
+
|
|
914
|
+
// Written as root: the tester's own permissions do not gate the log.
|
|
915
|
+
expect(loggedProps(mocks, 0)).toEqual({ isRoot: true });
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
test("the success log records the channel the provider reported, not just the request", async () => {
|
|
919
|
+
const mocks: Mocks = mockDeps({
|
|
920
|
+
workspaceType: WorkspaceType.Slack,
|
|
921
|
+
responses: [
|
|
922
|
+
makeSendResponse({
|
|
923
|
+
workspaceType: WorkspaceType.Slack,
|
|
924
|
+
threads: [
|
|
925
|
+
{
|
|
926
|
+
id: SLACK_CHANNEL_ID,
|
|
927
|
+
name: "incident-war-room",
|
|
928
|
+
threadId: "1726000000.000200",
|
|
929
|
+
},
|
|
930
|
+
],
|
|
931
|
+
}),
|
|
932
|
+
],
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
await sendTest({
|
|
936
|
+
workspaceType: WorkspaceType.Slack,
|
|
937
|
+
channelId: SLACK_CHANNEL_ID,
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
expect(loggedEntries(mocks)[0]!.channelName).toBe("incident-war-room");
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
test("does not consult the Microsoft Teams chat list", async () => {
|
|
944
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
945
|
+
|
|
946
|
+
await sendTest({
|
|
947
|
+
workspaceType: WorkspaceType.Slack,
|
|
948
|
+
channelId: SLACK_CHANNEL_ID,
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
expect(mocks.connectedChatsSpy).not.toHaveBeenCalled();
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
test("does not list Microsoft Teams channels, even with a teamId in the request", async () => {
|
|
955
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
956
|
+
|
|
957
|
+
await sendTest({
|
|
958
|
+
workspaceType: WorkspaceType.Slack,
|
|
959
|
+
channelId: SLACK_CHANNEL_ID,
|
|
960
|
+
teamId: TEAMS_TEAM_ID,
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
expect(mocks.listTeamChannelsSpy).not.toHaveBeenCalled();
|
|
964
|
+
expect(onlyPayload(mocks).channelIds).toEqual([SLACK_CHANNEL_ID]);
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
test("a teamId sent alongside a Slack channel is ignored, not forwarded", async () => {
|
|
968
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
969
|
+
|
|
970
|
+
await sendTest({
|
|
971
|
+
workspaceType: WorkspaceType.Slack,
|
|
972
|
+
channelId: SLACK_CHANNEL_ID,
|
|
973
|
+
teamId: TEAMS_TEAM_ID,
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
const payload: WorkspaceMessagePayload = onlyPayload(mocks);
|
|
977
|
+
expect(payload).not.toHaveProperty("teamId");
|
|
978
|
+
expect(payload.channelIds).toEqual([SLACK_CHANNEL_ID]);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
test("returns the first thread when the provider reports several", async () => {
|
|
982
|
+
mockDeps({
|
|
983
|
+
workspaceType: WorkspaceType.Slack,
|
|
984
|
+
responses: [
|
|
985
|
+
makeSendResponse({
|
|
986
|
+
workspaceType: WorkspaceType.Slack,
|
|
987
|
+
threads: [
|
|
988
|
+
{ id: SLACK_CHANNEL_ID, name: "first", threadId: "t-1" },
|
|
989
|
+
{ id: SLACK_CHANNEL_ID, name: "second", threadId: "t-2" },
|
|
990
|
+
],
|
|
991
|
+
}),
|
|
992
|
+
],
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
const thread: WorkspaceThread = await sendTest({
|
|
996
|
+
workspaceType: WorkspaceType.Slack,
|
|
997
|
+
channelId: SLACK_CHANNEL_ID,
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
expect(thread.threadId).toBe("t-1");
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
test.each<[string, string]>([
|
|
1004
|
+
["a public channel id", "C0123ABCD"],
|
|
1005
|
+
["a private channel id", "G0123ABCD"],
|
|
1006
|
+
["a direct message id", "D0123ABCD"],
|
|
1007
|
+
["lower-case letters", "c0123abcd"],
|
|
1008
|
+
["a single character", "C"],
|
|
1009
|
+
["exactly 64 characters", "C".repeat(64)],
|
|
1010
|
+
])(
|
|
1011
|
+
"accepts %s",
|
|
1012
|
+
async (_description: string, channelId: string): Promise<void> => {
|
|
1013
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
1014
|
+
|
|
1015
|
+
await sendTest({ workspaceType: WorkspaceType.Slack, channelId });
|
|
1016
|
+
|
|
1017
|
+
expect(onlyPayload(mocks).channelIds).toEqual([channelId]);
|
|
1018
|
+
},
|
|
1019
|
+
);
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
// ======================================================= Teams channels
|
|
1023
|
+
|
|
1024
|
+
describe("sendTestNotificationToDestination: Microsoft Teams channel", () => {
|
|
1025
|
+
test("forwards the teamId and addresses only the channel id", async () => {
|
|
1026
|
+
const mocks: Mocks = mockDeps({
|
|
1027
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
await sendTest({
|
|
1031
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1032
|
+
teamId: TEAMS_TEAM_ID,
|
|
1033
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
const payload: WorkspaceMessagePayload = onlyPayload(mocks);
|
|
1037
|
+
|
|
1038
|
+
expect(payload._type).toBe("WorkspaceMessagePayload");
|
|
1039
|
+
expect(payload.workspaceType).toBe(WorkspaceType.MicrosoftTeams);
|
|
1040
|
+
expect(payload.teamId).toBe(TEAMS_TEAM_ID);
|
|
1041
|
+
expect(payload.channelIds).toEqual([TEAMS_CHANNEL_ID]);
|
|
1042
|
+
expect(payload.channelNames).toEqual([]);
|
|
1043
|
+
expect(payload).not.toHaveProperty("chatIds");
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
test("the message says 'this channel' and names the project and the tester", async () => {
|
|
1047
|
+
const mocks: Mocks = mockDeps({
|
|
1048
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
await sendTest({
|
|
1052
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1053
|
+
teamId: TEAMS_TEAM_ID,
|
|
1054
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
expect(markdownOf(onlyPayload(mocks))).toBe(FULL_CHANNEL_MARKDOWN);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
test("loads the Microsoft Teams project auth for this project", async () => {
|
|
1061
|
+
const mocks: Mocks = mockDeps({
|
|
1062
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
await sendTest({
|
|
1066
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1067
|
+
teamId: TEAMS_TEAM_ID,
|
|
1068
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
expect(mocks.getProjectAuthSpy).toHaveBeenCalledWith({
|
|
1072
|
+
projectId: PROJECT_ID,
|
|
1073
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1074
|
+
});
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
test("does not consult the connected chat list for a channel", async () => {
|
|
1078
|
+
const mocks: Mocks = mockDeps({
|
|
1079
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
await sendTest({
|
|
1083
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1084
|
+
teamId: TEAMS_TEAM_ID,
|
|
1085
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
expect(mocks.connectedChatsSpy).not.toHaveBeenCalled();
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
test("lists the team's channels once, with the project's own token, before the message is built or sent", async () => {
|
|
1092
|
+
const mocks: Mocks = mockDeps({
|
|
1093
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
await sendTest({
|
|
1097
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1098
|
+
teamId: TEAMS_TEAM_ID,
|
|
1099
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
expectTeamListedOnce(mocks, TEAMS_TEAM_ID);
|
|
1103
|
+
|
|
1104
|
+
// Auth first (the listing needs its token), then the tenant check.
|
|
1105
|
+
const authOrder: number =
|
|
1106
|
+
mocks.getProjectAuthSpy.mock.invocationCallOrder[0]!;
|
|
1107
|
+
const listOrder: number =
|
|
1108
|
+
mocks.listTeamChannelsSpy.mock.invocationCallOrder[0]!;
|
|
1109
|
+
const messageOrder: number =
|
|
1110
|
+
mocks.findProjectSpy.mock.invocationCallOrder[0]!;
|
|
1111
|
+
const sendOrder: number = mocks.postSpy.mock.invocationCallOrder[0]!;
|
|
1112
|
+
|
|
1113
|
+
expect(authOrder).toBeLessThan(listOrder);
|
|
1114
|
+
expect(listOrder).toBeLessThan(messageOrder);
|
|
1115
|
+
expect(listOrder).toBeLessThan(sendOrder);
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
test("returns the thread and writes one Success log for the channel", async () => {
|
|
1119
|
+
const mocks: Mocks = mockDeps({
|
|
1120
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1121
|
+
responses: [
|
|
1122
|
+
makeSendResponse({
|
|
1123
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1124
|
+
threads: [
|
|
1125
|
+
{
|
|
1126
|
+
id: TEAMS_CHANNEL_ID,
|
|
1127
|
+
name: TEAMS_CHANNEL_NAME,
|
|
1128
|
+
threadId: "1726000000001",
|
|
1129
|
+
},
|
|
1130
|
+
],
|
|
1131
|
+
}),
|
|
1132
|
+
],
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
const thread: WorkspaceThread = await sendTest({
|
|
1136
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1137
|
+
teamId: TEAMS_TEAM_ID,
|
|
1138
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
expect(thread.threadId).toBe("1726000000001");
|
|
1142
|
+
expect(mocks.createLogSpy).toHaveBeenCalledTimes(1);
|
|
1143
|
+
|
|
1144
|
+
const log: WorkspaceNotificationLog = loggedEntries(mocks)[0]!;
|
|
1145
|
+
expect(log.projectId?.toString()).toBe(PROJECT_ID.toString());
|
|
1146
|
+
expect(log.workspaceType).toBe(WorkspaceType.MicrosoftTeams);
|
|
1147
|
+
expect(log.channelId).toBe(TEAMS_CHANNEL_ID);
|
|
1148
|
+
expect(log.channelName).toBe(TEAMS_CHANNEL_NAME);
|
|
1149
|
+
expect(log.threadId).toBe("1726000000001");
|
|
1150
|
+
expect(log.userId?.toString()).toBe(TEST_BY_USER_ID.toString());
|
|
1151
|
+
expect(log.status).toBe(WorkspaceNotificationStatus.Success);
|
|
1152
|
+
expect(log.statusMessage).toBe(SUCCESS_STATUS_MESSAGE);
|
|
1153
|
+
expect(log.actionType).toBe(WorkspaceNotificationActionType.SendMessage);
|
|
1154
|
+
expect(log.message).toBe(FULL_CHANNEL_MARKDOWN);
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
test("an empty chatId alongside a channel is treated as missing", async () => {
|
|
1158
|
+
const mocks: Mocks = mockDeps({
|
|
1159
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
await sendTest({
|
|
1163
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1164
|
+
teamId: TEAMS_TEAM_ID,
|
|
1165
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1166
|
+
chatId: "",
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
const payload: WorkspaceMessagePayload = onlyPayload(mocks);
|
|
1170
|
+
expect(payload.channelIds).toEqual([TEAMS_CHANNEL_ID]);
|
|
1171
|
+
expect(payload).not.toHaveProperty("chatIds");
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
test.each<[string, string, string]>([
|
|
1175
|
+
[
|
|
1176
|
+
"a real team GUID and thread.tacv2 channel id",
|
|
1177
|
+
"d1f7e0c6-4b0a-4a6a-9c6e-1e2f3a4b5c6d",
|
|
1178
|
+
"19:9c5a1f8e2b3d4c5e6f7a8b9c0d1e2f3a@thread.tacv2",
|
|
1179
|
+
],
|
|
1180
|
+
[
|
|
1181
|
+
"a channel id with @, :, . and -",
|
|
1182
|
+
TEAMS_TEAM_ID,
|
|
1183
|
+
"19:abc-def.ghi@thread.skype",
|
|
1184
|
+
],
|
|
1185
|
+
["ids of exactly 512 characters", "t".repeat(512), "c".repeat(512)],
|
|
1186
|
+
/*
|
|
1187
|
+
* Only a value made ENTIRELY of dots is a "." / ".." path segment; real
|
|
1188
|
+
* ids contain dots all the time and must keep working.
|
|
1189
|
+
*/
|
|
1190
|
+
[
|
|
1191
|
+
"a GUID team and a channel id containing dots",
|
|
1192
|
+
"d1f7e0c6-4b0a-4a6a-9c6e-1e2f3a4b5c6d",
|
|
1193
|
+
"19:abc@thread.tacv2",
|
|
1194
|
+
],
|
|
1195
|
+
["a team id with a leading dot", ".team", TEAMS_CHANNEL_ID],
|
|
1196
|
+
["a team id with trailing dots", "team..", TEAMS_CHANNEL_ID],
|
|
1197
|
+
["a channel id that starts with dots", TEAMS_TEAM_ID, "..19:abc@thread"],
|
|
1198
|
+
["a channel id that ends with dots", TEAMS_TEAM_ID, "19:abc@thread..."],
|
|
1199
|
+
])(
|
|
1200
|
+
"accepts %s",
|
|
1201
|
+
async (
|
|
1202
|
+
_description: string,
|
|
1203
|
+
teamId: string,
|
|
1204
|
+
channelId: string,
|
|
1205
|
+
): Promise<void> => {
|
|
1206
|
+
const mocks: Mocks = mockDeps({
|
|
1207
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1208
|
+
teamChannelsByTeamId: {
|
|
1209
|
+
[teamId]: teamListing(
|
|
1210
|
+
makeTeamChannel({ id: channelId, name: "Listed", teamId }),
|
|
1211
|
+
),
|
|
1212
|
+
},
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
await sendTest({
|
|
1216
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1217
|
+
teamId,
|
|
1218
|
+
channelId,
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
expectTeamListedOnce(mocks, teamId);
|
|
1222
|
+
|
|
1223
|
+
const payload: WorkspaceMessagePayload = onlyPayload(mocks);
|
|
1224
|
+
expect(payload.teamId).toBe(teamId);
|
|
1225
|
+
expect(payload.channelIds).toEqual([channelId]);
|
|
1226
|
+
},
|
|
1227
|
+
);
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
// ============================================ Teams channel tenant boundary
|
|
1231
|
+
|
|
1232
|
+
/*
|
|
1233
|
+
* The SaaS bot identity is shared across every customer tenant, and neither
|
|
1234
|
+
* the id format checks nor the send path prove that a Teams channel belongs
|
|
1235
|
+
* to THIS project: a caller's own installed teamId paired with a channelId
|
|
1236
|
+
* pasted from another tenant's Teams link skips Graph entirely downstream.
|
|
1237
|
+
* So the service lists the requested team's channels with the project's own
|
|
1238
|
+
* app token and requires the channelId to be an exact key of that listing.
|
|
1239
|
+
* Every refusal here must happen before the message is built: nothing sent,
|
|
1240
|
+
* nothing logged.
|
|
1241
|
+
*/
|
|
1242
|
+
describe("sendTestNotificationToDestination: a Microsoft Teams channel must be in this project's tenant", () => {
|
|
1243
|
+
test("a listing that fails with an HTTPErrorResponse sends nothing and reports Graph's message", async () => {
|
|
1244
|
+
const graphError: HTTPErrorResponse = new HTTPErrorResponse(
|
|
1245
|
+
403,
|
|
1246
|
+
{
|
|
1247
|
+
error: {
|
|
1248
|
+
code: "Forbidden",
|
|
1249
|
+
message: "Caller is not authorized to access the team.",
|
|
1250
|
+
},
|
|
1251
|
+
},
|
|
1252
|
+
{},
|
|
1253
|
+
);
|
|
1254
|
+
|
|
1255
|
+
const mocks: Mocks = mockDeps({
|
|
1256
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1257
|
+
listChannelsRejects: { error: graphError },
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
const err: Error = await expectBadData(
|
|
1261
|
+
sendTest({
|
|
1262
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1263
|
+
teamId: TEAMS_TEAM_ID,
|
|
1264
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1265
|
+
}),
|
|
1266
|
+
`${MSG_CHANNEL_LISTING_FAILED_PREFIX} Caller is not authorized to access the team.`,
|
|
1267
|
+
);
|
|
1268
|
+
|
|
1269
|
+
expect(err.message).not.toContain("[object Object]");
|
|
1270
|
+
expectTeamListedOnce(mocks, TEAMS_TEAM_ID);
|
|
1271
|
+
expectRefusedByTenantCheck(mocks);
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
test("a listing that fails with an empty HTTPErrorResponse still reports the HTTP status", async () => {
|
|
1275
|
+
const mocks: Mocks = mockDeps({
|
|
1276
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1277
|
+
listChannelsRejects: { error: new HTTPErrorResponse(502, {}, {}) },
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
const err: Error = await expectBadData(
|
|
1281
|
+
sendTest({
|
|
1282
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1283
|
+
teamId: TEAMS_TEAM_ID,
|
|
1284
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1285
|
+
}),
|
|
1286
|
+
`${MSG_CHANNEL_LISTING_FAILED_PREFIX} Request failed with HTTP status 502`,
|
|
1287
|
+
);
|
|
1288
|
+
|
|
1289
|
+
expect(err.message).not.toContain("[object Object]");
|
|
1290
|
+
expectRefusedByTenantCheck(mocks);
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
test("a listing that fails with an Error sends nothing and reports its message", async () => {
|
|
1294
|
+
const mocks: Mocks = mockDeps({
|
|
1295
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1296
|
+
listChannelsRejects: {
|
|
1297
|
+
error: new Error(
|
|
1298
|
+
"Microsoft Teams tenant ID not found for this project",
|
|
1299
|
+
),
|
|
1300
|
+
},
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
await expectBadData(
|
|
1304
|
+
sendTest({
|
|
1305
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1306
|
+
teamId: TEAMS_TEAM_ID,
|
|
1307
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1308
|
+
}),
|
|
1309
|
+
`${MSG_CHANNEL_LISTING_FAILED_PREFIX} Microsoft Teams tenant ID not found for this project`,
|
|
1310
|
+
);
|
|
1311
|
+
|
|
1312
|
+
expectTeamListedOnce(mocks, TEAMS_TEAM_ID);
|
|
1313
|
+
expectRefusedByTenantCheck(mocks);
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
test("a team outside the project's tenant (Graph refuses to list it) sends nothing", async () => {
|
|
1317
|
+
const mocks: Mocks = mockDeps({
|
|
1318
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
await expectBadData(
|
|
1322
|
+
sendTest({
|
|
1323
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1324
|
+
teamId: "bbbbbbbb-9999-8888-7777-666666666666",
|
|
1325
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1326
|
+
}),
|
|
1327
|
+
`${MSG_CHANNEL_LISTING_FAILED_PREFIX} No team found with Group Id in the tenant.`,
|
|
1328
|
+
);
|
|
1329
|
+
|
|
1330
|
+
expectTeamListedOnce(mocks, "bbbbbbbb-9999-8888-7777-666666666666");
|
|
1331
|
+
expectRefusedByTenantCheck(mocks);
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
test("a listing that resolves without the channel sends nothing", async () => {
|
|
1335
|
+
// A channelId pasted from another tenant's Teams link.
|
|
1336
|
+
const mocks: Mocks = mockDeps({
|
|
1337
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
await expectBadData(
|
|
1341
|
+
sendTest({
|
|
1342
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1343
|
+
teamId: TEAMS_TEAM_ID,
|
|
1344
|
+
channelId: "19:other-tenant-channel@thread.tacv2",
|
|
1345
|
+
}),
|
|
1346
|
+
MSG_CHANNEL_NOT_IN_TEAM,
|
|
1347
|
+
);
|
|
1348
|
+
|
|
1349
|
+
expectTeamListedOnce(mocks, TEAMS_TEAM_ID);
|
|
1350
|
+
expectRefusedByTenantCheck(mocks);
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
test("an empty listing (a team with no postable channels) sends nothing", async () => {
|
|
1354
|
+
const mocks: Mocks = mockDeps({
|
|
1355
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1356
|
+
teamChannelsByTeamId: { [TEAMS_TEAM_ID]: {} },
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
await expectBadData(
|
|
1360
|
+
sendTest({
|
|
1361
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1362
|
+
teamId: TEAMS_TEAM_ID,
|
|
1363
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1364
|
+
}),
|
|
1365
|
+
MSG_CHANNEL_NOT_IN_TEAM,
|
|
1366
|
+
);
|
|
1367
|
+
|
|
1368
|
+
expectRefusedByTenantCheck(mocks);
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
test("a channel from another team of the tenant is not accepted for the requested team", async () => {
|
|
1372
|
+
const otherTeamId: string = "cccccccc-1234-5678-9abc-def012345678";
|
|
1373
|
+
const otherTeamChannelId: string = "19:other-team-channel@thread.tacv2";
|
|
1374
|
+
|
|
1375
|
+
const mocks: Mocks = mockDeps({
|
|
1376
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1377
|
+
teamChannelsByTeamId: {
|
|
1378
|
+
[TEAMS_TEAM_ID]: teamListing(
|
|
1379
|
+
makeTeamChannel({ id: TEAMS_CHANNEL_ID, name: TEAMS_CHANNEL_NAME }),
|
|
1380
|
+
),
|
|
1381
|
+
[otherTeamId]: teamListing(
|
|
1382
|
+
makeTeamChannel({
|
|
1383
|
+
id: otherTeamChannelId,
|
|
1384
|
+
name: "Other Team Channel",
|
|
1385
|
+
teamId: otherTeamId,
|
|
1386
|
+
}),
|
|
1387
|
+
),
|
|
1388
|
+
},
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
await expectBadData(
|
|
1392
|
+
sendTest({
|
|
1393
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1394
|
+
teamId: TEAMS_TEAM_ID,
|
|
1395
|
+
channelId: otherTeamChannelId,
|
|
1396
|
+
}),
|
|
1397
|
+
MSG_CHANNEL_NOT_IN_TEAM,
|
|
1398
|
+
);
|
|
1399
|
+
|
|
1400
|
+
// Only the requested team was listed - the other team was never searched.
|
|
1401
|
+
expectTeamListedOnce(mocks, TEAMS_TEAM_ID);
|
|
1402
|
+
expectRefusedByTenantCheck(mocks);
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
test("the channel lookup is by exact id: a different-case id is not the listed channel", async () => {
|
|
1406
|
+
const mocks: Mocks = mockDeps({
|
|
1407
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
await expectBadData(
|
|
1411
|
+
sendTest({
|
|
1412
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1413
|
+
teamId: TEAMS_TEAM_ID,
|
|
1414
|
+
channelId: TEAMS_CHANNEL_ID.toUpperCase(),
|
|
1415
|
+
}),
|
|
1416
|
+
MSG_CHANNEL_NOT_IN_TEAM,
|
|
1417
|
+
);
|
|
1418
|
+
|
|
1419
|
+
expectRefusedByTenantCheck(mocks);
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
test("the channel lookup is by id: a channel NAME is not a channel id", async () => {
|
|
1423
|
+
const mocks: Mocks = mockDeps({
|
|
1424
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
await expectBadData(
|
|
1428
|
+
sendTest({
|
|
1429
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1430
|
+
teamId: TEAMS_TEAM_ID,
|
|
1431
|
+
channelId: TEAMS_CHANNEL_NAME,
|
|
1432
|
+
}),
|
|
1433
|
+
MSG_CHANNEL_NOT_IN_TEAM,
|
|
1434
|
+
);
|
|
1435
|
+
|
|
1436
|
+
expectRefusedByTenantCheck(mocks);
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
/*
|
|
1440
|
+
* Object prototype keys are "in" every plain object, so a plain index into
|
|
1441
|
+
* the listing would mistake them for a listed channel.
|
|
1442
|
+
*/
|
|
1443
|
+
test.each<[string]>([
|
|
1444
|
+
["__proto__"],
|
|
1445
|
+
["constructor"],
|
|
1446
|
+
["toString"],
|
|
1447
|
+
["hasOwnProperty"],
|
|
1448
|
+
])(
|
|
1449
|
+
"a prototype key (%s) is not a listed channel",
|
|
1450
|
+
async (channelId: string): Promise<void> => {
|
|
1451
|
+
const mocks: Mocks = mockDeps({
|
|
1452
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
await expectBadData(
|
|
1456
|
+
sendTest({
|
|
1457
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1458
|
+
teamId: TEAMS_TEAM_ID,
|
|
1459
|
+
channelId,
|
|
1460
|
+
}),
|
|
1461
|
+
MSG_CHANNEL_NOT_IN_TEAM,
|
|
1462
|
+
);
|
|
1463
|
+
|
|
1464
|
+
expectTeamListedOnce(mocks, TEAMS_TEAM_ID);
|
|
1465
|
+
expectRefusedByTenantCheck(mocks);
|
|
1466
|
+
},
|
|
1467
|
+
);
|
|
1468
|
+
|
|
1469
|
+
test("the listing is not consulted when the project is not connected", async () => {
|
|
1470
|
+
const mocks: Mocks = mockDeps({
|
|
1471
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1472
|
+
connection: "no-auth-token",
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
await expectBadData(
|
|
1476
|
+
sendTest({
|
|
1477
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1478
|
+
teamId: TEAMS_TEAM_ID,
|
|
1479
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1480
|
+
}),
|
|
1481
|
+
notConnectedMessage("Microsoft Teams"),
|
|
1482
|
+
);
|
|
1483
|
+
|
|
1484
|
+
expect(mocks.listTeamChannelsSpy).not.toHaveBeenCalled();
|
|
1485
|
+
expectRefusedByTenantCheck(mocks);
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
test("the listed channel's name labels a delivery error the send then reports", async () => {
|
|
1489
|
+
/*
|
|
1490
|
+
* Downstream, a channel whose name Graph would not return is reported
|
|
1491
|
+
* under its raw id; the admin knows it by the name the listing gave.
|
|
1492
|
+
*/
|
|
1493
|
+
const mocks: Mocks = mockDeps({
|
|
1494
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1495
|
+
teamChannelsByTeamId: {
|
|
1496
|
+
[TEAMS_TEAM_ID]: teamListing(
|
|
1497
|
+
makeTeamChannel({ id: TEAMS_CHANNEL_ID, name: "Incident Bridge" }),
|
|
1498
|
+
),
|
|
1499
|
+
},
|
|
1500
|
+
responses: [
|
|
1501
|
+
makeSendResponse({
|
|
1502
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1503
|
+
errors: [
|
|
1504
|
+
{
|
|
1505
|
+
id: TEAMS_CHANNEL_ID,
|
|
1506
|
+
name: TEAMS_CHANNEL_ID,
|
|
1507
|
+
error: "Forbidden",
|
|
1508
|
+
},
|
|
1509
|
+
],
|
|
1510
|
+
}),
|
|
1511
|
+
],
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
await expectBadData(
|
|
1515
|
+
sendTest({
|
|
1516
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1517
|
+
teamId: TEAMS_TEAM_ID,
|
|
1518
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1519
|
+
}),
|
|
1520
|
+
`Could not send the test notification to "Incident Bridge". Forbidden`,
|
|
1521
|
+
);
|
|
1522
|
+
|
|
1523
|
+
expect(mocks.postSpy).toHaveBeenCalledTimes(1);
|
|
1524
|
+
expect(errorLogs(mocks)).toHaveLength(1);
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
test("the listed channel's name labels the reconnect error when nothing was sent", async () => {
|
|
1528
|
+
const mocks: Mocks = mockDeps({
|
|
1529
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1530
|
+
teamChannelsByTeamId: {
|
|
1531
|
+
[TEAMS_TEAM_ID]: teamListing(
|
|
1532
|
+
makeTeamChannel({ id: TEAMS_CHANNEL_ID, name: "Incident Bridge" }),
|
|
1533
|
+
),
|
|
1534
|
+
},
|
|
1535
|
+
responses: [],
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
await expectBadData(
|
|
1539
|
+
sendTest({
|
|
1540
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1541
|
+
teamId: TEAMS_TEAM_ID,
|
|
1542
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
1543
|
+
}),
|
|
1544
|
+
reconnectMessage({
|
|
1545
|
+
destinationLabel: `"Incident Bridge"`,
|
|
1546
|
+
displayName: "Microsoft Teams",
|
|
1547
|
+
}),
|
|
1548
|
+
);
|
|
1549
|
+
|
|
1550
|
+
const failures: Array<WorkspaceNotificationLog> = errorLogs(mocks);
|
|
1551
|
+
expect(failures).toHaveLength(1);
|
|
1552
|
+
expect(failures[0]!.channelId).toBe(TEAMS_CHANNEL_ID);
|
|
1553
|
+
expect(failures[0]!.channelName).toBe("Incident Bridge");
|
|
1554
|
+
});
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
// ========================================================== Teams chats
|
|
1558
|
+
|
|
1559
|
+
describe("sendTestNotificationToDestination: Microsoft Teams chat", () => {
|
|
1560
|
+
test("addresses only the chat id: no channels, no teamId", async () => {
|
|
1561
|
+
const mocks: Mocks = mockDeps({
|
|
1562
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
await sendTest({
|
|
1566
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1567
|
+
chatId: TEAMS_CHAT_ID,
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
const payload: WorkspaceMessagePayload = onlyPayload(mocks);
|
|
1571
|
+
|
|
1572
|
+
expect(payload._type).toBe("WorkspaceMessagePayload");
|
|
1573
|
+
expect(payload.workspaceType).toBe(WorkspaceType.MicrosoftTeams);
|
|
1574
|
+
expect(payload.chatIds).toEqual([TEAMS_CHAT_ID]);
|
|
1575
|
+
expect(payload.channelIds).toEqual([]);
|
|
1576
|
+
expect(payload.channelNames).toEqual([]);
|
|
1577
|
+
expect(payload).not.toHaveProperty("teamId");
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
test("the message says 'this chat', not 'this channel'", async () => {
|
|
1581
|
+
const mocks: Mocks = mockDeps({
|
|
1582
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
await sendTest({
|
|
1586
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1587
|
+
chatId: TEAMS_CHAT_ID,
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
const text: string = markdownOf(onlyPayload(mocks));
|
|
1591
|
+
|
|
1592
|
+
expect(text).toContain("this chat");
|
|
1593
|
+
expect(text).not.toContain("this channel");
|
|
1594
|
+
expect(text).toBe(FULL_CHAT_MARKDOWN);
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
test("consults the project's connected chats before sending", async () => {
|
|
1598
|
+
const mocks: Mocks = mockDeps({
|
|
1599
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
await sendTest({
|
|
1603
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1604
|
+
chatId: TEAMS_CHAT_ID,
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
expect(mocks.connectedChatsSpy).toHaveBeenCalledTimes(1);
|
|
1608
|
+
expect(mocks.connectedChatsSpy).toHaveBeenCalledWith({
|
|
1609
|
+
projectId: PROJECT_ID,
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
const chatsCheckOrder: number =
|
|
1613
|
+
mocks.connectedChatsSpy.mock.invocationCallOrder[0]!;
|
|
1614
|
+
const sendOrder: number = mocks.postSpy.mock.invocationCallOrder[0]!;
|
|
1615
|
+
expect(chatsCheckOrder).toBeLessThan(sendOrder);
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
test("does not list any team's channels, even with a teamId in the request", async () => {
|
|
1619
|
+
const mocks: Mocks = mockDeps({
|
|
1620
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
await sendTest({
|
|
1624
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1625
|
+
chatId: TEAMS_CHAT_ID,
|
|
1626
|
+
teamId: TEAMS_TEAM_ID,
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
expect(mocks.listTeamChannelsSpy).not.toHaveBeenCalled();
|
|
1630
|
+
expect(onlyPayload(mocks).chatIds).toEqual([TEAMS_CHAT_ID]);
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
test("uses the connected chat's name when a failure is reported", async () => {
|
|
1634
|
+
// The provider only knows the raw conversation id; the admin knows the name.
|
|
1635
|
+
mockDeps({
|
|
1636
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1637
|
+
responses: [
|
|
1638
|
+
makeSendResponse({
|
|
1639
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1640
|
+
errors: [
|
|
1641
|
+
{
|
|
1642
|
+
id: TEAMS_CHAT_ID,
|
|
1643
|
+
name: TEAMS_CHAT_ID,
|
|
1644
|
+
error: "Bot is not a member of this conversation.",
|
|
1645
|
+
},
|
|
1646
|
+
],
|
|
1647
|
+
}),
|
|
1648
|
+
],
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
await expectBadData(
|
|
1652
|
+
sendTest({
|
|
1653
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1654
|
+
chatId: TEAMS_CHAT_ID,
|
|
1655
|
+
}),
|
|
1656
|
+
`Could not send the test notification to "${TEAMS_CHAT_NAME}". Bot is not a member of this conversation.`,
|
|
1657
|
+
);
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
test("returns the thread and writes one Success log for the chat", async () => {
|
|
1661
|
+
const mocks: Mocks = mockDeps({
|
|
1662
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1663
|
+
responses: [
|
|
1664
|
+
makeSendResponse({
|
|
1665
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1666
|
+
threads: [
|
|
1667
|
+
{
|
|
1668
|
+
id: TEAMS_CHAT_ID,
|
|
1669
|
+
name: TEAMS_CHAT_NAME,
|
|
1670
|
+
threadId: "1726000000002",
|
|
1671
|
+
},
|
|
1672
|
+
],
|
|
1673
|
+
}),
|
|
1674
|
+
],
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
const thread: WorkspaceThread = await sendTest({
|
|
1678
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1679
|
+
chatId: TEAMS_CHAT_ID,
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
expect(thread.threadId).toBe("1726000000002");
|
|
1683
|
+
expect(mocks.createLogSpy).toHaveBeenCalledTimes(1);
|
|
1684
|
+
|
|
1685
|
+
const log: WorkspaceNotificationLog = loggedEntries(mocks)[0]!;
|
|
1686
|
+
expect(log.projectId?.toString()).toBe(PROJECT_ID.toString());
|
|
1687
|
+
expect(log.workspaceType).toBe(WorkspaceType.MicrosoftTeams);
|
|
1688
|
+
expect(log.channelId).toBe(TEAMS_CHAT_ID);
|
|
1689
|
+
expect(log.channelName).toBe(TEAMS_CHAT_NAME);
|
|
1690
|
+
expect(log.threadId).toBe("1726000000002");
|
|
1691
|
+
expect(log.userId?.toString()).toBe(TEST_BY_USER_ID.toString());
|
|
1692
|
+
expect(log.status).toBe(WorkspaceNotificationStatus.Success);
|
|
1693
|
+
expect(log.statusMessage).toBe(SUCCESS_STATUS_MESSAGE);
|
|
1694
|
+
expect(log.actionType).toBe(WorkspaceNotificationActionType.SendMessage);
|
|
1695
|
+
expect(log.message).toBe(FULL_CHAT_MARKDOWN);
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
test("a personal (1:1) chat is sent to just like a group chat", async () => {
|
|
1699
|
+
const mocks: Mocks = mockDeps({
|
|
1700
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
await sendTest({
|
|
1704
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1705
|
+
chatId: "19:personal-bob@unq.gbl.spaces",
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
expect(onlyPayload(mocks).chatIds).toEqual([
|
|
1709
|
+
"19:personal-bob@unq.gbl.spaces",
|
|
1710
|
+
]);
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
test("a teamId sent alongside a chat is ignored, not forwarded", async () => {
|
|
1714
|
+
const mocks: Mocks = mockDeps({
|
|
1715
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
await sendTest({
|
|
1719
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1720
|
+
chatId: TEAMS_CHAT_ID,
|
|
1721
|
+
teamId: TEAMS_TEAM_ID,
|
|
1722
|
+
});
|
|
1723
|
+
|
|
1724
|
+
const payload: WorkspaceMessagePayload = onlyPayload(mocks);
|
|
1725
|
+
expect(payload).not.toHaveProperty("teamId");
|
|
1726
|
+
expect(payload.chatIds).toEqual([TEAMS_CHAT_ID]);
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
test("an empty channelId alongside a chat is treated as missing", async () => {
|
|
1730
|
+
const mocks: Mocks = mockDeps({
|
|
1731
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
await sendTest({
|
|
1735
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1736
|
+
chatId: TEAMS_CHAT_ID,
|
|
1737
|
+
channelId: "",
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
const payload: WorkspaceMessagePayload = onlyPayload(mocks);
|
|
1741
|
+
expect(payload.chatIds).toEqual([TEAMS_CHAT_ID]);
|
|
1742
|
+
expect(payload.channelIds).toEqual([]);
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
test("a chat id of exactly 1024 characters is accepted when it is connected", async () => {
|
|
1746
|
+
const longChatId: string = "19:" + "c".repeat(1021);
|
|
1747
|
+
expect(longChatId).toHaveLength(1024);
|
|
1748
|
+
|
|
1749
|
+
const mocks: Mocks = mockDeps({
|
|
1750
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1751
|
+
availableChats: {
|
|
1752
|
+
[longChatId]: makeChat({ id: longChatId, name: "Long Chat" }),
|
|
1753
|
+
},
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
await sendTest({
|
|
1757
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1758
|
+
chatId: longChatId,
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
expect(onlyPayload(mocks).chatIds).toEqual([longChatId]);
|
|
1762
|
+
});
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
// ============================================================== trimming
|
|
1766
|
+
|
|
1767
|
+
describe("sendTestNotificationToDestination: ids are trimmed before use", () => {
|
|
1768
|
+
test("a Slack channel id with surrounding whitespace is sent trimmed", async () => {
|
|
1769
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
1770
|
+
|
|
1771
|
+
await sendTest({
|
|
1772
|
+
workspaceType: WorkspaceType.Slack,
|
|
1773
|
+
channelId: ` ${SLACK_CHANNEL_ID}\t\n`,
|
|
1774
|
+
});
|
|
1775
|
+
|
|
1776
|
+
expect(onlyPayload(mocks).channelIds).toEqual([SLACK_CHANNEL_ID]);
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
test("Microsoft Teams team and channel ids are sent trimmed", async () => {
|
|
1780
|
+
const mocks: Mocks = mockDeps({
|
|
1781
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
await sendTest({
|
|
1785
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1786
|
+
teamId: `\n ${TEAMS_TEAM_ID} `,
|
|
1787
|
+
channelId: ` \t${TEAMS_CHANNEL_ID} `,
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
// The tenant check lists the trimmed team and finds the trimmed channel.
|
|
1791
|
+
expectTeamListedOnce(mocks, TEAMS_TEAM_ID);
|
|
1792
|
+
|
|
1793
|
+
const payload: WorkspaceMessagePayload = onlyPayload(mocks);
|
|
1794
|
+
expect(payload.teamId).toBe(TEAMS_TEAM_ID);
|
|
1795
|
+
expect(payload.channelIds).toEqual([TEAMS_CHANNEL_ID]);
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
test("a chat id is trimmed before the connected-chat lookup and the send", async () => {
|
|
1799
|
+
const mocks: Mocks = mockDeps({
|
|
1800
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
await sendTest({
|
|
1804
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1805
|
+
chatId: ` ${TEAMS_CHAT_ID} `,
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
expect(onlyPayload(mocks).chatIds).toEqual([TEAMS_CHAT_ID]);
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
test("a whitespace-only chatId alongside a Slack channel is treated as missing", async () => {
|
|
1812
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
1813
|
+
|
|
1814
|
+
await sendTest({
|
|
1815
|
+
workspaceType: WorkspaceType.Slack,
|
|
1816
|
+
channelId: SLACK_CHANNEL_ID,
|
|
1817
|
+
chatId: " ",
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
expect(onlyPayload(mocks).channelIds).toEqual([SLACK_CHANNEL_ID]);
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
test("the failure log records the trimmed destination", async () => {
|
|
1824
|
+
const mocks: Mocks = mockDeps({
|
|
1825
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1826
|
+
responses: [],
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
/*
|
|
1830
|
+
* Nothing sent: the failure log's destination comes from the request
|
|
1831
|
+
* itself, named by the team's channel listing.
|
|
1832
|
+
*/
|
|
1833
|
+
await expectBadData(
|
|
1834
|
+
sendTest({
|
|
1835
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1836
|
+
teamId: ` ${TEAMS_TEAM_ID} `,
|
|
1837
|
+
channelId: ` ${TEAMS_CHANNEL_ID} `,
|
|
1838
|
+
}),
|
|
1839
|
+
reconnectMessage({
|
|
1840
|
+
destinationLabel: `"${TEAMS_CHANNEL_NAME}"`,
|
|
1841
|
+
displayName: "Microsoft Teams",
|
|
1842
|
+
}),
|
|
1843
|
+
);
|
|
1844
|
+
|
|
1845
|
+
expect(errorLogs(mocks)[0]!.channelId).toBe(TEAMS_CHANNEL_ID);
|
|
1846
|
+
expect(errorLogs(mocks)[0]!.channelName).toBe(TEAMS_CHANNEL_NAME);
|
|
1847
|
+
});
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
// ============================================================ validation
|
|
1851
|
+
|
|
1852
|
+
describe("sendTestNotificationToDestination: validation rejects before any work", () => {
|
|
1853
|
+
describe("workspace type", () => {
|
|
1854
|
+
test.each<[string, string]>([
|
|
1855
|
+
["an unknown workspace", "Discord"],
|
|
1856
|
+
["the wrong casing of a real one", "slack"],
|
|
1857
|
+
["an empty workspace type", ""],
|
|
1858
|
+
])(
|
|
1859
|
+
"rejects %s",
|
|
1860
|
+
async (_description: string, workspaceType: string): Promise<void> => {
|
|
1861
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
1862
|
+
|
|
1863
|
+
await expectBadData(
|
|
1864
|
+
sendTest({
|
|
1865
|
+
workspaceType: workspaceType as WorkspaceType,
|
|
1866
|
+
channelId: SLACK_CHANNEL_ID,
|
|
1867
|
+
}),
|
|
1868
|
+
MSG_UNSUPPORTED_WORKSPACE_TYPE_PATTERN,
|
|
1869
|
+
);
|
|
1870
|
+
|
|
1871
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
1872
|
+
},
|
|
1873
|
+
);
|
|
1874
|
+
|
|
1875
|
+
test("an unsupported workspace type is rejected even with no destination", async () => {
|
|
1876
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
1877
|
+
|
|
1878
|
+
await expectBadData(
|
|
1879
|
+
sendTest({ workspaceType: "Discord" as WorkspaceType }),
|
|
1880
|
+
MSG_UNSUPPORTED_WORKSPACE_TYPE_PATTERN,
|
|
1881
|
+
);
|
|
1882
|
+
|
|
1883
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
1884
|
+
});
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
describe("exactly one of channel / chat", () => {
|
|
1888
|
+
test.each<[string, WorkspaceType]>([
|
|
1889
|
+
["Slack", WorkspaceType.Slack],
|
|
1890
|
+
["Microsoft Teams", WorkspaceType.MicrosoftTeams],
|
|
1891
|
+
])(
|
|
1892
|
+
"%s: neither a channel nor a chat",
|
|
1893
|
+
async (_name: string, workspaceType: WorkspaceType): Promise<void> => {
|
|
1894
|
+
const mocks: Mocks = mockDeps({ workspaceType });
|
|
1895
|
+
|
|
1896
|
+
await expectBadData(sendTest({ workspaceType }), MSG_NO_DESTINATION);
|
|
1897
|
+
|
|
1898
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
1899
|
+
},
|
|
1900
|
+
);
|
|
1901
|
+
|
|
1902
|
+
test.each<[string, string | undefined, string | undefined]>([
|
|
1903
|
+
["both empty strings", "", ""],
|
|
1904
|
+
["both whitespace-only", " ", "\t\n"],
|
|
1905
|
+
["an empty channel and no chat", "", undefined],
|
|
1906
|
+
["no channel and a whitespace-only chat", undefined, " "],
|
|
1907
|
+
])(
|
|
1908
|
+
"treats %s as no destination",
|
|
1909
|
+
async (
|
|
1910
|
+
_description: string,
|
|
1911
|
+
channelId: string | undefined,
|
|
1912
|
+
chatId: string | undefined,
|
|
1913
|
+
): Promise<void> => {
|
|
1914
|
+
const mocks: Mocks = mockDeps({
|
|
1915
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1918
|
+
await expectBadData(
|
|
1919
|
+
sendTest({
|
|
1920
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
1921
|
+
teamId: TEAMS_TEAM_ID,
|
|
1922
|
+
channelId,
|
|
1923
|
+
chatId,
|
|
1924
|
+
}),
|
|
1925
|
+
MSG_NO_DESTINATION,
|
|
1926
|
+
);
|
|
1927
|
+
|
|
1928
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
1929
|
+
},
|
|
1930
|
+
);
|
|
1931
|
+
|
|
1932
|
+
test("a non-string channel id (a malformed request body) counts as missing", async () => {
|
|
1933
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
1934
|
+
|
|
1935
|
+
await expectBadData(
|
|
1936
|
+
sendTest({
|
|
1937
|
+
workspaceType: WorkspaceType.Slack,
|
|
1938
|
+
channelId: 12345 as unknown as string,
|
|
1939
|
+
}),
|
|
1940
|
+
MSG_NO_DESTINATION,
|
|
1941
|
+
);
|
|
1942
|
+
|
|
1943
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
test.each<[string, WorkspaceType]>([
|
|
1947
|
+
["Slack", WorkspaceType.Slack],
|
|
1948
|
+
["Microsoft Teams", WorkspaceType.MicrosoftTeams],
|
|
1949
|
+
])(
|
|
1950
|
+
"%s: both a channel and a chat",
|
|
1951
|
+
async (_name: string, workspaceType: WorkspaceType): Promise<void> => {
|
|
1952
|
+
const mocks: Mocks = mockDeps({ workspaceType });
|
|
1953
|
+
|
|
1954
|
+
await expectBadData(
|
|
1955
|
+
sendTest({
|
|
1956
|
+
workspaceType,
|
|
1957
|
+
teamId: TEAMS_TEAM_ID,
|
|
1958
|
+
channelId:
|
|
1959
|
+
workspaceType === WorkspaceType.Slack
|
|
1960
|
+
? SLACK_CHANNEL_ID
|
|
1961
|
+
: TEAMS_CHANNEL_ID,
|
|
1962
|
+
chatId: TEAMS_CHAT_ID,
|
|
1963
|
+
}),
|
|
1964
|
+
MSG_BOTH_DESTINATIONS,
|
|
1965
|
+
);
|
|
1966
|
+
|
|
1967
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
1968
|
+
},
|
|
1969
|
+
);
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
describe("chats are Microsoft Teams only", () => {
|
|
1973
|
+
test("a chatId with Slack is rejected", async () => {
|
|
1974
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
1975
|
+
|
|
1976
|
+
await expectBadData(
|
|
1977
|
+
sendTest({ workspaceType: WorkspaceType.Slack, chatId: TEAMS_CHAT_ID }),
|
|
1978
|
+
MSG_CHAT_WITH_SLACK,
|
|
1979
|
+
);
|
|
1980
|
+
|
|
1981
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
test("a Slack-looking chatId with Slack is rejected too", async () => {
|
|
1985
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
1986
|
+
|
|
1987
|
+
await expectBadData(
|
|
1988
|
+
sendTest({ workspaceType: WorkspaceType.Slack, chatId: "D0123ABCD" }),
|
|
1989
|
+
MSG_CHAT_WITH_SLACK,
|
|
1990
|
+
);
|
|
1991
|
+
|
|
1992
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
1993
|
+
});
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
describe("Slack channel id format", () => {
|
|
1997
|
+
test.each<[string, string]>([
|
|
1998
|
+
["a path traversal", "../C0123ABCD"],
|
|
1999
|
+
["a forward slash", "C0123/ABCD"],
|
|
2000
|
+
["a backslash", "C0123\\ABCD"],
|
|
2001
|
+
["an interior space", "C0123 ABCD"],
|
|
2002
|
+
["an interior tab", "C0123\tABCD"],
|
|
2003
|
+
["an interior newline", "C0123\nABCD"],
|
|
2004
|
+
["a channel name rather than an id", "#general"],
|
|
2005
|
+
["a hyphen", "C0123-ABCD"],
|
|
2006
|
+
["an underscore", "C0123_ABCD"],
|
|
2007
|
+
["a dot", "C0123.ABCD"],
|
|
2008
|
+
["a query string", "C0123ABCD?limit=1"],
|
|
2009
|
+
["a percent escape", "C0123%2FABCD"],
|
|
2010
|
+
["a URL", "https://slack.com/api/chat.postMessage"],
|
|
2011
|
+
["a non-ASCII letter", `C0123${String.fromCharCode(0xc4)}BCD`],
|
|
2012
|
+
["a NUL control character", `C0123${String.fromCharCode(0x00)}ABCD`],
|
|
2013
|
+
["65 characters", "C".repeat(65)],
|
|
2014
|
+
["a very long id", "C".repeat(10000)],
|
|
2015
|
+
])(
|
|
2016
|
+
"rejects %s",
|
|
2017
|
+
async (_description: string, channelId: string): Promise<void> => {
|
|
2018
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
2019
|
+
|
|
2020
|
+
await expectBadData(
|
|
2021
|
+
sendTest({ workspaceType: WorkspaceType.Slack, channelId }),
|
|
2022
|
+
MSG_INVALID_SLACK_CHANNEL,
|
|
2023
|
+
);
|
|
2024
|
+
|
|
2025
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
2026
|
+
},
|
|
2027
|
+
);
|
|
2028
|
+
});
|
|
2029
|
+
|
|
2030
|
+
describe("Microsoft Teams channels need a team", () => {
|
|
2031
|
+
test.each<[string, string | undefined]>([
|
|
2032
|
+
["missing", undefined],
|
|
2033
|
+
["empty", ""],
|
|
2034
|
+
["whitespace-only", " \t "],
|
|
2035
|
+
])(
|
|
2036
|
+
"rejects a teamId that is %s",
|
|
2037
|
+
async (
|
|
2038
|
+
_description: string,
|
|
2039
|
+
teamId: string | undefined,
|
|
2040
|
+
): Promise<void> => {
|
|
2041
|
+
const mocks: Mocks = mockDeps({
|
|
2042
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2043
|
+
});
|
|
2044
|
+
|
|
2045
|
+
await expectBadData(
|
|
2046
|
+
sendTest({
|
|
2047
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2048
|
+
teamId,
|
|
2049
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
2050
|
+
}),
|
|
2051
|
+
MSG_TEAM_REQUIRED,
|
|
2052
|
+
);
|
|
2053
|
+
|
|
2054
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
2055
|
+
},
|
|
2056
|
+
);
|
|
2057
|
+
});
|
|
2058
|
+
|
|
2059
|
+
/*
|
|
2060
|
+
* Team and channel ids are interpolated unencoded into Microsoft Graph URL
|
|
2061
|
+
* paths downstream (/teams/{teamId}/channels/{channelId}). Each of these
|
|
2062
|
+
* characters would let a caller steer that request at a different Graph
|
|
2063
|
+
* resource using the project's app token.
|
|
2064
|
+
*/
|
|
2065
|
+
const FORBIDDEN_GRAPH_PATH_CHARACTERS: Array<[string, string]> = [
|
|
2066
|
+
["a forward slash", "/"],
|
|
2067
|
+
["a backslash", "\\"],
|
|
2068
|
+
["a question mark", "?"],
|
|
2069
|
+
["a hash", "#"],
|
|
2070
|
+
["a percent sign", "%"],
|
|
2071
|
+
["a space", " "],
|
|
2072
|
+
["a tab", "\t"],
|
|
2073
|
+
["a newline", "\n"],
|
|
2074
|
+
["a carriage return", "\r"],
|
|
2075
|
+
/*
|
|
2076
|
+
* Built from char codes rather than escapes so no formatter can turn
|
|
2077
|
+
* them into raw (and, for U+2028, line-breaking) characters.
|
|
2078
|
+
*/
|
|
2079
|
+
["a vertical tab", String.fromCharCode(0x0b)],
|
|
2080
|
+
["a form feed", String.fromCharCode(0x0c)],
|
|
2081
|
+
["a non-breaking space", String.fromCharCode(0xa0)],
|
|
2082
|
+
["a line separator", String.fromCharCode(0x2028)],
|
|
2083
|
+
["a NUL control character", String.fromCharCode(0x00)],
|
|
2084
|
+
["an escape control character", String.fromCharCode(0x1b)],
|
|
2085
|
+
["a unit-separator control character", String.fromCharCode(0x1f)],
|
|
2086
|
+
["a DEL control character", String.fromCharCode(0x7f)],
|
|
2087
|
+
["a C1 control character", String.fromCharCode(0x85)],
|
|
2088
|
+
["the last C1 control character", String.fromCharCode(0x9f)],
|
|
2089
|
+
];
|
|
2090
|
+
|
|
2091
|
+
/*
|
|
2092
|
+
* URL normalisation resolves "." and ".." as relative-path segments
|
|
2093
|
+
* (/teams/T/channels/.. becomes /teams/T/), so an id made only of dots is
|
|
2094
|
+
* path traversal even though it contains no forbidden character. Padded
|
|
2095
|
+
* variants are trimmed to the same thing first.
|
|
2096
|
+
*/
|
|
2097
|
+
const DOT_SEGMENT_IDS: Array<[string, string]> = [
|
|
2098
|
+
["a single dot", "."],
|
|
2099
|
+
["two dots", ".."],
|
|
2100
|
+
["three dots", "..."],
|
|
2101
|
+
["two dots padded with whitespace", " ..\t"],
|
|
2102
|
+
];
|
|
2103
|
+
|
|
2104
|
+
describe("Microsoft Teams team id is safe for a Graph URL path", () => {
|
|
2105
|
+
test.each<[string, string]>(FORBIDDEN_GRAPH_PATH_CHARACTERS)(
|
|
2106
|
+
"rejects a teamId containing %s",
|
|
2107
|
+
async (_description: string, character: string): Promise<void> => {
|
|
2108
|
+
const mocks: Mocks = mockDeps({
|
|
2109
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
await expectBadData(
|
|
2113
|
+
sendTest({
|
|
2114
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2115
|
+
teamId: `aaaaaaaa-1111${character}2222-3333`,
|
|
2116
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
2117
|
+
}),
|
|
2118
|
+
MSG_INVALID_TEAMS_TEAM,
|
|
2119
|
+
);
|
|
2120
|
+
|
|
2121
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
2122
|
+
},
|
|
2123
|
+
);
|
|
2124
|
+
|
|
2125
|
+
test.each<[string, string]>([
|
|
2126
|
+
["a path traversal to another resource", "../../users"],
|
|
2127
|
+
["an encoded path traversal", "team%2F..%2Fusers"],
|
|
2128
|
+
["a smuggled query", `${TEAMS_TEAM_ID}?$select=id`],
|
|
2129
|
+
["a smuggled fragment", `${TEAMS_TEAM_ID}#fragment`],
|
|
2130
|
+
["a full URL", "https://graph.microsoft.com/v1.0/users"],
|
|
2131
|
+
["513 characters", "t".repeat(513)],
|
|
2132
|
+
])(
|
|
2133
|
+
"rejects a teamId that is %s",
|
|
2134
|
+
async (_description: string, teamId: string): Promise<void> => {
|
|
2135
|
+
const mocks: Mocks = mockDeps({
|
|
2136
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2137
|
+
});
|
|
2138
|
+
|
|
2139
|
+
await expectBadData(
|
|
2140
|
+
sendTest({
|
|
2141
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2142
|
+
teamId,
|
|
2143
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
2144
|
+
}),
|
|
2145
|
+
MSG_INVALID_TEAMS_TEAM,
|
|
2146
|
+
);
|
|
2147
|
+
|
|
2148
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
2149
|
+
},
|
|
2150
|
+
);
|
|
2151
|
+
|
|
2152
|
+
test.each<[string, string]>(DOT_SEGMENT_IDS)(
|
|
2153
|
+
"rejects a teamId that is a dot segment (%s) before any lookup",
|
|
2154
|
+
async (_description: string, teamId: string): Promise<void> => {
|
|
2155
|
+
/*
|
|
2156
|
+
* Even a listing that would "contain" the channel for that team must
|
|
2157
|
+
* not be reached: the teamId goes into the listing URL itself.
|
|
2158
|
+
*/
|
|
2159
|
+
const mocks: Mocks = mockDeps({
|
|
2160
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2161
|
+
teamChannelsByTeamId: {
|
|
2162
|
+
[teamId.trim()]: teamListing(
|
|
2163
|
+
makeTeamChannel({
|
|
2164
|
+
id: TEAMS_CHANNEL_ID,
|
|
2165
|
+
name: TEAMS_CHANNEL_NAME,
|
|
2166
|
+
}),
|
|
2167
|
+
),
|
|
2168
|
+
},
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
await expectBadData(
|
|
2172
|
+
sendTest({
|
|
2173
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2174
|
+
teamId,
|
|
2175
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
2176
|
+
}),
|
|
2177
|
+
MSG_INVALID_TEAMS_TEAM,
|
|
2178
|
+
);
|
|
2179
|
+
|
|
2180
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
2181
|
+
},
|
|
2182
|
+
);
|
|
2183
|
+
});
|
|
2184
|
+
|
|
2185
|
+
describe("Microsoft Teams channel id is safe for a Graph URL path", () => {
|
|
2186
|
+
test.each<[string, string]>(FORBIDDEN_GRAPH_PATH_CHARACTERS)(
|
|
2187
|
+
"rejects a channelId containing %s",
|
|
2188
|
+
async (_description: string, character: string): Promise<void> => {
|
|
2189
|
+
const mocks: Mocks = mockDeps({
|
|
2190
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
await expectBadData(
|
|
2194
|
+
sendTest({
|
|
2195
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2196
|
+
teamId: TEAMS_TEAM_ID,
|
|
2197
|
+
channelId: `19:general${character}channel@thread.tacv2`,
|
|
2198
|
+
}),
|
|
2199
|
+
MSG_INVALID_TEAMS_CHANNEL,
|
|
2200
|
+
);
|
|
2201
|
+
|
|
2202
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
2203
|
+
},
|
|
2204
|
+
);
|
|
2205
|
+
|
|
2206
|
+
test.each<[string, string]>([
|
|
2207
|
+
["a path into messages", "19:x@thread.tacv2/messages"],
|
|
2208
|
+
["a path traversal", "19:x@thread.tacv2/../../../users"],
|
|
2209
|
+
["a smuggled query", "19:x@thread.tacv2?$top=50"],
|
|
2210
|
+
["an encoded slash", "19:x@thread.tacv2%2Fmessages"],
|
|
2211
|
+
["513 characters", "19:" + "c".repeat(510)],
|
|
2212
|
+
])(
|
|
2213
|
+
"rejects a channelId that is %s",
|
|
2214
|
+
async (_description: string, channelId: string): Promise<void> => {
|
|
2215
|
+
const mocks: Mocks = mockDeps({
|
|
2216
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2217
|
+
});
|
|
2218
|
+
|
|
2219
|
+
await expectBadData(
|
|
2220
|
+
sendTest({
|
|
2221
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2222
|
+
teamId: TEAMS_TEAM_ID,
|
|
2223
|
+
channelId,
|
|
2224
|
+
}),
|
|
2225
|
+
MSG_INVALID_TEAMS_CHANNEL,
|
|
2226
|
+
);
|
|
2227
|
+
|
|
2228
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
2229
|
+
},
|
|
2230
|
+
);
|
|
2231
|
+
|
|
2232
|
+
test.each<[string, string]>(DOT_SEGMENT_IDS)(
|
|
2233
|
+
"rejects a channelId that is a dot segment (%s) before any lookup",
|
|
2234
|
+
async (_description: string, channelId: string): Promise<void> => {
|
|
2235
|
+
// Rejected by format, not merely because the listing lacks it.
|
|
2236
|
+
const mocks: Mocks = mockDeps({
|
|
2237
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2238
|
+
teamChannelsByTeamId: {
|
|
2239
|
+
[TEAMS_TEAM_ID]: teamListing(
|
|
2240
|
+
makeTeamChannel({ id: channelId.trim(), name: "Dots" }),
|
|
2241
|
+
),
|
|
2242
|
+
},
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
await expectBadData(
|
|
2246
|
+
sendTest({
|
|
2247
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2248
|
+
teamId: TEAMS_TEAM_ID,
|
|
2249
|
+
channelId,
|
|
2250
|
+
}),
|
|
2251
|
+
MSG_INVALID_TEAMS_CHANNEL,
|
|
2252
|
+
);
|
|
2253
|
+
|
|
2254
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
2255
|
+
},
|
|
2256
|
+
);
|
|
2257
|
+
|
|
2258
|
+
test("a dot-segment teamId is reported before a dot-segment channelId", async () => {
|
|
2259
|
+
const mocks: Mocks = mockDeps({
|
|
2260
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2261
|
+
});
|
|
2262
|
+
|
|
2263
|
+
await expectBadData(
|
|
2264
|
+
sendTest({
|
|
2265
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2266
|
+
teamId: "..",
|
|
2267
|
+
channelId: ".",
|
|
2268
|
+
}),
|
|
2269
|
+
MSG_INVALID_TEAMS_TEAM,
|
|
2270
|
+
);
|
|
2271
|
+
|
|
2272
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
test("a bad teamId is reported before a bad channelId", async () => {
|
|
2276
|
+
const mocks: Mocks = mockDeps({
|
|
2277
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2280
|
+
await expectBadData(
|
|
2281
|
+
sendTest({
|
|
2282
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2283
|
+
teamId: "team/x",
|
|
2284
|
+
channelId: "channel/y",
|
|
2285
|
+
}),
|
|
2286
|
+
MSG_INVALID_TEAMS_TEAM,
|
|
2287
|
+
);
|
|
2288
|
+
|
|
2289
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
2290
|
+
});
|
|
2291
|
+
});
|
|
2292
|
+
|
|
2293
|
+
describe("Microsoft Teams chat id length", () => {
|
|
2294
|
+
test("rejects a chat id over 1024 characters", async () => {
|
|
2295
|
+
const chatId: string = "19:" + "c".repeat(1022);
|
|
2296
|
+
expect(chatId).toHaveLength(1025);
|
|
2297
|
+
|
|
2298
|
+
const mocks: Mocks = mockDeps({
|
|
2299
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2300
|
+
availableChats: {
|
|
2301
|
+
[chatId]: makeChat({ id: chatId, name: "Too Long" }),
|
|
2302
|
+
},
|
|
2303
|
+
});
|
|
2304
|
+
|
|
2305
|
+
await expectBadData(
|
|
2306
|
+
sendTest({ workspaceType: WorkspaceType.MicrosoftTeams, chatId }),
|
|
2307
|
+
MSG_INVALID_TEAMS_CHAT,
|
|
2308
|
+
);
|
|
2309
|
+
|
|
2310
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
2311
|
+
});
|
|
2312
|
+
|
|
2313
|
+
test("rejects a very long chat id", async () => {
|
|
2314
|
+
const mocks: Mocks = mockDeps({
|
|
2315
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2316
|
+
});
|
|
2317
|
+
|
|
2318
|
+
await expectBadData(
|
|
2319
|
+
sendTest({
|
|
2320
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2321
|
+
chatId: "x".repeat(100000),
|
|
2322
|
+
}),
|
|
2323
|
+
MSG_INVALID_TEAMS_CHAT,
|
|
2324
|
+
);
|
|
2325
|
+
|
|
2326
|
+
expectRejectedBeforeAnyWork(mocks);
|
|
2327
|
+
});
|
|
2328
|
+
});
|
|
2329
|
+
});
|
|
2330
|
+
|
|
2331
|
+
// ========================================================= not connected
|
|
2332
|
+
|
|
2333
|
+
describe("sendTestNotificationToDestination: project not connected", () => {
|
|
2334
|
+
test.each(DESTINATIONS)(
|
|
2335
|
+
"$label: no project auth record",
|
|
2336
|
+
async (destination: DestinationCase): Promise<void> => {
|
|
2337
|
+
const mocks: Mocks = mockDeps({
|
|
2338
|
+
workspaceType: destination.workspaceType,
|
|
2339
|
+
connection: "not-connected",
|
|
2340
|
+
});
|
|
2341
|
+
|
|
2342
|
+
await expectBadData(
|
|
2343
|
+
sendTest(destination.args),
|
|
2344
|
+
notConnectedMessage(destination.displayName),
|
|
2345
|
+
);
|
|
2346
|
+
|
|
2347
|
+
expect(mocks.getProjectAuthSpy).toHaveBeenCalledWith({
|
|
2348
|
+
projectId: PROJECT_ID,
|
|
2349
|
+
workspaceType: destination.workspaceType,
|
|
2350
|
+
});
|
|
2351
|
+
// No token, so no Teams channel listing either.
|
|
2352
|
+
expect(mocks.listTeamChannelsSpy).not.toHaveBeenCalled();
|
|
2353
|
+
expectNothingSentOrLogged(mocks);
|
|
2354
|
+
},
|
|
2355
|
+
);
|
|
2356
|
+
|
|
2357
|
+
test.each(DESTINATIONS)(
|
|
2358
|
+
"$label: auth record without an auth token",
|
|
2359
|
+
async (destination: DestinationCase): Promise<void> => {
|
|
2360
|
+
const mocks: Mocks = mockDeps({
|
|
2361
|
+
workspaceType: destination.workspaceType,
|
|
2362
|
+
connection: "no-auth-token",
|
|
2363
|
+
});
|
|
2364
|
+
|
|
2365
|
+
await expectBadData(
|
|
2366
|
+
sendTest(destination.args),
|
|
2367
|
+
notConnectedMessage(destination.displayName),
|
|
2368
|
+
);
|
|
2369
|
+
|
|
2370
|
+
expect(mocks.listTeamChannelsSpy).not.toHaveBeenCalled();
|
|
2371
|
+
expectNothingSentOrLogged(mocks);
|
|
2372
|
+
},
|
|
2373
|
+
);
|
|
2374
|
+
|
|
2375
|
+
test("the Slack message names Slack, not Microsoft Teams", async () => {
|
|
2376
|
+
mockDeps({
|
|
2377
|
+
workspaceType: WorkspaceType.Slack,
|
|
2378
|
+
connection: "not-connected",
|
|
2379
|
+
});
|
|
2380
|
+
|
|
2381
|
+
const err: Error = await captureError(
|
|
2382
|
+
sendTest({
|
|
2383
|
+
workspaceType: WorkspaceType.Slack,
|
|
2384
|
+
channelId: SLACK_CHANNEL_ID,
|
|
2385
|
+
}),
|
|
2386
|
+
);
|
|
2387
|
+
|
|
2388
|
+
expect(err.message).toContain("Slack");
|
|
2389
|
+
expect(err.message).not.toContain("Microsoft Teams");
|
|
2390
|
+
});
|
|
2391
|
+
|
|
2392
|
+
test("the Microsoft Teams message names Microsoft Teams, not Slack", async () => {
|
|
2393
|
+
mockDeps({
|
|
2394
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2395
|
+
connection: "not-connected",
|
|
2396
|
+
});
|
|
2397
|
+
|
|
2398
|
+
const err: Error = await captureError(
|
|
2399
|
+
sendTest({
|
|
2400
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2401
|
+
teamId: TEAMS_TEAM_ID,
|
|
2402
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
2403
|
+
}),
|
|
2404
|
+
);
|
|
2405
|
+
|
|
2406
|
+
expect(err.message).toContain("Microsoft Teams");
|
|
2407
|
+
expect(err.message).not.toContain("Slack");
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
test("an empty-string auth token counts as not connected", async () => {
|
|
2411
|
+
const mocks: Mocks = mockDeps({ workspaceType: WorkspaceType.Slack });
|
|
2412
|
+
|
|
2413
|
+
const auth: WorkspaceProjectAuthToken = makeProjectAuth({
|
|
2414
|
+
workspaceType: WorkspaceType.Slack,
|
|
2415
|
+
authToken: "",
|
|
2416
|
+
});
|
|
2417
|
+
mocks.getProjectAuthSpy.mockResolvedValue(auth);
|
|
2418
|
+
|
|
2419
|
+
await expectBadData(
|
|
2420
|
+
sendTest({
|
|
2421
|
+
workspaceType: WorkspaceType.Slack,
|
|
2422
|
+
channelId: SLACK_CHANNEL_ID,
|
|
2423
|
+
}),
|
|
2424
|
+
notConnectedMessage("Slack"),
|
|
2425
|
+
);
|
|
2426
|
+
|
|
2427
|
+
expectNothingSentOrLogged(mocks);
|
|
2428
|
+
});
|
|
2429
|
+
});
|
|
2430
|
+
|
|
2431
|
+
// ================================================== chat not connected
|
|
2432
|
+
|
|
2433
|
+
describe("sendTestNotificationToDestination: chat is not one of the project's connected chats", () => {
|
|
2434
|
+
test("a chat id missing from the connected chats is rejected", async () => {
|
|
2435
|
+
const mocks: Mocks = mockDeps({
|
|
2436
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2437
|
+
});
|
|
2438
|
+
|
|
2439
|
+
await expectBadData(
|
|
2440
|
+
sendTest({
|
|
2441
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2442
|
+
chatId: "19:someone-elses-chat@thread.v2",
|
|
2443
|
+
}),
|
|
2444
|
+
MSG_CHAT_NOT_CONNECTED,
|
|
2445
|
+
);
|
|
2446
|
+
|
|
2447
|
+
expect(mocks.connectedChatsSpy).toHaveBeenCalled();
|
|
2448
|
+
expectNothingSentOrLogged(mocks);
|
|
2449
|
+
});
|
|
2450
|
+
|
|
2451
|
+
test("an empty connected-chat list rejects every chat", async () => {
|
|
2452
|
+
const mocks: Mocks = mockDeps({
|
|
2453
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2454
|
+
availableChats: {},
|
|
2455
|
+
});
|
|
2456
|
+
|
|
2457
|
+
await expectBadData(
|
|
2458
|
+
sendTest({
|
|
2459
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2460
|
+
chatId: TEAMS_CHAT_ID,
|
|
2461
|
+
}),
|
|
2462
|
+
MSG_CHAT_NOT_CONNECTED,
|
|
2463
|
+
);
|
|
2464
|
+
|
|
2465
|
+
expectNothingSentOrLogged(mocks);
|
|
2466
|
+
});
|
|
2467
|
+
|
|
2468
|
+
test("an auth record with no misc data (no chats captured yet) rejects the chat", async () => {
|
|
2469
|
+
const mocks: Mocks = mockDeps({
|
|
2470
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2471
|
+
availableChats: null,
|
|
2472
|
+
});
|
|
2473
|
+
|
|
2474
|
+
await expectBadData(
|
|
2475
|
+
sendTest({
|
|
2476
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2477
|
+
chatId: TEAMS_CHAT_ID,
|
|
2478
|
+
}),
|
|
2479
|
+
MSG_CHAT_NOT_CONNECTED,
|
|
2480
|
+
);
|
|
2481
|
+
|
|
2482
|
+
expectNothingSentOrLogged(mocks);
|
|
2483
|
+
});
|
|
2484
|
+
|
|
2485
|
+
test("the lookup is by id: a chat with the same NAME but a different id does not count", async () => {
|
|
2486
|
+
const mocks: Mocks = mockDeps({
|
|
2487
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2488
|
+
availableChats: {
|
|
2489
|
+
"19:other@thread.v2": makeChat({
|
|
2490
|
+
id: "19:other@thread.v2",
|
|
2491
|
+
name: TEAMS_CHAT_NAME,
|
|
2492
|
+
}),
|
|
2493
|
+
},
|
|
2494
|
+
});
|
|
2495
|
+
|
|
2496
|
+
await expectBadData(
|
|
2497
|
+
sendTest({
|
|
2498
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2499
|
+
chatId: TEAMS_CHAT_ID,
|
|
2500
|
+
}),
|
|
2501
|
+
MSG_CHAT_NOT_CONNECTED,
|
|
2502
|
+
);
|
|
2503
|
+
|
|
2504
|
+
expectNothingSentOrLogged(mocks);
|
|
2505
|
+
});
|
|
2506
|
+
|
|
2507
|
+
test("the lookup is case-sensitive", async () => {
|
|
2508
|
+
const mocks: Mocks = mockDeps({
|
|
2509
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2510
|
+
});
|
|
2511
|
+
|
|
2512
|
+
await expectBadData(
|
|
2513
|
+
sendTest({
|
|
2514
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2515
|
+
chatId: TEAMS_CHAT_ID.toUpperCase(),
|
|
2516
|
+
}),
|
|
2517
|
+
MSG_CHAT_NOT_CONNECTED,
|
|
2518
|
+
);
|
|
2519
|
+
|
|
2520
|
+
expectNothingSentOrLogged(mocks);
|
|
2521
|
+
});
|
|
2522
|
+
|
|
2523
|
+
/*
|
|
2524
|
+
* The bot is shared across tenants; a project may only post to chats it
|
|
2525
|
+
* captured. Object prototype keys are "in" every plain object, so they must
|
|
2526
|
+
* not be mistaken for a captured chat.
|
|
2527
|
+
*/
|
|
2528
|
+
test.each<[string]>([
|
|
2529
|
+
["__proto__"],
|
|
2530
|
+
["constructor"],
|
|
2531
|
+
["toString"],
|
|
2532
|
+
["hasOwnProperty"],
|
|
2533
|
+
])(
|
|
2534
|
+
"a prototype key (%s) is not a connected chat",
|
|
2535
|
+
async (chatId: string): Promise<void> => {
|
|
2536
|
+
const mocks: Mocks = mockDeps({
|
|
2537
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2538
|
+
});
|
|
2539
|
+
|
|
2540
|
+
await expectBadData(
|
|
2541
|
+
sendTest({ workspaceType: WorkspaceType.MicrosoftTeams, chatId }),
|
|
2542
|
+
MSG_CHAT_NOT_CONNECTED,
|
|
2543
|
+
);
|
|
2544
|
+
|
|
2545
|
+
expectNothingSentOrLogged(mocks);
|
|
2546
|
+
},
|
|
2547
|
+
);
|
|
2548
|
+
|
|
2549
|
+
test("project connection is checked before the chat list", async () => {
|
|
2550
|
+
const mocks: Mocks = mockDeps({
|
|
2551
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2552
|
+
connection: "not-connected",
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
await expectBadData(
|
|
2556
|
+
sendTest({
|
|
2557
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2558
|
+
chatId: "19:unknown@thread.v2",
|
|
2559
|
+
}),
|
|
2560
|
+
notConnectedMessage("Microsoft Teams"),
|
|
2561
|
+
);
|
|
2562
|
+
|
|
2563
|
+
expectNothingSentOrLogged(mocks);
|
|
2564
|
+
});
|
|
2565
|
+
});
|
|
2566
|
+
|
|
2567
|
+
// ======================================================= delivery errors
|
|
2568
|
+
|
|
2569
|
+
describe("sendTestNotificationToDestination: provider reports delivery errors", () => {
|
|
2570
|
+
test("Slack: throws with #name and the provider error, and logs the failure", async () => {
|
|
2571
|
+
const mocks: Mocks = mockDeps({
|
|
2572
|
+
workspaceType: WorkspaceType.Slack,
|
|
2573
|
+
responses: [
|
|
2574
|
+
makeSendResponse({
|
|
2575
|
+
workspaceType: WorkspaceType.Slack,
|
|
2576
|
+
errors: [
|
|
2577
|
+
{
|
|
2578
|
+
id: SLACK_CHANNEL_ID,
|
|
2579
|
+
name: SLACK_CHANNEL_NAME,
|
|
2580
|
+
error: "not_in_channel",
|
|
2581
|
+
},
|
|
2582
|
+
],
|
|
2583
|
+
}),
|
|
2584
|
+
],
|
|
2585
|
+
});
|
|
2586
|
+
|
|
2587
|
+
const err: Error = await expectBadData(
|
|
2588
|
+
sendTest({
|
|
2589
|
+
workspaceType: WorkspaceType.Slack,
|
|
2590
|
+
channelId: SLACK_CHANNEL_ID,
|
|
2591
|
+
}),
|
|
2592
|
+
`Could not send the test notification to #${SLACK_CHANNEL_NAME}. not_in_channel`,
|
|
2593
|
+
);
|
|
2594
|
+
|
|
2595
|
+
expect(err.message).toContain(`#${SLACK_CHANNEL_NAME}`);
|
|
2596
|
+
expect(err.message).toContain("not_in_channel");
|
|
2597
|
+
|
|
2598
|
+
expect(mocks.createLogSpy).toHaveBeenCalledTimes(1);
|
|
2599
|
+
const log: WorkspaceNotificationLog = loggedEntries(mocks)[0]!;
|
|
2600
|
+
expect(log.status).toBe(WorkspaceNotificationStatus.Error);
|
|
2601
|
+
expect(log.statusMessage).toBe("not_in_channel");
|
|
2602
|
+
expect(log.projectId?.toString()).toBe(PROJECT_ID.toString());
|
|
2603
|
+
expect(log.workspaceType).toBe(WorkspaceType.Slack);
|
|
2604
|
+
expect(log.channelId).toBe(SLACK_CHANNEL_ID);
|
|
2605
|
+
expect(log.channelName).toBe(SLACK_CHANNEL_NAME);
|
|
2606
|
+
expect(log.userId?.toString()).toBe(TEST_BY_USER_ID.toString());
|
|
2607
|
+
expect(log.actionType).toBe(WorkspaceNotificationActionType.SendMessage);
|
|
2608
|
+
expect(log.message).toBe(FULL_CHANNEL_MARKDOWN);
|
|
2609
|
+
expect(successLogs(mocks)).toHaveLength(0);
|
|
2610
|
+
});
|
|
2611
|
+
|
|
2612
|
+
test("Microsoft Teams channel: throws with the quoted name and the provider error", async () => {
|
|
2613
|
+
const mocks: Mocks = mockDeps({
|
|
2614
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2615
|
+
responses: [
|
|
2616
|
+
makeSendResponse({
|
|
2617
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2618
|
+
errors: [
|
|
2619
|
+
{
|
|
2620
|
+
id: TEAMS_CHANNEL_ID,
|
|
2621
|
+
name: TEAMS_CHANNEL_NAME,
|
|
2622
|
+
error: "The bot is not part of the conversation roster.",
|
|
2623
|
+
},
|
|
2624
|
+
],
|
|
2625
|
+
}),
|
|
2626
|
+
],
|
|
2627
|
+
});
|
|
2628
|
+
|
|
2629
|
+
await expectBadData(
|
|
2630
|
+
sendTest({
|
|
2631
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2632
|
+
teamId: TEAMS_TEAM_ID,
|
|
2633
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
2634
|
+
}),
|
|
2635
|
+
`Could not send the test notification to "${TEAMS_CHANNEL_NAME}". The bot is not part of the conversation roster.`,
|
|
2636
|
+
);
|
|
2637
|
+
|
|
2638
|
+
const failures: Array<WorkspaceNotificationLog> = errorLogs(mocks);
|
|
2639
|
+
expect(failures).toHaveLength(1);
|
|
2640
|
+
expect(failures[0]!.statusMessage).toBe(
|
|
2641
|
+
"The bot is not part of the conversation roster.",
|
|
2642
|
+
);
|
|
2643
|
+
expect(failures[0]!.workspaceType).toBe(WorkspaceType.MicrosoftTeams);
|
|
2644
|
+
expect(failures[0]!.channelId).toBe(TEAMS_CHANNEL_ID);
|
|
2645
|
+
expect(successLogs(mocks)).toHaveLength(0);
|
|
2646
|
+
});
|
|
2647
|
+
|
|
2648
|
+
test("Microsoft Teams chat: logs the failure and throws with the chat's quoted name", async () => {
|
|
2649
|
+
const mocks: Mocks = mockDeps({
|
|
2650
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2651
|
+
responses: [
|
|
2652
|
+
makeSendResponse({
|
|
2653
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2654
|
+
errors: [
|
|
2655
|
+
{
|
|
2656
|
+
id: TEAMS_CHAT_ID,
|
|
2657
|
+
name: TEAMS_CHAT_NAME,
|
|
2658
|
+
error: "Forbidden",
|
|
2659
|
+
},
|
|
2660
|
+
],
|
|
2661
|
+
}),
|
|
2662
|
+
],
|
|
2663
|
+
});
|
|
2664
|
+
|
|
2665
|
+
await expectBadData(
|
|
2666
|
+
sendTest({
|
|
2667
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2668
|
+
chatId: TEAMS_CHAT_ID,
|
|
2669
|
+
}),
|
|
2670
|
+
`Could not send the test notification to "${TEAMS_CHAT_NAME}". Forbidden`,
|
|
2671
|
+
);
|
|
2672
|
+
|
|
2673
|
+
const failures: Array<WorkspaceNotificationLog> = errorLogs(mocks);
|
|
2674
|
+
expect(failures).toHaveLength(1);
|
|
2675
|
+
expect(failures[0]!.statusMessage).toBe("Forbidden");
|
|
2676
|
+
expect(failures[0]!.channelId).toBe(TEAMS_CHAT_ID);
|
|
2677
|
+
expect(successLogs(mocks)).toHaveLength(0);
|
|
2678
|
+
});
|
|
2679
|
+
|
|
2680
|
+
test("several errors: one failure log each, and every error in the message", async () => {
|
|
2681
|
+
const mocks: Mocks = mockDeps({
|
|
2682
|
+
workspaceType: WorkspaceType.Slack,
|
|
2683
|
+
responses: [
|
|
2684
|
+
makeSendResponse({
|
|
2685
|
+
workspaceType: WorkspaceType.Slack,
|
|
2686
|
+
errors: [
|
|
2687
|
+
{
|
|
2688
|
+
id: SLACK_CHANNEL_ID,
|
|
2689
|
+
name: SLACK_CHANNEL_NAME,
|
|
2690
|
+
error: "not_in_channel",
|
|
2691
|
+
},
|
|
2692
|
+
{
|
|
2693
|
+
id: SLACK_CHANNEL_ID,
|
|
2694
|
+
name: SLACK_CHANNEL_NAME,
|
|
2695
|
+
error: "rate_limited",
|
|
2696
|
+
},
|
|
2697
|
+
],
|
|
2698
|
+
}),
|
|
2699
|
+
],
|
|
2700
|
+
});
|
|
2701
|
+
|
|
2702
|
+
await expectBadData(
|
|
2703
|
+
sendTest({
|
|
2704
|
+
workspaceType: WorkspaceType.Slack,
|
|
2705
|
+
channelId: SLACK_CHANNEL_ID,
|
|
2706
|
+
}),
|
|
2707
|
+
`Could not send the test notification to #${SLACK_CHANNEL_NAME}. not_in_channel; rate_limited`,
|
|
2708
|
+
);
|
|
2709
|
+
|
|
2710
|
+
const failures: Array<WorkspaceNotificationLog> = errorLogs(mocks);
|
|
2711
|
+
expect(failures).toHaveLength(2);
|
|
2712
|
+
expect(
|
|
2713
|
+
failures.map((log: WorkspaceNotificationLog) => {
|
|
2714
|
+
return log.statusMessage;
|
|
2715
|
+
}),
|
|
2716
|
+
).toEqual(["not_in_channel", "rate_limited"]);
|
|
2717
|
+
});
|
|
2718
|
+
|
|
2719
|
+
test("Slack: with no channel name reported, the label falls back to #<id>", async () => {
|
|
2720
|
+
mockDeps({
|
|
2721
|
+
workspaceType: WorkspaceType.Slack,
|
|
2722
|
+
responses: [
|
|
2723
|
+
makeSendResponse({
|
|
2724
|
+
workspaceType: WorkspaceType.Slack,
|
|
2725
|
+
errors: [
|
|
2726
|
+
{ id: SLACK_CHANNEL_ID, name: "", error: "channel_not_found" },
|
|
2727
|
+
],
|
|
2728
|
+
}),
|
|
2729
|
+
],
|
|
2730
|
+
});
|
|
2731
|
+
|
|
2732
|
+
await expectBadData(
|
|
2733
|
+
sendTest({
|
|
2734
|
+
workspaceType: WorkspaceType.Slack,
|
|
2735
|
+
channelId: SLACK_CHANNEL_ID,
|
|
2736
|
+
}),
|
|
2737
|
+
`Could not send the test notification to #${SLACK_CHANNEL_ID}. channel_not_found`,
|
|
2738
|
+
);
|
|
2739
|
+
});
|
|
2740
|
+
|
|
2741
|
+
test("Microsoft Teams channel: with no name listed or reported, the label falls back to the quoted id", async () => {
|
|
2742
|
+
mockDeps({
|
|
2743
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2744
|
+
teamChannelsByTeamId: {
|
|
2745
|
+
[TEAMS_TEAM_ID]: teamListing(
|
|
2746
|
+
makeTeamChannel({ id: TEAMS_CHANNEL_ID, name: "" }),
|
|
2747
|
+
),
|
|
2748
|
+
},
|
|
2749
|
+
responses: [
|
|
2750
|
+
makeSendResponse({
|
|
2751
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2752
|
+
errors: [{ id: TEAMS_CHANNEL_ID, name: "", error: "NotFound" }],
|
|
2753
|
+
}),
|
|
2754
|
+
],
|
|
2755
|
+
});
|
|
2756
|
+
|
|
2757
|
+
await expectBadData(
|
|
2758
|
+
sendTest({
|
|
2759
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2760
|
+
teamId: TEAMS_TEAM_ID,
|
|
2761
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
2762
|
+
}),
|
|
2763
|
+
`Could not send the test notification to "${TEAMS_CHANNEL_ID}". NotFound`,
|
|
2764
|
+
);
|
|
2765
|
+
});
|
|
2766
|
+
|
|
2767
|
+
test("Microsoft Teams channel: with no name listed, the name the provider reported is used", async () => {
|
|
2768
|
+
mockDeps({
|
|
2769
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2770
|
+
teamChannelsByTeamId: {
|
|
2771
|
+
[TEAMS_TEAM_ID]: teamListing(
|
|
2772
|
+
makeTeamChannel({ id: TEAMS_CHANNEL_ID, name: "" }),
|
|
2773
|
+
),
|
|
2774
|
+
},
|
|
2775
|
+
responses: [
|
|
2776
|
+
makeSendResponse({
|
|
2777
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2778
|
+
errors: [
|
|
2779
|
+
{ id: TEAMS_CHANNEL_ID, name: "Reported Name", error: "NotFound" },
|
|
2780
|
+
],
|
|
2781
|
+
}),
|
|
2782
|
+
],
|
|
2783
|
+
});
|
|
2784
|
+
|
|
2785
|
+
await expectBadData(
|
|
2786
|
+
sendTest({
|
|
2787
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2788
|
+
teamId: TEAMS_TEAM_ID,
|
|
2789
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
2790
|
+
}),
|
|
2791
|
+
`Could not send the test notification to "Reported Name". NotFound`,
|
|
2792
|
+
);
|
|
2793
|
+
});
|
|
2794
|
+
|
|
2795
|
+
test("errors win over threads: a partial success is still reported as a failure", async () => {
|
|
2796
|
+
const mocks: Mocks = mockDeps({
|
|
2797
|
+
workspaceType: WorkspaceType.Slack,
|
|
2798
|
+
responses: [
|
|
2799
|
+
makeSendResponse({
|
|
2800
|
+
workspaceType: WorkspaceType.Slack,
|
|
2801
|
+
threads: [
|
|
2802
|
+
{
|
|
2803
|
+
id: SLACK_CHANNEL_ID,
|
|
2804
|
+
name: SLACK_CHANNEL_NAME,
|
|
2805
|
+
threadId: "t-1",
|
|
2806
|
+
},
|
|
2807
|
+
],
|
|
2808
|
+
errors: [
|
|
2809
|
+
{
|
|
2810
|
+
id: SLACK_CHANNEL_ID,
|
|
2811
|
+
name: SLACK_CHANNEL_NAME,
|
|
2812
|
+
error: "msg_too_long",
|
|
2813
|
+
},
|
|
2814
|
+
],
|
|
2815
|
+
}),
|
|
2816
|
+
],
|
|
2817
|
+
});
|
|
2818
|
+
|
|
2819
|
+
await expectBadData(
|
|
2820
|
+
sendTest({
|
|
2821
|
+
workspaceType: WorkspaceType.Slack,
|
|
2822
|
+
channelId: SLACK_CHANNEL_ID,
|
|
2823
|
+
}),
|
|
2824
|
+
`Could not send the test notification to #${SLACK_CHANNEL_NAME}. msg_too_long`,
|
|
2825
|
+
);
|
|
2826
|
+
|
|
2827
|
+
expect(successLogs(mocks)).toHaveLength(0);
|
|
2828
|
+
expect(errorLogs(mocks)).toHaveLength(1);
|
|
2829
|
+
});
|
|
2830
|
+
|
|
2831
|
+
test("the send is attempted exactly once; a reported error is not retried", async () => {
|
|
2832
|
+
const mocks: Mocks = mockDeps({
|
|
2833
|
+
workspaceType: WorkspaceType.Slack,
|
|
2834
|
+
responses: [
|
|
2835
|
+
makeSendResponse({
|
|
2836
|
+
workspaceType: WorkspaceType.Slack,
|
|
2837
|
+
errors: [
|
|
2838
|
+
{
|
|
2839
|
+
id: SLACK_CHANNEL_ID,
|
|
2840
|
+
name: SLACK_CHANNEL_NAME,
|
|
2841
|
+
error: "not_in_channel",
|
|
2842
|
+
},
|
|
2843
|
+
],
|
|
2844
|
+
}),
|
|
2845
|
+
],
|
|
2846
|
+
});
|
|
2847
|
+
|
|
2848
|
+
await captureError(
|
|
2849
|
+
sendTest({
|
|
2850
|
+
workspaceType: WorkspaceType.Slack,
|
|
2851
|
+
channelId: SLACK_CHANNEL_ID,
|
|
2852
|
+
}),
|
|
2853
|
+
);
|
|
2854
|
+
|
|
2855
|
+
expect(mocks.postSpy).toHaveBeenCalledTimes(1);
|
|
2856
|
+
});
|
|
2857
|
+
|
|
2858
|
+
test("a failure-log write that fails does not mask the delivery error", async () => {
|
|
2859
|
+
const mocks: Mocks = mockDeps({
|
|
2860
|
+
workspaceType: WorkspaceType.Slack,
|
|
2861
|
+
logWriteFails: true,
|
|
2862
|
+
responses: [
|
|
2863
|
+
makeSendResponse({
|
|
2864
|
+
workspaceType: WorkspaceType.Slack,
|
|
2865
|
+
errors: [
|
|
2866
|
+
{
|
|
2867
|
+
id: SLACK_CHANNEL_ID,
|
|
2868
|
+
name: SLACK_CHANNEL_NAME,
|
|
2869
|
+
error: "not_in_channel",
|
|
2870
|
+
},
|
|
2871
|
+
],
|
|
2872
|
+
}),
|
|
2873
|
+
],
|
|
2874
|
+
});
|
|
2875
|
+
|
|
2876
|
+
await expectBadData(
|
|
2877
|
+
sendTest({
|
|
2878
|
+
workspaceType: WorkspaceType.Slack,
|
|
2879
|
+
channelId: SLACK_CHANNEL_ID,
|
|
2880
|
+
}),
|
|
2881
|
+
`Could not send the test notification to #${SLACK_CHANNEL_NAME}. not_in_channel`,
|
|
2882
|
+
);
|
|
2883
|
+
|
|
2884
|
+
expect(mocks.createLogSpy).toHaveBeenCalled();
|
|
2885
|
+
});
|
|
2886
|
+
|
|
2887
|
+
test("only the response for the requested workspace type is read", async () => {
|
|
2888
|
+
// A Microsoft Teams error in the batch must not fail a Slack test.
|
|
2889
|
+
const mocks: Mocks = mockDeps({
|
|
2890
|
+
workspaceType: WorkspaceType.Slack,
|
|
2891
|
+
responses: [
|
|
2892
|
+
makeSendResponse({
|
|
2893
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
2894
|
+
errors: [{ id: TEAMS_CHANNEL_ID, name: "Other", error: "unrelated" }],
|
|
2895
|
+
}),
|
|
2896
|
+
makeSendResponse({
|
|
2897
|
+
workspaceType: WorkspaceType.Slack,
|
|
2898
|
+
threads: [
|
|
2899
|
+
{
|
|
2900
|
+
id: SLACK_CHANNEL_ID,
|
|
2901
|
+
name: SLACK_CHANNEL_NAME,
|
|
2902
|
+
threadId: "slack-thread",
|
|
2903
|
+
},
|
|
2904
|
+
],
|
|
2905
|
+
}),
|
|
2906
|
+
],
|
|
2907
|
+
});
|
|
2908
|
+
|
|
2909
|
+
const thread: WorkspaceThread = await sendTest({
|
|
2910
|
+
workspaceType: WorkspaceType.Slack,
|
|
2911
|
+
channelId: SLACK_CHANNEL_ID,
|
|
2912
|
+
});
|
|
2913
|
+
|
|
2914
|
+
expect(thread.threadId).toBe("slack-thread");
|
|
2915
|
+
expect(errorLogs(mocks)).toHaveLength(0);
|
|
2916
|
+
expect(successLogs(mocks)).toHaveLength(1);
|
|
2917
|
+
});
|
|
2918
|
+
});
|
|
2919
|
+
|
|
2920
|
+
// ========================================================== nothing sent
|
|
2921
|
+
|
|
2922
|
+
/*
|
|
2923
|
+
* WorkspaceNotificationLog.channelId / channelName are 100-character columns
|
|
2924
|
+
* and statusMessage is 500. The insert rejects an over-long field outright and
|
|
2925
|
+
* the best-effort logging swallows that, so without truncation the row simply
|
|
2926
|
+
* vanishes - and Microsoft's "app is not installed in this team" explanation
|
|
2927
|
+
* alone is longer than 500 characters.
|
|
2928
|
+
*/
|
|
2929
|
+
describe("sendTestNotificationToDestination: log fields are cut to their column limits", () => {
|
|
2930
|
+
const LONG_PROVIDER_ERROR: string = "E".repeat(650);
|
|
2931
|
+
const LONG_NAME: string = "n".repeat(150);
|
|
2932
|
+
const LONG_ID: string = "C".repeat(150);
|
|
2933
|
+
|
|
2934
|
+
test("a failure log keeps the first 500 characters of the provider error and 100 of the channel name", async () => {
|
|
2935
|
+
const mocks: Mocks = mockDeps({
|
|
2936
|
+
workspaceType: WorkspaceType.Slack,
|
|
2937
|
+
responses: [
|
|
2938
|
+
makeSendResponse({
|
|
2939
|
+
workspaceType: WorkspaceType.Slack,
|
|
2940
|
+
errors: [
|
|
2941
|
+
{
|
|
2942
|
+
id: SLACK_CHANNEL_ID,
|
|
2943
|
+
name: LONG_NAME,
|
|
2944
|
+
error: LONG_PROVIDER_ERROR,
|
|
2945
|
+
},
|
|
2946
|
+
],
|
|
2947
|
+
}),
|
|
2948
|
+
],
|
|
2949
|
+
});
|
|
2950
|
+
|
|
2951
|
+
const err: Error = await captureError(
|
|
2952
|
+
sendTest({
|
|
2953
|
+
workspaceType: WorkspaceType.Slack,
|
|
2954
|
+
channelId: SLACK_CHANNEL_ID,
|
|
2955
|
+
}),
|
|
2956
|
+
);
|
|
2957
|
+
|
|
2958
|
+
// The caller still sees the whole explanation.
|
|
2959
|
+
expect(err).toBeInstanceOf(BadDataException);
|
|
2960
|
+
expect(err.message).toContain(LONG_PROVIDER_ERROR);
|
|
2961
|
+
|
|
2962
|
+
const failures: Array<WorkspaceNotificationLog> = errorLogs(mocks);
|
|
2963
|
+
expect(failures).toHaveLength(1);
|
|
2964
|
+
expect(failures[0]!.statusMessage).toBe(
|
|
2965
|
+
LONG_PROVIDER_ERROR.substring(0, 500),
|
|
2966
|
+
);
|
|
2967
|
+
expect(failures[0]!.channelName).toBe(LONG_NAME.substring(0, 100));
|
|
2968
|
+
expect(failures[0]!.channelId).toBe(SLACK_CHANNEL_ID);
|
|
2969
|
+
});
|
|
2970
|
+
|
|
2971
|
+
test("a failure with an empty provider error is still logged with a readable status message", async () => {
|
|
2972
|
+
const mocks: Mocks = mockDeps({
|
|
2973
|
+
workspaceType: WorkspaceType.Slack,
|
|
2974
|
+
responses: [
|
|
2975
|
+
makeSendResponse({
|
|
2976
|
+
workspaceType: WorkspaceType.Slack,
|
|
2977
|
+
errors: [
|
|
2978
|
+
{
|
|
2979
|
+
id: SLACK_CHANNEL_ID,
|
|
2980
|
+
name: SLACK_CHANNEL_NAME,
|
|
2981
|
+
error: "",
|
|
2982
|
+
},
|
|
2983
|
+
],
|
|
2984
|
+
}),
|
|
2985
|
+
],
|
|
2986
|
+
});
|
|
2987
|
+
|
|
2988
|
+
await captureError(
|
|
2989
|
+
sendTest({
|
|
2990
|
+
workspaceType: WorkspaceType.Slack,
|
|
2991
|
+
channelId: SLACK_CHANNEL_ID,
|
|
2992
|
+
}),
|
|
2993
|
+
);
|
|
2994
|
+
|
|
2995
|
+
const failures: Array<WorkspaceNotificationLog> = errorLogs(mocks);
|
|
2996
|
+
expect(failures).toHaveLength(1);
|
|
2997
|
+
expect(failures[0]!.statusMessage).toBe("Failed to send message");
|
|
2998
|
+
});
|
|
2999
|
+
|
|
3000
|
+
test("a success log keeps the first 100 characters of the channel id and name", async () => {
|
|
3001
|
+
const mocks: Mocks = mockDeps({
|
|
3002
|
+
workspaceType: WorkspaceType.Slack,
|
|
3003
|
+
responses: [
|
|
3004
|
+
makeSendResponse({
|
|
3005
|
+
workspaceType: WorkspaceType.Slack,
|
|
3006
|
+
threads: [
|
|
3007
|
+
{
|
|
3008
|
+
id: LONG_ID,
|
|
3009
|
+
name: LONG_NAME,
|
|
3010
|
+
threadId: "1726000000.000100",
|
|
3011
|
+
},
|
|
3012
|
+
],
|
|
3013
|
+
}),
|
|
3014
|
+
],
|
|
3015
|
+
});
|
|
3016
|
+
|
|
3017
|
+
await sendTest({
|
|
3018
|
+
workspaceType: WorkspaceType.Slack,
|
|
3019
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3020
|
+
});
|
|
3021
|
+
|
|
3022
|
+
const successes: Array<WorkspaceNotificationLog> = successLogs(mocks);
|
|
3023
|
+
expect(successes).toHaveLength(1);
|
|
3024
|
+
expect(successes[0]!.channelId).toBe(LONG_ID.substring(0, 100));
|
|
3025
|
+
expect(successes[0]!.channelName).toBe(LONG_NAME.substring(0, 100));
|
|
3026
|
+
expect(successes[0]!.threadId).toBe("1726000000.000100");
|
|
3027
|
+
});
|
|
3028
|
+
});
|
|
3029
|
+
|
|
3030
|
+
describe("sendTestNotificationToDestination: a send that went nowhere is a failure", () => {
|
|
3031
|
+
test.each(DESTINATIONS)(
|
|
3032
|
+
"$label: an empty response array",
|
|
3033
|
+
async (destination: DestinationCase): Promise<void> => {
|
|
3034
|
+
const mocks: Mocks = mockDeps({
|
|
3035
|
+
workspaceType: destination.workspaceType,
|
|
3036
|
+
responses: [],
|
|
3037
|
+
});
|
|
3038
|
+
|
|
3039
|
+
/*
|
|
3040
|
+
* A chat's name comes from the captured chats and a Teams channel's
|
|
3041
|
+
* from the team's channel listing, both known before sending. Only a
|
|
3042
|
+
* Slack channel's name would come back from the send, so with nothing
|
|
3043
|
+
* sent it is labelled by its id.
|
|
3044
|
+
*/
|
|
3045
|
+
const expectedName: string =
|
|
3046
|
+
destination.kind === "chat"
|
|
3047
|
+
? TEAMS_CHAT_NAME
|
|
3048
|
+
: destination.workspaceType === WorkspaceType.Slack
|
|
3049
|
+
? destination.destinationId
|
|
3050
|
+
: TEAMS_CHANNEL_NAME;
|
|
3051
|
+
|
|
3052
|
+
const expectedLabel: string =
|
|
3053
|
+
destination.workspaceType === WorkspaceType.Slack
|
|
3054
|
+
? `#${expectedName}`
|
|
3055
|
+
: `"${expectedName}"`;
|
|
3056
|
+
|
|
3057
|
+
await expectBadData(
|
|
3058
|
+
sendTest(destination.args),
|
|
3059
|
+
reconnectMessage({
|
|
3060
|
+
destinationLabel: expectedLabel,
|
|
3061
|
+
displayName: destination.displayName,
|
|
3062
|
+
}),
|
|
3063
|
+
);
|
|
3064
|
+
|
|
3065
|
+
expect(successLogs(mocks)).toHaveLength(0);
|
|
3066
|
+
|
|
3067
|
+
const failures: Array<WorkspaceNotificationLog> = errorLogs(mocks);
|
|
3068
|
+
expect(failures).toHaveLength(1);
|
|
3069
|
+
expect(failures[0]!.channelId).toBe(destination.destinationId);
|
|
3070
|
+
expect(failures[0]!.channelName).toBe(expectedName);
|
|
3071
|
+
expect(failures[0]!.workspaceType).toBe(destination.workspaceType);
|
|
3072
|
+
expect(failures[0]!.projectId?.toString()).toBe(PROJECT_ID.toString());
|
|
3073
|
+
expect(failures[0]!.userId?.toString()).toBe(TEST_BY_USER_ID.toString());
|
|
3074
|
+
expect(failures[0]!.actionType).toBe(
|
|
3075
|
+
WorkspaceNotificationActionType.SendMessage,
|
|
3076
|
+
);
|
|
3077
|
+
expect(failures[0]!.message).toBe(
|
|
3078
|
+
destination.kind === "chat"
|
|
3079
|
+
? FULL_CHAT_MARKDOWN
|
|
3080
|
+
: FULL_CHANNEL_MARKDOWN,
|
|
3081
|
+
);
|
|
3082
|
+
expect(failures[0]!.statusMessage).toBeTruthy();
|
|
3083
|
+
},
|
|
3084
|
+
);
|
|
3085
|
+
|
|
3086
|
+
test("a response for the workspace with no threads and no errors", async () => {
|
|
3087
|
+
const mocks: Mocks = mockDeps({
|
|
3088
|
+
workspaceType: WorkspaceType.Slack,
|
|
3089
|
+
responses: [makeSendResponse({ workspaceType: WorkspaceType.Slack })],
|
|
3090
|
+
});
|
|
3091
|
+
|
|
3092
|
+
await expectBadData(
|
|
3093
|
+
sendTest({
|
|
3094
|
+
workspaceType: WorkspaceType.Slack,
|
|
3095
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3096
|
+
}),
|
|
3097
|
+
reconnectMessage({
|
|
3098
|
+
destinationLabel: `#${SLACK_CHANNEL_ID}`,
|
|
3099
|
+
displayName: "Slack",
|
|
3100
|
+
}),
|
|
3101
|
+
);
|
|
3102
|
+
|
|
3103
|
+
expect(errorLogs(mocks)).toHaveLength(1);
|
|
3104
|
+
expect(successLogs(mocks)).toHaveLength(0);
|
|
3105
|
+
});
|
|
3106
|
+
|
|
3107
|
+
test("a response with an explicitly empty errors array and no threads", async () => {
|
|
3108
|
+
const response: WorkspaceSendMessageResponse = makeSendResponse({
|
|
3109
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3110
|
+
errors: [],
|
|
3111
|
+
});
|
|
3112
|
+
expect(response.errors).toEqual([]);
|
|
3113
|
+
|
|
3114
|
+
const mocks: Mocks = mockDeps({
|
|
3115
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3116
|
+
responses: [response],
|
|
3117
|
+
});
|
|
3118
|
+
|
|
3119
|
+
await expectBadData(
|
|
3120
|
+
sendTest({
|
|
3121
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3122
|
+
teamId: TEAMS_TEAM_ID,
|
|
3123
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
3124
|
+
}),
|
|
3125
|
+
reconnectMessage({
|
|
3126
|
+
destinationLabel: `"${TEAMS_CHANNEL_NAME}"`,
|
|
3127
|
+
displayName: "Microsoft Teams",
|
|
3128
|
+
}),
|
|
3129
|
+
);
|
|
3130
|
+
|
|
3131
|
+
expect(errorLogs(mocks)).toHaveLength(1);
|
|
3132
|
+
});
|
|
3133
|
+
|
|
3134
|
+
test("a response only for a different workspace type counts as nothing sent", async () => {
|
|
3135
|
+
const mocks: Mocks = mockDeps({
|
|
3136
|
+
workspaceType: WorkspaceType.Slack,
|
|
3137
|
+
responses: [
|
|
3138
|
+
makeSendResponse({
|
|
3139
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3140
|
+
threads: [
|
|
3141
|
+
{ id: TEAMS_CHANNEL_ID, name: "General", threadId: "teams-t" },
|
|
3142
|
+
],
|
|
3143
|
+
}),
|
|
3144
|
+
],
|
|
3145
|
+
});
|
|
3146
|
+
|
|
3147
|
+
await expectBadData(
|
|
3148
|
+
sendTest({
|
|
3149
|
+
workspaceType: WorkspaceType.Slack,
|
|
3150
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3151
|
+
}),
|
|
3152
|
+
reconnectMessage({
|
|
3153
|
+
destinationLabel: `#${SLACK_CHANNEL_ID}`,
|
|
3154
|
+
displayName: "Slack",
|
|
3155
|
+
}),
|
|
3156
|
+
);
|
|
3157
|
+
|
|
3158
|
+
expect(successLogs(mocks)).toHaveLength(0);
|
|
3159
|
+
});
|
|
3160
|
+
|
|
3161
|
+
test("the chat's failure log names the chat", async () => {
|
|
3162
|
+
const mocks: Mocks = mockDeps({
|
|
3163
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3164
|
+
responses: [],
|
|
3165
|
+
});
|
|
3166
|
+
|
|
3167
|
+
await captureError(
|
|
3168
|
+
sendTest({
|
|
3169
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3170
|
+
chatId: TEAMS_CHAT_ID,
|
|
3171
|
+
}),
|
|
3172
|
+
);
|
|
3173
|
+
|
|
3174
|
+
expect(errorLogs(mocks)[0]!.channelName).toBe(TEAMS_CHAT_NAME);
|
|
3175
|
+
});
|
|
3176
|
+
|
|
3177
|
+
test("a failure-log write that fails does not mask the reconnect error", async () => {
|
|
3178
|
+
const mocks: Mocks = mockDeps({
|
|
3179
|
+
workspaceType: WorkspaceType.Slack,
|
|
3180
|
+
responses: [],
|
|
3181
|
+
logWriteFails: true,
|
|
3182
|
+
});
|
|
3183
|
+
|
|
3184
|
+
await expectBadData(
|
|
3185
|
+
sendTest({
|
|
3186
|
+
workspaceType: WorkspaceType.Slack,
|
|
3187
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3188
|
+
}),
|
|
3189
|
+
reconnectMessage({
|
|
3190
|
+
destinationLabel: `#${SLACK_CHANNEL_ID}`,
|
|
3191
|
+
displayName: "Slack",
|
|
3192
|
+
}),
|
|
3193
|
+
);
|
|
3194
|
+
|
|
3195
|
+
expect(mocks.createLogSpy).toHaveBeenCalled();
|
|
3196
|
+
});
|
|
3197
|
+
});
|
|
3198
|
+
|
|
3199
|
+
// ============================================================ send throws
|
|
3200
|
+
|
|
3201
|
+
describe("sendTestNotificationToDestination: the send call itself throws", () => {
|
|
3202
|
+
test("a plain Error becomes a BadDataException with the prefix and the reason", async () => {
|
|
3203
|
+
mockDeps({
|
|
3204
|
+
workspaceType: WorkspaceType.Slack,
|
|
3205
|
+
postRejects: { error: new Error("boom") },
|
|
3206
|
+
});
|
|
3207
|
+
|
|
3208
|
+
await expectBadData(
|
|
3209
|
+
sendTest({
|
|
3210
|
+
workspaceType: WorkspaceType.Slack,
|
|
3211
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3212
|
+
}),
|
|
3213
|
+
`${SEND_FAILED_PREFIX} boom`,
|
|
3214
|
+
);
|
|
3215
|
+
});
|
|
3216
|
+
|
|
3217
|
+
test("a downstream BadDataException keeps its meaningful message, prefixed exactly once", async () => {
|
|
3218
|
+
mockDeps({
|
|
3219
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3220
|
+
postRejects: {
|
|
3221
|
+
error: new BadDataException(
|
|
3222
|
+
"Team ID is required to send message to Microsoft Teams channel.",
|
|
3223
|
+
),
|
|
3224
|
+
},
|
|
3225
|
+
});
|
|
3226
|
+
|
|
3227
|
+
/*
|
|
3228
|
+
* Pinned exactly: rethrowing the downstream exception untouched would
|
|
3229
|
+
* still "contain" its text but lose the prefix that tells the admin it
|
|
3230
|
+
* was the send that failed.
|
|
3231
|
+
*/
|
|
3232
|
+
await expectBadData(
|
|
3233
|
+
sendTest({
|
|
3234
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3235
|
+
teamId: TEAMS_TEAM_ID,
|
|
3236
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
3237
|
+
}),
|
|
3238
|
+
`${SEND_FAILED_PREFIX} Team ID is required to send message to Microsoft Teams channel.`,
|
|
3239
|
+
);
|
|
3240
|
+
});
|
|
3241
|
+
|
|
3242
|
+
test("a BadDataException that already carries the prefix is not double-prefixed", async () => {
|
|
3243
|
+
mockDeps({
|
|
3244
|
+
workspaceType: WorkspaceType.Slack,
|
|
3245
|
+
postRejects: {
|
|
3246
|
+
error: new BadDataException(
|
|
3247
|
+
`${SEND_FAILED_PREFIX} The bot token has been revoked.`,
|
|
3248
|
+
),
|
|
3249
|
+
},
|
|
3250
|
+
});
|
|
3251
|
+
|
|
3252
|
+
const err: Error = await captureError(
|
|
3253
|
+
sendTest({
|
|
3254
|
+
workspaceType: WorkspaceType.Slack,
|
|
3255
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3256
|
+
}),
|
|
3257
|
+
);
|
|
3258
|
+
|
|
3259
|
+
expect(err).toBeInstanceOf(BadDataException);
|
|
3260
|
+
expect(countOccurrences(err.message, SEND_FAILED_PREFIX)).toBe(1);
|
|
3261
|
+
expect(err.message).toContain("The bot token has been revoked.");
|
|
3262
|
+
});
|
|
3263
|
+
|
|
3264
|
+
test("a thrown string still produces a readable message", async () => {
|
|
3265
|
+
mockDeps({
|
|
3266
|
+
workspaceType: WorkspaceType.Slack,
|
|
3267
|
+
postRejects: { error: "socket hang up" },
|
|
3268
|
+
});
|
|
3269
|
+
|
|
3270
|
+
const err: Error = await expectBadData(
|
|
3271
|
+
sendTest({
|
|
3272
|
+
workspaceType: WorkspaceType.Slack,
|
|
3273
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3274
|
+
}),
|
|
3275
|
+
/socket hang up/,
|
|
3276
|
+
);
|
|
3277
|
+
|
|
3278
|
+
expect(err.message.startsWith(SEND_FAILED_PREFIX)).toBe(true);
|
|
3279
|
+
});
|
|
3280
|
+
|
|
3281
|
+
test("a thrown plain object with a message is not '[object Object]'", async () => {
|
|
3282
|
+
mockDeps({
|
|
3283
|
+
workspaceType: WorkspaceType.Slack,
|
|
3284
|
+
postRejects: { error: { message: "invalid_auth", statusCode: 401 } },
|
|
3285
|
+
});
|
|
3286
|
+
|
|
3287
|
+
const err: Error = await expectBadData(
|
|
3288
|
+
sendTest({
|
|
3289
|
+
workspaceType: WorkspaceType.Slack,
|
|
3290
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3291
|
+
}),
|
|
3292
|
+
`${SEND_FAILED_PREFIX} invalid_auth`,
|
|
3293
|
+
);
|
|
3294
|
+
|
|
3295
|
+
expect(err.message).not.toContain("[object Object]");
|
|
3296
|
+
});
|
|
3297
|
+
|
|
3298
|
+
/*
|
|
3299
|
+
* Slack's send helpers `throw response` - an HTTPErrorResponse, which is
|
|
3300
|
+
* not an Error but has a `message` getter - so a 429 or 5xx must surface
|
|
3301
|
+
* its message rather than "[object Object]".
|
|
3302
|
+
*/
|
|
3303
|
+
test("a thrown HTTPErrorResponse yields its message, prefixed once", async () => {
|
|
3304
|
+
mockDeps({
|
|
3305
|
+
workspaceType: WorkspaceType.Slack,
|
|
3306
|
+
postRejects: {
|
|
3307
|
+
error: new HTTPErrorResponse(429, { error: "ratelimited" }, {}),
|
|
3308
|
+
},
|
|
3309
|
+
});
|
|
3310
|
+
|
|
3311
|
+
const err: Error = await expectBadData(
|
|
3312
|
+
sendTest({
|
|
3313
|
+
workspaceType: WorkspaceType.Slack,
|
|
3314
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3315
|
+
}),
|
|
3316
|
+
`${SEND_FAILED_PREFIX} ratelimited`,
|
|
3317
|
+
);
|
|
3318
|
+
|
|
3319
|
+
expect(err.message).not.toContain("[object Object]");
|
|
3320
|
+
expect(countOccurrences(err.message, SEND_FAILED_PREFIX)).toBe(1);
|
|
3321
|
+
});
|
|
3322
|
+
|
|
3323
|
+
test("a thrown HTTPErrorResponse with a nested Graph error yields the nested message", async () => {
|
|
3324
|
+
mockDeps({
|
|
3325
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3326
|
+
postRejects: {
|
|
3327
|
+
error: new HTTPErrorResponse(
|
|
3328
|
+
503,
|
|
3329
|
+
{
|
|
3330
|
+
error: {
|
|
3331
|
+
code: "ServiceUnavailable",
|
|
3332
|
+
message: "The service is temporarily unavailable.",
|
|
3333
|
+
},
|
|
3334
|
+
},
|
|
3335
|
+
{},
|
|
3336
|
+
),
|
|
3337
|
+
},
|
|
3338
|
+
});
|
|
3339
|
+
|
|
3340
|
+
const err: Error = await expectBadData(
|
|
3341
|
+
sendTest({
|
|
3342
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3343
|
+
teamId: TEAMS_TEAM_ID,
|
|
3344
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
3345
|
+
}),
|
|
3346
|
+
`${SEND_FAILED_PREFIX} The service is temporarily unavailable.`,
|
|
3347
|
+
);
|
|
3348
|
+
|
|
3349
|
+
expect(err.message).not.toContain("[object Object]");
|
|
3350
|
+
});
|
|
3351
|
+
|
|
3352
|
+
test("a thrown HTTPErrorResponse with no body message reports its HTTP status", async () => {
|
|
3353
|
+
mockDeps({
|
|
3354
|
+
workspaceType: WorkspaceType.Slack,
|
|
3355
|
+
postRejects: { error: new HTTPErrorResponse(500, {}, {}) },
|
|
3356
|
+
});
|
|
3357
|
+
|
|
3358
|
+
await expectBadData(
|
|
3359
|
+
sendTest({
|
|
3360
|
+
workspaceType: WorkspaceType.Slack,
|
|
3361
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3362
|
+
}),
|
|
3363
|
+
`${SEND_FAILED_PREFIX} Request failed with HTTP status 500`,
|
|
3364
|
+
);
|
|
3365
|
+
});
|
|
3366
|
+
|
|
3367
|
+
test("an Error with no message still yields a clean, meaningful message", async () => {
|
|
3368
|
+
mockDeps({
|
|
3369
|
+
workspaceType: WorkspaceType.Slack,
|
|
3370
|
+
postRejects: { error: new Error("") },
|
|
3371
|
+
});
|
|
3372
|
+
|
|
3373
|
+
const err: Error = await captureError(
|
|
3374
|
+
sendTest({
|
|
3375
|
+
workspaceType: WorkspaceType.Slack,
|
|
3376
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3377
|
+
}),
|
|
3378
|
+
);
|
|
3379
|
+
|
|
3380
|
+
expect(err).toBeInstanceOf(BadDataException);
|
|
3381
|
+
expect(err.message).toBe(`${SEND_FAILED_PREFIX} ${UNKNOWN_ERROR_REASON}`);
|
|
3382
|
+
});
|
|
3383
|
+
|
|
3384
|
+
test.each<[string, unknown]>([
|
|
3385
|
+
["null", null],
|
|
3386
|
+
["undefined", undefined],
|
|
3387
|
+
["an empty string", ""],
|
|
3388
|
+
])(
|
|
3389
|
+
"a thrown %s yields the unknown-error message",
|
|
3390
|
+
async (_description: string, thrown: unknown): Promise<void> => {
|
|
3391
|
+
const mocks: Mocks = mockDeps({
|
|
3392
|
+
workspaceType: WorkspaceType.Slack,
|
|
3393
|
+
postRejects: { error: thrown },
|
|
3394
|
+
});
|
|
3395
|
+
|
|
3396
|
+
const err: Error = await expectBadData(
|
|
3397
|
+
sendTest({
|
|
3398
|
+
workspaceType: WorkspaceType.Slack,
|
|
3399
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3400
|
+
}),
|
|
3401
|
+
`${SEND_FAILED_PREFIX} ${UNKNOWN_ERROR_REASON}`,
|
|
3402
|
+
);
|
|
3403
|
+
|
|
3404
|
+
expect(err.message).not.toContain("null");
|
|
3405
|
+
expect(err.message).not.toContain("undefined");
|
|
3406
|
+
expect(successLogs(mocks)).toHaveLength(0);
|
|
3407
|
+
},
|
|
3408
|
+
);
|
|
3409
|
+
|
|
3410
|
+
test("a throwing send is not recorded as a success", async () => {
|
|
3411
|
+
const mocks: Mocks = mockDeps({
|
|
3412
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3413
|
+
postRejects: { error: new Error("ECONNRESET") },
|
|
3414
|
+
});
|
|
3415
|
+
|
|
3416
|
+
await captureError(
|
|
3417
|
+
sendTest({
|
|
3418
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3419
|
+
chatId: TEAMS_CHAT_ID,
|
|
3420
|
+
}),
|
|
3421
|
+
);
|
|
3422
|
+
|
|
3423
|
+
expect(mocks.postSpy).toHaveBeenCalledTimes(1);
|
|
3424
|
+
expect(successLogs(mocks)).toHaveLength(0);
|
|
3425
|
+
});
|
|
3426
|
+
});
|
|
3427
|
+
|
|
3428
|
+
// ================================================== best-effort lookups
|
|
3429
|
+
|
|
3430
|
+
describe("sendTestNotificationToDestination: project / user lookups are best-effort", () => {
|
|
3431
|
+
test("a project lookup that throws omits the project fragment and still sends", async () => {
|
|
3432
|
+
const mocks: Mocks = mockDeps({
|
|
3433
|
+
workspaceType: WorkspaceType.Slack,
|
|
3434
|
+
projectLookupFails: true,
|
|
3435
|
+
});
|
|
3436
|
+
|
|
3437
|
+
await sendTest({
|
|
3438
|
+
workspaceType: WorkspaceType.Slack,
|
|
3439
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3440
|
+
});
|
|
3441
|
+
|
|
3442
|
+
const text: string = markdownOf(onlyPayload(mocks));
|
|
3443
|
+
expect(text).not.toContain("from the OneUptime project");
|
|
3444
|
+
expect(text).toContain(` by ${USER_MARKDOWN}`);
|
|
3445
|
+
expect(text).toBe(NO_PROJECT_CHANNEL_MARKDOWN);
|
|
3446
|
+
expect(mocks.loggerErrorSpy).toHaveBeenCalled();
|
|
3447
|
+
expect(successLogs(mocks)).toHaveLength(1);
|
|
3448
|
+
});
|
|
3449
|
+
|
|
3450
|
+
test("a project lookup that returns null omits the project fragment", async () => {
|
|
3451
|
+
const mocks: Mocks = mockDeps({
|
|
3452
|
+
workspaceType: WorkspaceType.Slack,
|
|
3453
|
+
projectName: null,
|
|
3454
|
+
});
|
|
3455
|
+
|
|
3456
|
+
await sendTest({
|
|
3457
|
+
workspaceType: WorkspaceType.Slack,
|
|
3458
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3459
|
+
});
|
|
3460
|
+
|
|
3461
|
+
expect(markdownOf(onlyPayload(mocks))).toBe(NO_PROJECT_CHANNEL_MARKDOWN);
|
|
3462
|
+
});
|
|
3463
|
+
|
|
3464
|
+
test("a project with an empty name omits the project fragment", async () => {
|
|
3465
|
+
const mocks: Mocks = mockDeps({
|
|
3466
|
+
workspaceType: WorkspaceType.Slack,
|
|
3467
|
+
projectName: "",
|
|
3468
|
+
});
|
|
3469
|
+
|
|
3470
|
+
await sendTest({
|
|
3471
|
+
workspaceType: WorkspaceType.Slack,
|
|
3472
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3473
|
+
});
|
|
3474
|
+
|
|
3475
|
+
const text: string = markdownOf(onlyPayload(mocks));
|
|
3476
|
+
expect(text).not.toContain("****");
|
|
3477
|
+
expect(text).toBe(NO_PROJECT_CHANNEL_MARKDOWN);
|
|
3478
|
+
});
|
|
3479
|
+
|
|
3480
|
+
test("a user lookup that throws omits the 'by' fragment and still sends", async () => {
|
|
3481
|
+
const mocks: Mocks = mockDeps({
|
|
3482
|
+
workspaceType: WorkspaceType.Slack,
|
|
3483
|
+
userLookupFails: true,
|
|
3484
|
+
});
|
|
3485
|
+
|
|
3486
|
+
await sendTest({
|
|
3487
|
+
workspaceType: WorkspaceType.Slack,
|
|
3488
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3489
|
+
});
|
|
3490
|
+
|
|
3491
|
+
const text: string = markdownOf(onlyPayload(mocks));
|
|
3492
|
+
expect(text).not.toContain(" by ");
|
|
3493
|
+
expect(text).toContain(`from the OneUptime project **${PROJECT_NAME}**`);
|
|
3494
|
+
expect(text).toBe(NO_USER_CHANNEL_MARKDOWN);
|
|
3495
|
+
expect(mocks.loggerErrorSpy).toHaveBeenCalled();
|
|
3496
|
+
expect(successLogs(mocks)).toHaveLength(1);
|
|
3497
|
+
});
|
|
3498
|
+
|
|
3499
|
+
test("a user lookup that returns an empty string omits the 'by' fragment", async () => {
|
|
3500
|
+
const mocks: Mocks = mockDeps({
|
|
3501
|
+
workspaceType: WorkspaceType.Slack,
|
|
3502
|
+
userMarkdown: "",
|
|
3503
|
+
});
|
|
3504
|
+
|
|
3505
|
+
await sendTest({
|
|
3506
|
+
workspaceType: WorkspaceType.Slack,
|
|
3507
|
+
channelId: SLACK_CHANNEL_ID,
|
|
3508
|
+
});
|
|
3509
|
+
|
|
3510
|
+
expect(markdownOf(onlyPayload(mocks))).toBe(NO_USER_CHANNEL_MARKDOWN);
|
|
3511
|
+
});
|
|
3512
|
+
|
|
3513
|
+
test("both lookups failing still sends the bare message", async () => {
|
|
3514
|
+
const mocks: Mocks = mockDeps({
|
|
3515
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3516
|
+
projectLookupFails: true,
|
|
3517
|
+
userLookupFails: true,
|
|
3518
|
+
});
|
|
3519
|
+
|
|
3520
|
+
const thread: WorkspaceThread = await sendTest({
|
|
3521
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3522
|
+
teamId: TEAMS_TEAM_ID,
|
|
3523
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
3524
|
+
});
|
|
3525
|
+
|
|
3526
|
+
expect(thread).toBeDefined();
|
|
3527
|
+
expect(markdownOf(onlyPayload(mocks))).toBe(BARE_CHANNEL_MARKDOWN);
|
|
3528
|
+
expect(loggedEntries(mocks)[0]!.message).toBe(BARE_CHANNEL_MARKDOWN);
|
|
3529
|
+
});
|
|
3530
|
+
|
|
3531
|
+
test("a chat test with both lookups failing still says 'this chat'", async () => {
|
|
3532
|
+
const mocks: Mocks = mockDeps({
|
|
3533
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3534
|
+
projectName: null,
|
|
3535
|
+
userMarkdown: "",
|
|
3536
|
+
});
|
|
3537
|
+
|
|
3538
|
+
await sendTest({
|
|
3539
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3540
|
+
chatId: TEAMS_CHAT_ID,
|
|
3541
|
+
});
|
|
3542
|
+
|
|
3543
|
+
expect(markdownOf(onlyPayload(mocks))).toBe(
|
|
3544
|
+
"**Test notification from OneUptime**\n\n" +
|
|
3545
|
+
"This is a test notification sent. " +
|
|
3546
|
+
"If you can see this message, OneUptime can post notifications to this chat. No action is needed.",
|
|
3547
|
+
);
|
|
3548
|
+
});
|
|
3549
|
+
});
|
|
3550
|
+
|
|
3551
|
+
// ================================================== success-log failure
|
|
3552
|
+
|
|
3553
|
+
describe("sendTestNotificationToDestination: a success-log write failure is not a send failure", () => {
|
|
3554
|
+
test.each(DESTINATIONS)(
|
|
3555
|
+
"$label: still resolves with the thread",
|
|
3556
|
+
async (destination: DestinationCase): Promise<void> => {
|
|
3557
|
+
const mocks: Mocks = mockDeps({
|
|
3558
|
+
workspaceType: destination.workspaceType,
|
|
3559
|
+
logWriteFails: true,
|
|
3560
|
+
responses: [
|
|
3561
|
+
makeSendResponse({
|
|
3562
|
+
workspaceType: destination.workspaceType,
|
|
3563
|
+
threads: [destination.thread],
|
|
3564
|
+
}),
|
|
3565
|
+
],
|
|
3566
|
+
});
|
|
3567
|
+
|
|
3568
|
+
const thread: WorkspaceThread = await sendTest(destination.args);
|
|
3569
|
+
|
|
3570
|
+
expect(thread.threadId).toBe(destination.thread.threadId);
|
|
3571
|
+
expect(thread.channel.id).toBe(destination.thread.id);
|
|
3572
|
+
expect(mocks.postSpy).toHaveBeenCalledTimes(1);
|
|
3573
|
+
expect(mocks.createLogSpy).toHaveBeenCalledTimes(1);
|
|
3574
|
+
expect(mocks.loggerErrorSpy).toHaveBeenCalled();
|
|
3575
|
+
},
|
|
3576
|
+
);
|
|
3577
|
+
});
|
|
3578
|
+
|
|
3579
|
+
// =============================================== isolation from testRule
|
|
3580
|
+
|
|
3581
|
+
describe("sendTestNotificationToDestination: never touches the testRule machinery", () => {
|
|
3582
|
+
test.each(DESTINATIONS)(
|
|
3583
|
+
"$label: no rule is loaded, no channel created, nobody invited",
|
|
3584
|
+
async (destination: DestinationCase): Promise<void> => {
|
|
3585
|
+
const mocks: Mocks = mockDeps({
|
|
3586
|
+
workspaceType: destination.workspaceType,
|
|
3587
|
+
responses: [
|
|
3588
|
+
makeSendResponse({
|
|
3589
|
+
workspaceType: destination.workspaceType,
|
|
3590
|
+
threads: [destination.thread],
|
|
3591
|
+
}),
|
|
3592
|
+
],
|
|
3593
|
+
});
|
|
3594
|
+
|
|
3595
|
+
await sendTest(destination.args);
|
|
3596
|
+
|
|
3597
|
+
expect(mocks.testRuleSpy).not.toHaveBeenCalled();
|
|
3598
|
+
expect(mocks.ruleFindOneByIdSpy).not.toHaveBeenCalled();
|
|
3599
|
+
expect(mocks.createChannelsSpy).not.toHaveBeenCalled();
|
|
3600
|
+
expect(mocks.inviteUsersSpy).not.toHaveBeenCalled();
|
|
3601
|
+
},
|
|
3602
|
+
);
|
|
3603
|
+
|
|
3604
|
+
test("a failing destination test does not fall back to the testRule machinery", async () => {
|
|
3605
|
+
const mocks: Mocks = mockDeps({
|
|
3606
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3607
|
+
responses: [],
|
|
3608
|
+
});
|
|
3609
|
+
|
|
3610
|
+
await captureError(
|
|
3611
|
+
sendTest({
|
|
3612
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
3613
|
+
teamId: TEAMS_TEAM_ID,
|
|
3614
|
+
channelId: TEAMS_CHANNEL_ID,
|
|
3615
|
+
}),
|
|
3616
|
+
);
|
|
3617
|
+
|
|
3618
|
+
expect(mocks.testRuleSpy).not.toHaveBeenCalled();
|
|
3619
|
+
expect(mocks.createChannelsSpy).not.toHaveBeenCalled();
|
|
3620
|
+
expect(mocks.inviteUsersSpy).not.toHaveBeenCalled();
|
|
3621
|
+
});
|
|
3622
|
+
});
|
|
3623
|
+
|
|
3624
|
+
// ======================================================== all destinations
|
|
3625
|
+
|
|
3626
|
+
describe("sendTestNotificationToDestination: invariants for every destination", () => {
|
|
3627
|
+
test.each(DESTINATIONS)(
|
|
3628
|
+
"$label: one send, one payload, one Success log, thread returned",
|
|
3629
|
+
async (destination: DestinationCase): Promise<void> => {
|
|
3630
|
+
const mocks: Mocks = mockDeps({
|
|
3631
|
+
workspaceType: destination.workspaceType,
|
|
3632
|
+
responses: [
|
|
3633
|
+
makeSendResponse({
|
|
3634
|
+
workspaceType: destination.workspaceType,
|
|
3635
|
+
threads: [destination.thread],
|
|
3636
|
+
}),
|
|
3637
|
+
],
|
|
3638
|
+
});
|
|
3639
|
+
|
|
3640
|
+
const thread: WorkspaceThread = await sendTest(destination.args);
|
|
3641
|
+
|
|
3642
|
+
const payload: WorkspaceMessagePayload = onlyPayload(mocks);
|
|
3643
|
+
expect(payload.workspaceType).toBe(destination.workspaceType);
|
|
3644
|
+
expect(markdownOf(payload)).toContain(`this ${destination.kind}`);
|
|
3645
|
+
|
|
3646
|
+
const addressed: Array<string> = [
|
|
3647
|
+
...(payload.channelIds || []),
|
|
3648
|
+
...(payload.chatIds || []),
|
|
3649
|
+
];
|
|
3650
|
+
expect(addressed).toEqual([destination.destinationId]);
|
|
3651
|
+
|
|
3652
|
+
expect(thread.threadId).toBe(destination.thread.threadId);
|
|
3653
|
+
expect(mocks.createLogSpy).toHaveBeenCalledTimes(1);
|
|
3654
|
+
expect(successLogs(mocks)).toHaveLength(1);
|
|
3655
|
+
expect(errorLogs(mocks)).toHaveLength(0);
|
|
3656
|
+
|
|
3657
|
+
// Only a Microsoft Teams channel needs the team's channel listing.
|
|
3658
|
+
if (
|
|
3659
|
+
destination.workspaceType === WorkspaceType.MicrosoftTeams &&
|
|
3660
|
+
destination.kind === "channel"
|
|
3661
|
+
) {
|
|
3662
|
+
expectTeamListedOnce(mocks, TEAMS_TEAM_ID);
|
|
3663
|
+
} else {
|
|
3664
|
+
expect(mocks.listTeamChannelsSpy).not.toHaveBeenCalled();
|
|
3665
|
+
}
|
|
3666
|
+
},
|
|
3667
|
+
);
|
|
3668
|
+
|
|
3669
|
+
test.each(DESTINATIONS)(
|
|
3670
|
+
"$label: the project auth is loaded before anything is sent",
|
|
3671
|
+
async (destination: DestinationCase): Promise<void> => {
|
|
3672
|
+
const mocks: Mocks = mockDeps({
|
|
3673
|
+
workspaceType: destination.workspaceType,
|
|
3674
|
+
responses: [
|
|
3675
|
+
makeSendResponse({
|
|
3676
|
+
workspaceType: destination.workspaceType,
|
|
3677
|
+
threads: [destination.thread],
|
|
3678
|
+
}),
|
|
3679
|
+
],
|
|
3680
|
+
});
|
|
3681
|
+
|
|
3682
|
+
await sendTest(destination.args);
|
|
3683
|
+
|
|
3684
|
+
const authOrder: number =
|
|
3685
|
+
mocks.getProjectAuthSpy.mock.invocationCallOrder[0]!;
|
|
3686
|
+
const sendOrder: number = mocks.postSpy.mock.invocationCallOrder[0]!;
|
|
3687
|
+
expect(authOrder).toBeLessThan(sendOrder);
|
|
3688
|
+
},
|
|
3689
|
+
);
|
|
3690
|
+
});
|